diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 2fb30518d..dd2b6a8d4 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1177,6 +1177,7 @@ export type MutationSignUpArgs = { email: Scalars['String']; locale?: InputMaybe; password: Scalars['String']; + verifyEmailNextPath?: InputMaybe; workspaceId?: InputMaybe; workspaceInviteHash?: InputMaybe; workspacePersonalInviteToken?: InputMaybe; @@ -2672,6 +2673,7 @@ export type SignUpMutationVariables = Exact<{ captchaToken?: InputMaybe; workspaceId?: InputMaybe; locale?: InputMaybe; + verifyEmailNextPath?: InputMaybe; }>; @@ -4057,7 +4059,7 @@ export type ResendEmailVerificationTokenMutationHookResult = ReturnType; export type ResendEmailVerificationTokenMutationOptions = Apollo.BaseMutationOptions; export const SignUpDocument = gql` - mutation SignUp($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String, $workspaceId: String, $locale: String) { + mutation SignUp($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String, $workspaceId: String, $locale: String, $verifyEmailNextPath: String) { signUp( email: $email password: $password @@ -4066,6 +4068,7 @@ export const SignUpDocument = gql` captchaToken: $captchaToken workspaceId: $workspaceId locale: $locale + verifyEmailNextPath: $verifyEmailNextPath ) { loginToken { ...AuthTokenFragment @@ -4102,6 +4105,7 @@ export type SignUpMutationFn = Apollo.MutationFunction { }; jest.mock('recoil'); -const setupMockRecoil = (objectNamePlural?: string) => { +const setupMockRecoil = ( + objectNamePlural?: string, + verifyEmailNextPath?: string, +) => { jest .mocked(useRecoilValue) - .mockReturnValueOnce([{ namePlural: objectNamePlural ?? '' }]); + .mockReturnValueOnce([{ namePlural: objectNamePlural ?? '' }]) + .mockReturnValueOnce(verifyEmailNextPath); }; // prettier-ignore @@ -72,6 +77,7 @@ const testCases: { res: string | undefined; objectNamePluralFromParams?: string; objectNamePluralFromMetadata?: string; + verifyEmailNextPath?: string; }[] = [ { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' }, @@ -110,7 +116,9 @@ const testCases: { { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, + { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, verifyEmailNextPath: '/nextPath?key=value', res: '/nextPath?key=value' }, { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' }, + { loc: AppPath.VerifyEmail, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, verifyEmailNextPath: '/nextPath?key=value', res: undefined }, { loc: AppPath.VerifyEmail, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, @@ -275,6 +283,7 @@ describe('usePageChangeEffectNavigateLocation', () => { isLoggedIn, objectNamePluralFromParams, objectNamePluralFromMetadata, + verifyEmailNextPath, res, }) => { setupMockIsMatchingLocation(loc); @@ -282,7 +291,7 @@ describe('usePageChangeEffectNavigateLocation', () => { setupMockIsWorkspaceActivationStatusEqualsTo(isWorkspaceSuspended); setupMockIsLogged(isLoggedIn); setupMockUseParams(objectNamePluralFromParams); - setupMockRecoil(objectNamePluralFromMetadata); + setupMockRecoil(objectNamePluralFromMetadata, verifyEmailNextPath); expect(usePageChangeEffectNavigateLocation()).toEqual(res); }, @@ -294,7 +303,8 @@ describe('usePageChangeEffectNavigateLocation', () => { (Object.keys(OnboardingStatus).length + ['isWorkspaceSuspended:true', 'isWorkspaceSuspended:false'] .length) + - ['nonExistingObjectInParam', 'existingObjectInParam:false'].length, + ['nonExistingObjectInParam', 'existingObjectInParam:false'].length + + ['caseWithRedirectionToVerifyEmailNextPath', 'caseWithout'].length, ); }); }); diff --git a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts index 99c59c409..268fd895a 100644 --- a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts +++ b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts @@ -1,3 +1,4 @@ +import { verifyEmailNextPathState } from '@/app/states/verifyEmailNextPathState'; import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; @@ -43,6 +44,7 @@ export const usePageChangeEffectNavigateLocation = () => { const objectMetadataItem = objectMetadataItems.find( (objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural, ); + const verifyEmailNextPath = useRecoilValue(verifyEmailNextPathState); if ( !isLoggedIn && @@ -58,6 +60,12 @@ export const usePageChangeEffectNavigateLocation = () => { onboardingStatus === OnboardingStatus.PLAN_REQUIRED && !someMatchingLocationOf([AppPath.PlanRequired, AppPath.PlanRequiredSuccess]) ) { + if ( + isMatchingLocation(location, AppPath.VerifyEmail) && + isDefined(verifyEmailNextPath) + ) { + return verifyEmailNextPath; + } return AppPath.PlanRequired; } diff --git a/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts b/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts index d443ae906..d01a7c914 100644 --- a/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts +++ b/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts @@ -1,9 +1,9 @@ import { useRecoilCallback } from 'recoil'; -import { isQueryParamInitializedState } from '@/app/states/isQueryParamInitializedState'; import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState'; import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type'; import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue'; +import deepEqual from 'deep-equal'; // Initialize state that are hydrated from query parameters // We used to use recoil-sync to do this, but it was causing issues with Firefox @@ -11,52 +11,49 @@ export const useInitializeQueryParamState = () => { const initializeQueryParamState = useRecoilCallback( ({ set, snapshot }) => () => { - const isInitialized = snapshot - .getLoadable(isQueryParamInitializedState) - .getValue(); + const handlers = { + billingCheckoutSession: (value: string) => { + const billingCheckoutSession = snapshot + .getLoadable(billingCheckoutSessionState) + .getValue(); - if (!isInitialized) { - const handlers = { - billingCheckoutSession: (value: string) => { - try { - const parsedValue = JSON.parse(decodeURIComponent(value)); + try { + const parsedValue = JSON.parse(decodeURIComponent(value)); - if ( - typeof parsedValue === 'object' && - parsedValue !== null && - 'plan' in parsedValue && - 'interval' in parsedValue && - 'requirePaymentMethod' in parsedValue - ) { - set( - billingCheckoutSessionState, - parsedValue as BillingCheckoutSession, - ); - } - } catch (error) { - // eslint-disable-next-line no-console - console.error( - 'Failed to parse billingCheckoutSession from URL', - error, - ); + if ( + typeof parsedValue === 'object' && + parsedValue !== null && + 'plan' in parsedValue && + 'interval' in parsedValue && + 'requirePaymentMethod' in parsedValue && + !deepEqual(billingCheckoutSession, parsedValue) + ) { set( billingCheckoutSessionState, - BILLING_CHECKOUT_SESSION_DEFAULT_VALUE, + parsedValue as BillingCheckoutSession, ); } - }, - }; - - const queryParams = new URLSearchParams(window.location.search); - - for (const [paramName, handler] of Object.entries(handlers)) { - const value = queryParams.get(paramName); - if (value !== null) { - handler(value); + } catch (error) { + // eslint-disable-next-line no-console + console.error( + 'Failed to parse billingCheckoutSession from URL', + error, + ); + set( + billingCheckoutSessionState, + BILLING_CHECKOUT_SESSION_DEFAULT_VALUE, + ); } - } + }, + }; - set(isQueryParamInitializedState, true); + const queryParams = new URLSearchParams(window.location.search); + + for (const [paramName, handler] of Object.entries(handlers)) { + const value = queryParams.get(paramName); + if (value !== null) { + handler(value); + } } }, [], diff --git a/packages/twenty-front/src/modules/app/states/isQueryParamInitializedState.ts b/packages/twenty-front/src/modules/app/states/isQueryParamInitializedState.ts deleted file mode 100644 index cd4d50739..000000000 --- a/packages/twenty-front/src/modules/app/states/isQueryParamInitializedState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; - -export const isQueryParamInitializedState = createState({ - key: 'isQueryParamInitializedState', - defaultValue: false, -}); diff --git a/packages/twenty-front/src/modules/app/states/verifyEmailNextPathState.ts b/packages/twenty-front/src/modules/app/states/verifyEmailNextPathState.ts new file mode 100644 index 000000000..71882210b --- /dev/null +++ b/packages/twenty-front/src/modules/app/states/verifyEmailNextPathState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui/utilities'; + +export const verifyEmailNextPathState = createState({ + key: 'verifyEmailNextPathState', + defaultValue: undefined, +}); diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx index 23896d9b5..640424baf 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx @@ -4,12 +4,15 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { ApolloError } from '@apollo/client'; +import { verifyEmailNextPathState } from '@/app/states/verifyEmailNextPathState'; import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { Modal } from '@/ui/layout/modal/components/Modal'; import { useLingui } from '@lingui/react/macro'; import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; import { useNavigateApp } from '~/hooks/useNavigateApp'; import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; import { EmailVerificationSent } from '../sign-in-up/components/EmailVerificationSent'; @@ -22,8 +25,11 @@ export const VerifyEmailEffect = () => { const [searchParams] = useSearchParams(); const [isError, setIsError] = useState(false); + const setVerifyEmailNextPath = useSetRecoilState(verifyEmailNextPathState); + const email = searchParams.get('email'); const emailVerificationToken = searchParams.get('emailVerificationToken'); + const verifyEmailNextPath = searchParams.get('nextPath'); const navigate = useNavigateApp(); const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); @@ -58,6 +64,11 @@ export const VerifyEmailEffect = () => { loginToken: loginToken.token, }); } + + if (isDefined(verifyEmailNextPath)) { + setVerifyEmailNextPath(verifyEmailNextPath); + } + verifyLoginToken(loginToken.token); } catch (error) { const message: string = diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts index 0e99fe68d..ce60908d1 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts @@ -9,6 +9,7 @@ export const SIGN_UP = gql` $captchaToken: String $workspaceId: String $locale: String + $verifyEmailNextPath: String ) { signUp( email: $email @@ -18,6 +19,7 @@ export const SIGN_UP = gql` captchaToken: $captchaToken workspaceId: $workspaceId locale: $locale + verifyEmailNextPath: $verifyEmailNextPath ) { loginToken { ...AuthTokenFragment diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx index a2baee372..b04f39bc2 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx @@ -167,7 +167,7 @@ describe('useAuth', () => { const { result } = renderHooks(); await act(async () => { - await result.current.signUpWithCredentials(email, password); + await result.current.signUpWithCredentials({ email, password }); }); expect(mocks[2].result).toHaveBeenCalled(); diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 5149ab2b5..ac3ed8ed7 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -397,13 +397,21 @@ export const useAuth = () => { }, [clearSession]); const handleCredentialsSignUp = useCallback( - async ( - email: string, - password: string, - workspaceInviteHash?: string, - workspacePersonalInviteToken?: string, - captchaToken?: string, - ) => { + async ({ + email, + password, + workspaceInviteHash, + workspacePersonalInviteToken, + captchaToken, + verifyEmailNextPath, + }: { + email: string; + password: string; + workspaceInviteHash?: string; + workspacePersonalInviteToken?: string; + captchaToken?: string; + verifyEmailNextPath?: string; + }) => { const signUpResult = await signUp({ variables: { email, @@ -415,6 +423,7 @@ export const useAuth = () => { ...(workspacePublicData?.id ? { workspaceId: workspacePublicData.id } : {}), + verifyEmailNextPath, }, }); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts index f5b644309..5344fe297 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts @@ -11,10 +11,12 @@ import { import { SignInUpMode } from '@/auth/types/signInUpMode'; import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken'; import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken'; +import { useBuildSearchParamsFromUrlSyncedStates } from '@/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates'; 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 { useRecoilState } from 'recoil'; +import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams'; import { isMatchingLocation } from '~/utils/isMatchingLocation'; import { useAuth } from '../../hooks/useAuth'; @@ -44,6 +46,9 @@ export const useSignInUp = (form: UseFormReturn
) => { const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken(); const { readCaptchaToken } = useReadCaptchaToken(); + const { buildSearchParamsFromUrlSyncedStates } = + useBuildSearchParamsFromUrlSyncedStates(); + const continueWithEmail = useCallback(() => { requestFreshCaptchaToken(); setSignInUpStep(SignInUpStep.Email); @@ -99,13 +104,19 @@ export const useSignInUp = (form: UseFormReturn) => { token, ); } else { - await signUpWithCredentials( - data.email.toLowerCase().trim(), - data.password, + const verifyEmailNextPath = buildAppPathWithQueryParams( + AppPath.PlanRequired, + await buildSearchParamsFromUrlSyncedStates(), + ); + + await signUpWithCredentials({ + email: data.email.toLowerCase().trim(), + password: data.password, workspaceInviteHash, workspacePersonalInviteToken, - token, - ); + captchaToken: token, + verifyEmailNextPath, + }); } } catch (err: any) { enqueueSnackBar(err?.message, { @@ -124,6 +135,7 @@ export const useSignInUp = (form: UseFormReturn) => { workspacePersonalInviteToken, enqueueSnackBar, requestFreshCaptchaToken, + buildSearchParamsFromUrlSyncedStates, ], ); diff --git a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx index 2bdeb3ce8..3d825b54b 100644 --- a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx +++ b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx @@ -1,3 +1,4 @@ +import { verifyEmailNextPathState } from '@/app/states/verifyEmailNextPathState'; import { SubTitle } from '@/auth/components/SubTitle'; import { Title } from '@/auth/components/Title'; import { useAuth } from '@/auth/hooks/useAuth'; @@ -96,6 +97,12 @@ export const ChooseYourPlan = () => { billingCheckoutSessionState, ); + const [verifyEmailNextPath, setVerifyEmailNextPath] = useRecoilState( + verifyEmailNextPathState, + ); + if (isDefined(verifyEmailNextPath)) { + setVerifyEmailNextPath(undefined); + } const { data: plans } = useBillingBaseProductPricesQuery(); const currentPlan = billingCheckoutSession.plan; diff --git a/packages/twenty-front/src/utils/__tests__/buildAppPathWithQueryParams.test.ts b/packages/twenty-front/src/utils/__tests__/buildAppPathWithQueryParams.test.ts new file mode 100644 index 000000000..13da7e081 --- /dev/null +++ b/packages/twenty-front/src/utils/__tests__/buildAppPathWithQueryParams.test.ts @@ -0,0 +1,13 @@ +import { AppPath } from '@/types/AppPath'; +import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams'; + +describe('buildAppPathWithQueryParams', () => { + it('should build the correct path with query params', () => { + expect( + buildAppPathWithQueryParams(AppPath.PlanRequired, { + key1: 'value1', + key2: 'value2', + }), + ).toBe('/plan-required?key1=value1&key2=value2'); + }); +}); diff --git a/packages/twenty-front/src/utils/buildAppPathWithQueryParams.ts b/packages/twenty-front/src/utils/buildAppPathWithQueryParams.ts new file mode 100644 index 000000000..fb382b1c3 --- /dev/null +++ b/packages/twenty-front/src/utils/buildAppPathWithQueryParams.ts @@ -0,0 +1,12 @@ +import { AppPath } from '@/types/AppPath'; + +export const buildAppPathWithQueryParams = ( + path: AppPath, + queryParams: Record, +) => { + const searchParams = []; + for (const [key, value] of Object.entries(queryParams)) { + searchParams.push(`${key}=${value}`); + } + return `${path}?${searchParams.join('&')}`; +}; diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 27a439fa9..621d4f105 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -251,6 +251,7 @@ export class AuthResolver { user.email, workspace, signUpInput.locale ?? SOURCE_LOCALE, + signUpInput.verifyEmailNextPath, ); const loginToken = await this.loginTokenService.generateLoginToken( diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.input.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.input.ts index a6a7f92cc..21f1cb51f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.input.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.input.ts @@ -39,4 +39,9 @@ export class SignUpInput { @IsString() @IsOptional() locale?: keyof typeof APP_LOCALES; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + verifyEmailNextPath?: string; } diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts index b84967543..274aeb856 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts @@ -8,6 +8,7 @@ import { addMilliseconds, differenceInMilliseconds } from 'date-fns'; import ms from 'ms'; import { SendEmailVerificationLinkEmail } from 'twenty-emails'; import { APP_LOCALES } from 'twenty-shared/translations'; +import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { @@ -45,6 +46,7 @@ export class EmailVerificationService { | WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType | undefined, locale: keyof typeof APP_LOCALES, + verifyEmailNextPath?: string, ) { if (!this.twentyConfigService.get('IS_EMAIL_VERIFICATION_REQUIRED')) { return { success: false }; @@ -55,7 +57,13 @@ export class EmailVerificationService { const linkPathnameAndSearchParams = { pathname: 'verify-email', - searchParams: { emailVerificationToken, email }, + searchParams: { + emailVerificationToken, + email, + ...(isDefined(verifyEmailNextPath) + ? { nextPath: verifyEmailNextPath } + : {}), + }, }; const verificationLink = workspace ? this.domainManagerService.buildWorkspaceURL({ diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts index 4ad512a6d..c6805b1bb 100644 --- a/packages/twenty-ui/src/utilities/index.ts +++ b/packages/twenty-ui/src/utilities/index.ts @@ -29,3 +29,4 @@ export { createState } from './state/utils/createState'; export type { ClickOutsideAttributes } from './types/ClickOutsideAttributes'; export type { Nullable } from './types/Nullable'; export { getDisplayValueByUrlType } from './utils/getDisplayValueByUrlType'; +