Files
twenty_crm/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts
Etienne 081376f594 Onboarding - add nextPath logic after email verification (#12342)
Context :
Plan choice [on pricing page on website](https://twenty.com/pricing)
should redirect you the right plan on app /plan-required page (after
sign in), thanks to query parameters and BillingCheckoutSessionState
sync.
With email verification, an other session starts at CTA click in
verification email. Initial BillingCheckoutSessionState is lost and user
can't submit to the plan he choose.

Solution : 
Pass a nextPath query parameter in email verification link

To test : 
- Modify .env to add IS_BILLING_ENABLED (+ reset db + sync billing) +
IS_EMAIL_VERIFICATION_REQUIRED
- Start test from this page
http://app.localhost:3001/welcome?billingCheckoutSession={%22plan%22:%22ENTERPRISE%22,%22interval%22:%22Year%22,%22requirePaymentMethod%22:true}
- After verification, check you arrive on /plan-required page with
Enterprise plan on a yearly interval (default is Pro/monthly).

closes https://github.com/twentyhq/twenty/issues/12288
2025-05-28 17:20:31 +00:00

151 lines
4.7 KiB
TypeScript

import { useCallback, useState } from 'react';
import { SubmitHandler, UseFormReturn } from 'react-hook-form';
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { signInUpModeState } from '@/auth/states/signInUpModeState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
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';
export const useSignInUp = (form: UseFormReturn<Form>) => {
const { enqueueSnackBar } = useSnackBar();
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
const location = useLocation();
const workspaceInviteHash = useParams().workspaceInviteHash;
const [searchParams] = useSearchParams();
const workspacePersonalInviteToken =
searchParams.get('inviteToken') ?? undefined;
const [isInviteMode] = useState(() =>
isMatchingLocation(location, AppPath.Invite),
);
const {
signInWithCredentials,
signUpWithCredentials,
checkUserExists: { checkUserExistsQuery },
} = useAuth();
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
const { readCaptchaToken } = useReadCaptchaToken();
const { buildSearchParamsFromUrlSyncedStates } =
useBuildSearchParamsFromUrlSyncedStates();
const continueWithEmail = useCallback(() => {
requestFreshCaptchaToken();
setSignInUpStep(SignInUpStep.Email);
}, [requestFreshCaptchaToken, setSignInUpStep]);
const continueWithCredentials = useCallback(async () => {
const token = await readCaptchaToken();
if (!form.getValues('email')) {
return;
}
checkUserExistsQuery({
variables: {
email: form.getValues('email').toLowerCase().trim(),
captchaToken: token,
},
onError: (error) => {
enqueueSnackBar(`${error.message}`, {
variant: SnackBarVariant.Error,
});
},
onCompleted: (data) => {
requestFreshCaptchaToken();
if (data?.checkUserExists.exists) {
setSignInUpMode(SignInUpMode.SignIn);
} else {
setSignInUpMode(SignInUpMode.SignUp);
}
setSignInUpStep(SignInUpStep.Password);
},
});
}, [
readCaptchaToken,
form,
checkUserExistsQuery,
enqueueSnackBar,
requestFreshCaptchaToken,
setSignInUpStep,
setSignInUpMode,
]);
const submitCredentials: SubmitHandler<Form> = useCallback(
async (data) => {
const token = await readCaptchaToken();
try {
if (!data.email || !data.password) {
throw new Error('Email and password are required');
}
if (signInUpMode === SignInUpMode.SignIn && !isInviteMode) {
await signInWithCredentials(
data.email.toLowerCase().trim(),
data.password,
token,
);
} else {
const verifyEmailNextPath = buildAppPathWithQueryParams(
AppPath.PlanRequired,
await buildSearchParamsFromUrlSyncedStates(),
);
await signUpWithCredentials({
email: data.email.toLowerCase().trim(),
password: data.password,
workspaceInviteHash,
workspacePersonalInviteToken,
captchaToken: token,
verifyEmailNextPath,
});
}
} catch (err: any) {
enqueueSnackBar(err?.message, {
variant: SnackBarVariant.Error,
});
requestFreshCaptchaToken();
}
},
[
readCaptchaToken,
signInUpMode,
isInviteMode,
signInWithCredentials,
signUpWithCredentials,
workspaceInviteHash,
workspacePersonalInviteToken,
enqueueSnackBar,
requestFreshCaptchaToken,
buildSearchParamsFromUrlSyncedStates,
],
);
return {
isInviteMode,
signInUpStep,
signInUpMode,
continueWithCredentials,
continueWithEmail,
submitCredentials,
};
};