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
151 lines
4.7 KiB
TypeScript
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,
|
|
};
|
|
};
|