Add Email Verification for non-Microsoft/Google Emails (#9288)

Closes twentyhq/twenty#8240 

This PR introduces email verification for non-Microsoft/Google Emails:

## Email Verification SignInUp Flow:

https://github.com/user-attachments/assets/740e9714-5413-4fd8-b02e-ace728ea47ef

The email verification link is sent as part of the
`SignInUpStep.EmailVerification`. The email verification token
validation is handled on a separate page (`AppPath.VerifyEmail`). A
verification email resend can be triggered from both pages.

## Email Verification Flow Screenshots (In Order):

![image](https://github.com/user-attachments/assets/d52237dc-fcc6-4754-a40f-b7d6294eebad)

![image](https://github.com/user-attachments/assets/263a4b6b-db49-406b-9e43-6c0f90488bb8)

![image](https://github.com/user-attachments/assets/0343ae51-32ef-48b8-8167-a96deb7db99e)

## Sent Email Details (Subject & Template):
![Screenshot 2025-01-05 at 11 56
56 PM](https://github.com/user-attachments/assets/475840d1-7d47-4792-b8c6-5c9ef5e02229)

![image](https://github.com/user-attachments/assets/a41b3b36-a36f-4a8e-b1f9-beeec7fe23e4)

### Successful Email Verification Redirect:

![image](https://github.com/user-attachments/assets/e2fad9e2-f4b1-485e-8f4a-32163c2718e7)

### Unsuccessful Email Verification (invalid token, invalid email, token
expired, user does not exist, etc.):

![image](https://github.com/user-attachments/assets/92f4b65e-2971-4f26-a9fa-7aafadd2b305)

### Force Sign In When Email Not Verified:

![image](https://github.com/user-attachments/assets/86d0f188-cded-49a6-bde9-9630fd18d71e)

# TODOs:

## Sign Up Process

- [x] Introduce server-level environment variable
IS_EMAIL_VERIFICATION_REQUIRED (defaults to false)
- [x] Ensure users joining an existing workspace through an invite are
not required to validate their email
- [x] Generate an email verification token
- [x] Store the token in appToken
- [x] Send email containing the verification link
  - [x] Create new email template for email verification
- [x] Create a frontend page to handle verification requests

## Sign In Process

- [x] After verifying user credentials, check if user's email is
verified and prompt to to verify
- [x] Show an option to resend the verification email

## Database

- [x] Rename the `emailVerified` colum on `user` to to `isEmailVerified`
for consistency

## During Deployment
- [x] Run a script/sql query to set `isEmailVerified` to `true` for all
users with a Google/Microsoft email and all users that show an
indication of a valid subscription (e.g. linked credit card)
- I have created a draft migration file below that shows one possible
approach to implementing this change:

```typescript
import { MigrationInterface, QueryRunner } from 'typeorm';

export class UpdateEmailVerifiedForActiveUsers1733318043628
  implements MigrationInterface
{
  name = 'UpdateEmailVerifiedForActiveUsers1733318043628';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      CREATE TABLE core."user_email_verified_backup" AS
      SELECT id, email, "isEmailVerified"
      FROM core."user"
      WHERE "deletedAt" IS NULL;
    `);

    await queryRunner.query(`
      -- Update isEmailVerified for users who have been part of workspaces with active subscriptions
      UPDATE core."user" u
      SET "isEmailVerified" = true
      WHERE EXISTS (
        -- Check if user has been part of a workspace through userWorkspace table
        SELECT 1 
        FROM core."userWorkspace" uw
        JOIN core."workspace" w ON uw."workspaceId" = w.id
        WHERE uw."userId" = u.id
        -- Check for valid subscription indicators
        AND (
          w."activationStatus" = 'ACTIVE'
          -- Add any other subscription-related conditions here
        )
      )
      AND u."deletedAt" IS NULL;
  `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      UPDATE core."user" u
      SET "isEmailVerified" = b."isEmailVerified"
      FROM core."user_email_verified_backup" b
      WHERE u.id = b.id;
    `);

    await queryRunner.query(`DROP TABLE core."user_email_verified_backup";`);
  }
}

```

---------

Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Samyak Piya
2025-01-15 12:43:40 -05:00
committed by GitHub
parent 266b771a5b
commit f722a2d619
61 changed files with 1460 additions and 171 deletions

View File

@ -1,6 +1,8 @@
import { AppRouterProviders } from '@/app/components/AppRouterProviders';
import { SettingsRoutes } from '@/app/components/SettingsRoutes';
import { VerifyEffect } from '@/auth/components/VerifyEffect';
import { VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect';
import indexAppPath from '@/navigation/utils/indexAppPath';
import { AppPath } from '@/types/AppPath';
import { BlankLayout } from '@/ui/layout/page/components/BlankLayout';
@ -40,6 +42,7 @@ export const useCreateAppRouter = (
>
<Route element={<DefaultLayout />}>
<Route path={AppPath.Verify} element={<VerifyEffect />} />
<Route path={AppPath.VerifyEmail} element={<VerifyEmailEffect />} />
<Route path={AppPath.SignInUp} element={<SignInUp />} />
<Route path={AppPath.Invite} element={<Invite />} />
<Route path={AppPath.ResetPassword} element={<PasswordReset />} />

View File

@ -5,9 +5,9 @@ import { useAuth } from '@/auth/hooks/useAuth';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { AppPath } from '@/types/AppPath';
import { useSetRecoilState } from 'recoil';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui';
export const VerifyEffect = () => {
@ -29,6 +29,7 @@ export const VerifyEffect = () => {
useEffect(() => {
if (isDefined(errorMessage)) {
enqueueSnackBar(errorMessage, {
dedupeKey: 'verify-failed-dedupe-key',
variant: SnackBarVariant.Error,
});
}

View File

@ -0,0 +1,68 @@
import { useAuth } from '@/auth/hooks/useAuth';
import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { EmailVerificationSent } from '../sign-in-up/components/EmailVerificationSent';
export const VerifyEmailEffect = () => {
const { getLoginTokenFromEmailVerificationToken } = useAuth();
const { enqueueSnackBar } = useSnackBar();
const [searchParams] = useSearchParams();
const [isError, setIsError] = useState(false);
const email = searchParams.get('email');
const emailVerificationToken = searchParams.get('emailVerificationToken');
const navigate = useNavigate();
const { readCaptchaToken } = useReadCaptchaToken();
useEffect(() => {
const verifyEmailToken = async () => {
if (!email || !emailVerificationToken) {
enqueueSnackBar(`Invalid email verification link.`, {
dedupeKey: 'email-verification-link-dedupe-key',
variant: SnackBarVariant.Error,
});
return navigate(AppPath.SignInUp);
}
const captchaToken = await readCaptchaToken();
try {
const { loginToken } = await getLoginTokenFromEmailVerificationToken(
emailVerificationToken,
captchaToken,
);
enqueueSnackBar('Email verified.', {
dedupeKey: 'email-verification-dedupe-key',
variant: SnackBarVariant.Success,
});
navigate(`${AppPath.Verify}?loginToken=${loginToken.token}`);
} catch (error) {
enqueueSnackBar('Email verification failed.', {
dedupeKey: 'email-verification-dedupe-key',
variant: SnackBarVariant.Error,
});
setIsError(true);
}
};
verifyEmailToken();
// Verify email only needs to run once at mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (isError) {
return <EmailVerificationSent email={email} isError={true} />;
}
return <></>;
};

View File

@ -0,0 +1,17 @@
import { gql } from '@apollo/client';
export const GET_LOGIN_TOKEN_FROM_EMAIL_VERIFICATION_TOKEN = gql`
mutation GetLoginTokenFromEmailVerificationToken(
$emailVerificationToken: String!
$captchaToken: String
) {
getLoginTokenFromEmailVerificationToken(
emailVerificationToken: $emailVerificationToken
captchaToken: $captchaToken
) {
loginToken {
...AuthTokenFragment
}
}
}
`;

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const RESEND_EMAIL_VERIFICATION_TOKEN = gql`
mutation ResendEmailVerificationToken($email: String!) {
resendEmailVerificationToken(email: $email) {
success
}
}
`;

View File

@ -19,6 +19,7 @@ export const CHECK_USER_EXISTS = gql`
status
}
}
isEmailVerified
}
... on UserNotExists {
exists

View File

@ -2,6 +2,7 @@ import { useApolloClient } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { expect } from '@storybook/test';
import { ReactNode, act } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { iconsState } from 'twenty-ui';
@ -13,12 +14,14 @@ import { supportChatState } from '@/client-config/states/supportChatState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
import { renderHook } from '@testing-library/react';
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks} addTypename={false}>
<RecoilRoot>{children}</RecoilRoot>
<RecoilRoot>
<MemoryRouter>{children}</MemoryRouter>
</RecoilRoot>
</MockedProvider>
);

View File

@ -1,4 +1,4 @@
import { useApolloClient } from '@apollo/client';
import { ApolloError, useApolloClient } from '@apollo/client';
import { useCallback } from 'react';
import {
snapshot_UNSTABLE,
@ -26,6 +26,7 @@ import {
useChallengeMutation,
useCheckUserExistsLazyQuery,
useGetCurrentUserLazyQuery,
useGetLoginTokenFromEmailVerificationTokenMutation,
useSignUpMutation,
useVerifyMutation,
} from '~/generated/graphql';
@ -44,7 +45,12 @@ import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTi
import { currentUserState } from '../states/currentUserState';
import { tokenPairState } from '../states/tokenPairState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
@ -54,6 +60,7 @@ import { domainConfigurationState } from '@/domain-manager/states/domainConfigur
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useSearchParams } from 'react-router-dom';
export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
@ -68,7 +75,11 @@ export const useAuth = () => {
currentWorkspaceMembersState,
);
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const isEmailVerificationRequired = useRecoilValue(
isEmailVerificationRequiredState,
);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState);
const setWorkspaces = useSetRecoilState(workspacesState);
@ -78,6 +89,8 @@ export const useAuth = () => {
const [challenge] = useChallengeMutation();
const [signUp] = useSignUpMutation();
const [verify] = useVerifyMutation();
const [getLoginTokenFromEmailVerificationToken] =
useGetLoginTokenFromEmailVerificationTokenMutation();
const [getCurrentUser] = useGetCurrentUserLazyQuery();
const { isOnAWorkspaceSubdomain } =
@ -96,6 +109,8 @@ export const useAuth = () => {
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
const [, setSearchParams] = useSearchParams();
const clearSession = useRecoilCallback(
({ snapshot }) =>
async () => {
@ -154,25 +169,59 @@ export const useAuth = () => {
const handleChallenge = useCallback(
async (email: string, password: string, captchaToken?: string) => {
const challengeResult = await challenge({
try {
const challengeResult = await challenge({
variables: {
email,
password,
captchaToken,
},
});
if (isDefined(challengeResult.errors)) {
throw challengeResult.errors;
}
if (!challengeResult.data?.challenge) {
throw new Error('No login token');
}
return challengeResult.data.challenge;
} catch (error) {
// TODO: Get intellisense for graphql error extensions code (codegen?)
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.code === 'EMAIL_NOT_VERIFIED'
) {
setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification);
throw error;
}
throw error;
}
},
[challenge, setSearchParams, setSignInUpStep],
);
const handleGetLoginTokenFromEmailVerificationToken = useCallback(
async (emailVerificationToken: string, captchaToken?: string) => {
const loginTokenResult = await getLoginTokenFromEmailVerificationToken({
variables: {
email,
password,
emailVerificationToken,
captchaToken,
},
});
if (isDefined(challengeResult.errors)) {
throw challengeResult.errors;
if (isDefined(loginTokenResult.errors)) {
throw loginTokenResult.errors;
}
if (!challengeResult.data?.challenge) {
if (!loginTokenResult.data?.getLoginTokenFromEmailVerificationToken) {
throw new Error('No login token');
}
return challengeResult.data.challenge;
return loginTokenResult.data.getLoginTokenFromEmailVerificationToken;
},
[challenge],
[getLoginTokenFromEmailVerificationToken],
);
const loadCurrentUser = useCallback(async () => {
@ -343,12 +392,21 @@ export const useAuth = () => {
throw new Error('No login token');
}
if (isEmailVerificationRequired) {
setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification);
return null;
}
if (isMultiWorkspaceEnabled) {
return redirectToWorkspaceDomain(
signUpResult.data.signUp.workspace.subdomain,
AppPath.Verify,
isEmailVerificationRequired ? AppPath.SignInUp : AppPath.Verify,
{
loginToken: signUpResult.data.signUp.loginToken.token,
...(!isEmailVerificationRequired && {
loginToken: signUpResult.data.signUp.loginToken.token,
}),
email,
},
);
}
@ -361,6 +419,9 @@ export const useAuth = () => {
workspacePublicData,
isMultiWorkspaceEnabled,
handleVerify,
setSignInUpStep,
setSearchParams,
isEmailVerificationRequired,
redirectToWorkspaceDomain,
],
);
@ -424,6 +485,8 @@ export const useAuth = () => {
return {
challenge: handleChallenge,
getLoginTokenFromEmailVerificationToken:
handleGetLoginTokenFromEmailVerificationToken,
verify: handleVerify,
loadCurrentUser,

View File

@ -0,0 +1,79 @@
import styled from '@emotion/styled';
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { useHandleResendEmailVerificationToken } from '@/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken';
import { useTheme } from '@emotion/react';
import { AnimatedEaseIn, IconMail, Loader, MainButton, RGBA } from 'twenty-ui';
const StyledMailContainer = styled.div`
align-items: center;
display: flex;
justify-content: center;
border: 2px solid ${(props) => props.color};
border-radius: ${({ theme }) => theme.border.radius.rounded};
box-shadow: ${(props) =>
props.color && `-4px 4px 0 -2px ${RGBA(props.color, 1)}`};
height: 36px;
width: 36px;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledEmail = styled.span`
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledButtonContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(8)};
`;
export const EmailVerificationSent = ({
email,
isError = false,
}: {
email: string | null;
isError?: boolean;
}) => {
const theme = useTheme();
const color =
theme.name === 'light' ? theme.grayScale.gray90 : theme.grayScale.gray10;
const { handleResendEmailVerificationToken, loading: isLoading } =
useHandleResendEmailVerificationToken();
return (
<>
<AnimatedEaseIn>
<StyledMailContainer color={color}>
<IconMail color={color} size={24} stroke={3} />
</StyledMailContainer>
</AnimatedEaseIn>
<Title animate>
{isError ? 'Email Verification Failed' : 'Confirm Your Email Address'}
</Title>
<SubTitle>
{isError ? (
<>
Oops! We encountered an issue verifying{' '}
<StyledEmail>{email}</StyledEmail>. Please request a new
verification email and try again.
</>
) : (
<>
A verification email has been sent to{' '}
<StyledEmail>{email}</StyledEmail>. Please check your inbox and
click the link in the email to activate your account.
</>
)}
</SubTitle>
<StyledButtonContainer>
<MainButton
title="Click to resend"
onClick={handleResendEmailVerificationToken(email)}
Icon={() => (isLoading ? <Loader /> : undefined)}
width={200}
/>
</StyledButtonContainer>
</>
);
};

View File

@ -0,0 +1,47 @@
import { useCallback } from 'react';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useResendEmailVerificationTokenMutation } from '~/generated/graphql';
export const useHandleResendEmailVerificationToken = () => {
const { enqueueSnackBar } = useSnackBar();
const [resendEmailVerificationToken, { loading }] =
useResendEmailVerificationTokenMutation();
const handleResendEmailVerificationToken = useCallback(
(email: string | null) => {
return async () => {
if (!email) {
enqueueSnackBar('Invalid email', {
variant: SnackBarVariant.Error,
});
return;
}
try {
const { data } = await resendEmailVerificationToken({
variables: { email },
});
if (data?.resendEmailVerificationToken?.success === true) {
enqueueSnackBar('Email verification link resent!', {
variant: SnackBarVariant.Success,
});
} else {
enqueueSnackBar('There was some issue', {
variant: SnackBarVariant.Error,
});
}
} catch (error) {
enqueueSnackBar((error as Error).message, {
variant: SnackBarVariant.Error,
});
}
};
},
[enqueueSnackBar, resendEmailVerificationToken],
);
return { handleResendEmailVerificationToken, loading };
};

View File

@ -4,6 +4,7 @@ export enum SignInUpStep {
Init = 'init',
Email = 'email',
Password = 'password',
EmailVerification = 'emailVerification',
WorkspaceSelection = 'workspaceSelection',
SSOIdentityProviderSelection = 'SSOIdentityProviderSelection',
}

View File

@ -8,6 +8,7 @@ import { clientConfigApiStatusState } from '@/client-config/states/clientConfigA
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState';
@ -29,6 +30,9 @@ export const ClientConfigProviderEffect = () => {
const setIsMultiWorkspaceEnabled = useSetRecoilState(
isMultiWorkspaceEnabledState,
);
const setIsEmailVerificationRequired = useSetRecoilState(
isEmailVerificationRequiredState,
);
const setBilling = useSetRecoilState(billingState);
const setSupportChat = useSetRecoilState(supportChatState);
@ -89,6 +93,9 @@ export const ClientConfigProviderEffect = () => {
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
setIsDeveloperDefaultSignInPrefilled(data?.clientConfig.signInPrefilled);
setIsMultiWorkspaceEnabled(data?.clientConfig.isMultiWorkspaceEnabled);
setIsEmailVerificationRequired(
data?.clientConfig.isEmailVerificationRequired,
);
setBilling(data?.clientConfig.billing);
setSupportChat(data?.clientConfig.support);
@ -115,6 +122,7 @@ export const ClientConfigProviderEffect = () => {
setIsDebugMode,
setIsDeveloperDefaultSignInPrefilled,
setIsMultiWorkspaceEnabled,
setIsEmailVerificationRequired,
setSupportChat,
setBilling,
setSentryConfig,

View File

@ -22,6 +22,7 @@ export const GET_CLIENT_CONFIG = gql`
}
signInPrefilled
isMultiWorkspaceEnabled
isEmailVerificationRequired
defaultSubdomain
frontDomain
debugMode

View File

@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';
export const isEmailVerificationRequiredState = createState<boolean>({
key: 'isEmailVerificationRequired',
defaultValue: false,
});

View File

@ -1,6 +1,7 @@
export enum AppPath {
// Not logged-in
Verify = '/verify',
VerifyEmail = '/verify-email',
SignInUp = '/welcome',
Invite = '/invite/:workspaceInviteHash',
ResetPassword = '/reset-password/:passwordResetToken',

View File

@ -35,6 +35,7 @@ export type SnackBarProps = Pick<ComponentPropsWithoutRef<'div'>, 'id'> & {
onClose?: () => void;
role?: 'alert' | 'status';
variant?: SnackBarVariant;
dedupeKey?: string;
};
const StyledContainer = styled.div`

View File

@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { useRecoilCallback } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { isDefined } from '~/utils/isDefined';
import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext';
import {
@ -27,8 +28,17 @@ export const useSnackBar = () => {
const setSnackBarQueue = useRecoilCallback(
({ set }) =>
(newValue) =>
(newValue: SnackBarOptions) =>
set(snackBarInternalScopedState({ scopeId }), (prev) => {
if (
isDefined(newValue.dedupeKey) &&
prev.queue.some(
(snackBar) => snackBar.dedupeKey === newValue.dedupeKey,
)
) {
return prev;
}
if (prev.queue.length >= prev.maxQueue) {
return {
...prev,

View File

@ -67,6 +67,17 @@ const testCases = [
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: false },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.VerifyEmail, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: true },

View File

@ -28,6 +28,7 @@ export const useShowAuthModal = () => {
if (
isMatchingLocation(AppPath.Invite) ||
isMatchingLocation(AppPath.ResetPassword) ||
isMatchingLocation(AppPath.VerifyEmail) ||
isMatchingLocation(AppPath.SignInUp)
) {
return isDefaultLayoutAuthModalVisible;