Skip to content

Commit

Permalink
Move AccountSaas folder contents to Account
Browse files Browse the repository at this point in the history
  • Loading branch information
elboletaire committed Oct 21, 2024
1 parent 02ebfce commit 00bac43
Show file tree
Hide file tree
Showing 20 changed files with 325 additions and 760 deletions.
2 changes: 1 addition & 1 deletion src/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { EnvOptions } from '@vocdoni/sdk'
import { PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import { useAccount, useWalletClient, WagmiConfig } from 'wagmi'
import { SaasAccountProvider } from '~components/AccountSaas/SaasAccountContext'
import { SaasAccountProvider } from '~components/Account/SaasAccountContext'
import { AuthProvider } from '~components/Auth/AuthContext'
import { walletClientToSigner } from '~constants/wagmi-adapters'
import { VocdoniEnvironment } from './constants'
Expand Down
File renamed without changes.
318 changes: 104 additions & 214 deletions src/components/Account/Create.tsx
Original file line number Diff line number Diff line change
@@ -1,227 +1,117 @@
import {
Alert,
AlertIcon,
Box,
Flex,
FlexProps,
FormControl,
FormErrorMessage,
FormLabel,
Icon,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Spinner,
Stack,
Text,
Textarea,
useDisclosure,
} from '@chakra-ui/react'
import { Box, Flex, FlexProps, Heading, Text } from '@chakra-ui/react'
import { Button } from '@vocdoni/chakra-components'
import { useClient } from '@vocdoni/react-providers'
import { useCallback, useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { useAccount } from 'wagmi'
import { CreateAccountParams, useAccountCreate } from '~components/Account/useAccountCreate'
import { ucfirst } from '~constants/strings'
import { Check, Close } from '~theme/icons'
import { useAccountHealthTools } from './use-account-health-tools'
import hello from '/shared/hello.jpeg'

export const AccountCreate = ({ children, ...props }: FlexProps) => {
const { t } = useTranslation()
import { FormProvider, useForm } from 'react-hook-form'
import { Trans, useTranslation } from 'react-i18next'

const {
register,
handleSubmit,
formState: { errors },
} = useForm({
defaultValues: {
name: '',
description: '',
},
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { useClient } from '@vocdoni/react-providers'
import { useState } from 'react'
import { CreateOrgParams } from '~components/Account/AccountTypes'
import { PrivateOrgForm, PrivateOrgFormData, PublicOrgForm } from '~components/Account/Layout'
import LogoutBtn from '~components/Account/LogoutBtn'
import { useAccountCreate } from '~components/Account/useAccountCreate'
import { ApiEndpoints } from '~components/Auth/api'
import { useAuth } from '~components/Auth/useAuth'
import FormSubmitMessage from '~components/Layout/FormSubmitMessage'
import useDarkMode from '~components/Layout/useDarkMode'

type FormData = PrivateOrgFormData & CreateOrgParams

// This specific error message should be ignored and not displayed in the UI.
// Context: After login, a RemoteSigner is created and passed to the SDK via the useClient hook.
// Immediately following this, the provider attempts to fetch the signer's address. However,
// at this point, the signer has not yet been associated with any organization.
// As a result, the backend returns an error, which is stored in the provider's state.
// We rely on this error message for handling because no error code is provided,
// and the error is not thrown as an exception.
const IgnoreAccountError = 'this user has not been assigned to any organization'

const useSaasAccountCreate = (options?: Omit<UseMutationOptions<void, Error, CreateOrgParams>, 'mutationFn'>) => {
const { bearedFetch } = useAuth()
return useMutation<void, Error, CreateOrgParams>({
mutationFn: (params: CreateOrgParams) =>
bearedFetch<void>(ApiEndpoints.Organizations, { body: params, method: 'POST' }),
...options,
})

const { create, error } = useAccountCreate()

const [sent, setSent] = useState<boolean>(false)

const required = {
value: true,
message: t('form.error.field_is_required'),
}

const onSubmit = async (values: CreateAccountParams) => {
return create(values)?.finally(() => setSent(true))
}

return (
<Flex
as='form'
id='process-create-form'
direction='column'
gap={6}
{...props}
onSubmit={(e) => {
e.stopPropagation()
e.preventDefault()
handleSubmit(onSubmit)(e)
}}
>
{children}
<Box px={{ base: 5, md: 10 }} pb={10}>
<FormControl isInvalid={!!errors.name} mb={5}>
<FormLabel className='brand-theme' fontWeight='bold' textTransform='uppercase'>
{t('new_organization.name')}
</FormLabel>
<Input
type='text'
{...register('name', { required })}
mb={1}
placeholder={t('form.account_create.title_placeholder').toString()}
/>
{!!errors.name && (
<FormErrorMessage>{errors.name?.message || 'Error performing the operation'}</FormErrorMessage>
)}
</FormControl>

<FormControl mb={5}>
<Textarea
{...register('description')}
placeholder={t('form.account_create.description_placeholder').toString()}
/>
</FormControl>
</Box>

{sent && error && (
<Alert status='error'>
<AlertIcon />
{error}
</Alert>
)}
</Flex>
)
}

export const BasicAccountCreation = () => {
const { isOpen, onOpen, onClose } = useDisclosure()
const { isConnected } = useAccount()
const {
client,
account,
createAccount,
loaded: { fetch: loaded },
errors: { create: error },
} = useClient()
const { exists } = useAccountHealthTools()
export const AccountCreate = ({ children, ...props }: FlexProps) => {
const { t } = useTranslation()
const [creating, setCreating] = useState<boolean>(false)

// site name, to be used in translations
let sitename = 'Vocdoni'
if (import.meta.env.theme !== 'default') {
sitename = ucfirst(import.meta.env.theme)
const [isPending, setIsPending] = useState(false)
const { textColor, textColorSecondary } = useDarkMode()

const methods = useForm<FormData>()
const { handleSubmit } = methods

const { signer } = useClient()

const { create: createAccount, error: accountError } = useAccountCreate()
const { mutateAsync: createSaasAccount, isError: isSaasError, error: saasError } = useSaasAccountCreate()

const error = saasError || accountError

const onSubmit = (values: FormData) => {
setIsPending(true)
// Create account on the saas to generate new priv keys
createSaasAccount({
name: values.name,
website: values.website,
description: values.description,
size: values.sizeSelect?.value,
country: values.countrySelect?.value,
type: values.typeSelect?.value,
})
.then(() => signer.getAddress()) // Get the address of newly created signer
.then(() =>
createAccount({
name: typeof values.name === 'object' ? values.name.default : values.name,
description: typeof values.description === 'object' ? values.description.default : values.description,
})
) // Create the new account on the vochain
.finally(() => setIsPending(false))
}

// create account logic (used both in effect and retry button)
const create = useCallback(async () => {
if (creating) return

setCreating(true)
await createAccount()
setCreating(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [creating, client.wallet, isConnected, exists, account])

// open modal and init create for the first time
useEffect(() => {
if (!isConnected || (isConnected && exists) || !client.wallet || creating || !loaded) return
;(async () => {
onOpen()
create()
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isConnected, exists, account, client.wallet, loaded])

// close modal after successfully creating account
useEffect(() => {
if (!isConnected || !exists || !isOpen || !account) return
onClose()
}, [isConnected, exists, account, isOpen, onClose])
const isError = (!!accountError || isSaasError) && error !== IgnoreAccountError

return (
<>
<Modal isOpen={isOpen} onClose={onClose} closeOnEsc={!!error} closeOnOverlayClick={!!error}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{t('welcome.title', { sitename })}</ModalHeader>
{!!error && <ModalCloseButton />}
<ModalBody>
<Box
className='welcome-modal'
bgImage={hello}
bgRepeat='no-repeat'
bgPosition='top center'
bgSize='100%'
mb={5}
borderRadius='10px'
boxShadow='3px 3px 20px lightgray'
height='150px'
/>
<Stack gap={4}>
<Text>{t('welcome.intro', { sitename })}</Text>
<Text>{t('welcome.description', { sitename })}</Text>
<Stack mt={4}>
<Flex flexDir='row'>
<StateIcon creating={creating} error={error} />
{t('welcome.step.register')}
</Flex>
<Flex flexDir='row'>
<StateIcon creating={creating} error={error} />
{t('welcome.step.sik')}
</Flex>
</Stack>
{error && <Text color='error'>{error}</Text>}
</Stack>
</ModalBody>

{error && (
<ModalFooter>
<Button variant='ghost' onClick={onClose}>
{t('close')}
</Button>
<Button mr={3} onClick={create} isLoading={creating}>
{t('retry')}
</Button>
</ModalFooter>
)}
</ModalContent>
</Modal>
</>
<FormProvider {...methods}>
<Flex
as='form'
id='process-create-form'
direction='column'
gap={6}
maxW='90%'
mx='auto'
{...props}
onSubmit={(e) => {
e.stopPropagation()
e.preventDefault()
handleSubmit(onSubmit)(e)
}}
>
{children}
<Box me='auto'>
<Heading color={textColor} fontSize='36px' mb={4}>
<Trans i18nKey='create_org.title'>Create Your Organization</Trans>
</Heading>
</Box>
<PublicOrgForm />
<PrivateOrgForm />
<Button form='process-create-form' type='submit' isLoading={isPending} mx='auto' mt={8} w='80%'>
{t('organization.create_org')}
</Button>
<FormSubmitMessage isError={isError} error={error} />
<Text color={textColorSecondary} fontSize='sm' textAlign='center' py={5} mt='auto'>
<Trans i18nKey='create_org.already_profile'>
If your organization already have a profile, ask the admin to invite you to your organization.
</Trans>
</Text>
<Text color={textColorSecondary} fontSize='sm'>
<Trans i18nKey='create_org.logout'>If you want to login from another account, please logout</Trans>
</Text>
<LogoutBtn />
</Flex>
</FormProvider>
)
}

const StateIcon = ({ creating, error }: { creating: boolean; error: string | null }) => {
const style = {
alignSelf: 'center',
mr: 1,
width: 4,
height: 4,
}

if (creating) {
return <Spinner {...style} />
}

if (error) {
return <Icon as={Close} {...style} />
}

return <Icon as={Check} {...style} />
}
Loading

2 comments on commit 00bac43

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.