-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Invite to team fixes, improvements and API implementation (#828)
* Fix issues related to the Team view * Fix accidentaly changed props * Remove unnecessary (and confusing) form default value * Moved utils to new utils folder * Implement invite to team logic (or most of it) - Added new CallbackProvider to easily add success/error callbacks to components * Fix teams list in account not showing team name * Implement pending members list - Moved team members table to a new component and edited that one to use the common component instead - Created a new PendingTeamMembersList component to show the pending members table/list - Created a new wrapper component to have both of them as <TeamMembers /> * Implemented roles call * Fix query cache not properly cleaned when inviting someone Also changed the order of the members table order to be more consistent * Properly select data structure using react-query * Recover roles border radius * Properly show both member lists in team * Set isLoading to team invite form button * Update translations * Accept invitation flow - Created #839 to tackle an issue I've found doing this - Changed how the auth.ts routes were sorted in order to be able to reuse the AuthLayout without requiring an account to not be logged in. - Updated the signup component to be reused in the verification for non-existing accounts. - Minor changes to AuthProvider to have errors in an Enum - Most if not all cases covered, but the verification process needs a revamp: + If the invited user has an unverified account, a page with a button to go to the verify page is shown, but such verify requires the params in the URL, which cannot be get from this point (at all). The verify user needs a revamp so it can be accessed without url params, and show there a button to request a new code if required * Minor translation changes
- Loading branch information
1 parent
c593714
commit 8b61573
Showing
28 changed files
with
1,111 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { Alert, AlertDescription, AlertIcon, Button, Flex, Spinner, Text, useToast } from '@chakra-ui/react' | ||
import { useMutation } from '@tanstack/react-query' | ||
import { ReactNode, useEffect } from 'react' | ||
import { Trans, useTranslation } from 'react-i18next' | ||
import { generatePath, Link as RouterLink, useNavigate, useOutletContext } from 'react-router-dom' | ||
import { api, ApiEndpoints, ApiError, ErrorCode } from '~components/Auth/api' | ||
import SignUp, { InviteFields } from '~components/Auth/SignUp' | ||
import { AuthOutletContextType } from '~elements/LayoutAuth' | ||
import { Routes } from '~src/router/routes' | ||
|
||
const Error = ({ error }: { error: ReactNode }) => ( | ||
<Alert status='error'> | ||
<AlertIcon /> | ||
<AlertDescription>{error}</AlertDescription> | ||
</Alert> | ||
) | ||
|
||
const AcceptInvitation: React.FC<InviteFields> = ({ address, code, email }) => { | ||
const { t } = useTranslation() | ||
const navigate = useNavigate() | ||
const toast = useToast() | ||
const { setTitle, setSubTitle } = useOutletContext<AuthOutletContextType>() | ||
|
||
const acceptInvitationMutation = useMutation({ | ||
mutationFn: ({ code, address }: { code: string; address: string }) => | ||
api(ApiEndpoints.InviteAccept.replace('{address}', address), { | ||
method: 'POST', | ||
body: { code }, | ||
}), | ||
}) | ||
|
||
// Accept the invitation | ||
useEffect(() => { | ||
if ( | ||
!code || | ||
!address || | ||
acceptInvitationMutation.isPending || | ||
acceptInvitationMutation.isError || | ||
acceptInvitationMutation.isSuccess | ||
) | ||
return | ||
|
||
acceptInvitationMutation.mutate({ code, address }) | ||
}, [code, address, acceptInvitationMutation]) | ||
|
||
// Redirect on success | ||
useEffect(() => { | ||
if (!acceptInvitationMutation.isSuccess) return | ||
|
||
toast({ | ||
title: t('invite.success_title', { defaultValue: 'Invitation accepted' }), | ||
description: t('invite.success_description', { defaultValue: 'You can now sign in' }), | ||
status: 'success', | ||
}) | ||
navigate(Routes.auth.signIn) | ||
}, [acceptInvitationMutation.isSuccess]) | ||
|
||
// Change layout title and subtitle | ||
useEffect(() => { | ||
if (!acceptInvitationMutation.isError || !(acceptInvitationMutation.error instanceof ApiError)) return | ||
const error = (acceptInvitationMutation.error as ApiError).apiError | ||
if (error?.code !== ErrorCode.MalformedJSONBody) return | ||
|
||
setTitle(t('invite.create_account_title', { defaultValue: 'Create your account' })) | ||
setSubTitle( | ||
t('invite.create_account_subtitle', { defaultValue: 'You need an account first, in order to accept your invite' }) | ||
) | ||
}, [acceptInvitationMutation.isError]) | ||
|
||
if (!code || !address || !email) { | ||
return <Error error={<Trans i18nKey='invite.invalid_link'>Invalid invite link received</Trans>} /> | ||
} | ||
|
||
if (acceptInvitationMutation.isPending) { | ||
return ( | ||
<Flex justify='center' p={4} gap={3}> | ||
<Spinner /> | ||
<Text> | ||
<Trans i18nKey='invite.processing'>Processing your invitation...</Trans> | ||
</Text> | ||
</Flex> | ||
) | ||
} | ||
|
||
if (acceptInvitationMutation.isError) { | ||
const error = (acceptInvitationMutation.error as ApiError)?.apiError | ||
if (error?.code === ErrorCode.MalformedJSONBody) { | ||
return <SignUp invite={{ address, code, email }} /> | ||
} | ||
|
||
if (error?.code === ErrorCode.UserNotVerified) { | ||
return ( | ||
<Flex direction='column' justify='center' p={4} gap={4}> | ||
<Text> | ||
<Trans i18nKey='invite.account_not_verified'> | ||
Your account is not verified. Please verify your account to continue. | ||
</Trans> | ||
</Text> | ||
<Button as={RouterLink} to={generatePath(Routes.auth.verify)}> | ||
<Trans i18nKey='invite.go_to_verify'>Verify Account</Trans> | ||
</Button> | ||
</Flex> | ||
) | ||
} | ||
|
||
return <Error error={error?.error || t('invite.unexpected_error')} /> | ||
} | ||
|
||
return <Spinner /> | ||
} | ||
|
||
export default AcceptInvitation |
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
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,52 +1,72 @@ | ||
import { Button, Flex, Link, Text } from '@chakra-ui/react' | ||
import { useEffect, useState } from 'react' | ||
import { FormProvider, useForm } from 'react-hook-form' | ||
import { Trans, useTranslation } from 'react-i18next' | ||
import { NavLink, Link as ReactRouterLink, useOutletContext } from 'react-router-dom' | ||
import { Navigate, NavLink, Link as ReactRouterLink } from 'react-router-dom' | ||
import { IRegisterParams } from '~components/Auth/authQueries' | ||
import { useAuth } from '~components/Auth/useAuth' | ||
import { VerifyAccountNeeded } from '~components/Auth/Verify' | ||
import FormSubmitMessage from '~components/Layout/FormSubmitMessage' | ||
import InputPassword from '~components/Layout/InputPassword' | ||
import { AuthOutletContextType } from '~elements/LayoutAuth' | ||
import { useSignupFromInvite } from '~src/queries/account' | ||
import { Routes } from '~src/router/routes' | ||
import CustomCheckbox from '../Layout/CheckboxCustom' | ||
import { default as InputBasic } from '../Layout/InputBasic' | ||
import GoogleAuth from './GoogleAuth' | ||
import { HSeparator } from './SignIn' | ||
|
||
export type InviteFields = { | ||
code: string | ||
address: string | ||
email: string | ||
} | ||
|
||
export type SignupProps = { | ||
invite?: InviteFields | ||
} | ||
|
||
type FormData = { | ||
terms: boolean | ||
} & IRegisterParams | ||
|
||
const SignUp = () => { | ||
const SignUp = ({ invite }: SignupProps) => { | ||
const { t } = useTranslation() | ||
const { setTitle, setSubTitle } = useOutletContext<AuthOutletContextType>() | ||
|
||
const { | ||
register: { mutateAsync: signup, isError, error, isPending }, | ||
} = useAuth() | ||
|
||
const methods = useForm<FormData>() | ||
const { register } = useAuth() | ||
const inviteSignup = useSignupFromInvite(invite?.address) | ||
const methods = useForm<FormData>({ | ||
defaultValues: { | ||
terms: false, | ||
email: invite?.email, | ||
}, | ||
}) | ||
const { handleSubmit, watch } = methods | ||
const email = watch('email') | ||
|
||
// State to show signup is successful | ||
const [isSuccess, setIsSuccess] = useState(false) | ||
const isPending = register.isPending || inviteSignup.isPending | ||
const isError = register.isError || inviteSignup.isError | ||
const error = register.error || inviteSignup.error | ||
|
||
useEffect(() => { | ||
setTitle(t('signup_title')) | ||
setSubTitle(t('signup_subtitle')) | ||
}, []) | ||
const onSubmit = (user: FormData) => { | ||
if (!invite) { | ||
return register.mutate(user) | ||
} | ||
|
||
const onSubmit = async (data: FormData) => { | ||
await signup(data).then(() => setIsSuccess(true)) | ||
// if there's an invite, the process' a bit different | ||
return inviteSignup.mutate({ | ||
code: invite.code, | ||
user, | ||
}) | ||
} | ||
|
||
if (isSuccess) { | ||
// normally registered accounts need verification | ||
if (register.isSuccess) { | ||
return <VerifyAccountNeeded email={email} /> | ||
} | ||
|
||
// accounts coming from invites don't need verification | ||
if (inviteSignup.isSuccess) { | ||
return <Navigate to={Routes.auth.signIn} /> | ||
} | ||
|
||
return ( | ||
<> | ||
<GoogleAuth /> | ||
|
@@ -69,6 +89,7 @@ const SignUp = () => { | |
placeholder={t('email_placeholder', { defaultValue: '[email protected]' })} | ||
type='email' | ||
required | ||
isDisabled={!!invite} | ||
/> | ||
<InputPassword | ||
formValue='password' | ||
|
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
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
Oops, something went wrong.
8b61573
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://673ca27cc63a12310345d5e9--vocdoni-app-stg.netlify.app
8b61573
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://673ca27e2bddcd31760e4ec3--vocdoni-app-dev.netlify.app