-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move AccountSaas folder contents to Account
- Loading branch information
1 parent
02ebfce
commit 00bac43
Showing
20 changed files
with
325 additions
and
760 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} /> | ||
} |
Oops, something went wrong.
00bac43
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Published on https://vocdoni-app-stg.netlify.app as production
π Deployed on https://671653248771da309a505a43--vocdoni-app-stg.netlify.app
00bac43
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Published on https://vocdoni-app-dev.netlify.app as production
π Deployed on https://6716532add30272be4985ee7--vocdoni-app-dev.netlify.app