v2 onboarding: dedicated verify step and upgrade-free-trial as the last step (#22303)
https://github.com/user-attachments/assets/b1ee4f77-c6d7-4638-b9f1-dd801d1cc0db Completes the onboarding-v2 flow: a dedicated verify step, the reordering that makes the plan step come last, and the upgrade-free-trial page itself. ## Verify step (`/verify-v2`) After the cross-domain token exchange, v2 sign-ups land on a clean `BlankLayout` "Verifying your email" screen (fading Twenty logo) instead of the v1 `AuthModal` flashing over the background mock. The redirect target is chosen from `isOnboardingV2` (read from the Jotai store at redirect time). The pulsing logo is extracted into a shared `OnboardingPulsingLogo`, reused by the workspace-activation loader. `/verify-v2` joins the same exempt lists as `/verify` (ongoing-creation guard, metadata gater, apollo unauthenticated handler, captcha, page title) — intentionally not `useShowAuthModal`, which is what drops the modal. ## Plan step is now last `getOnboardingStatus` checks `PLAN_REQUIRED` after invite-team instead of first, so onboarding runs workspace activation → email → profile → invite → plan. This is what lets the upgrade step be reached as the final step instead of gating right after sign-up. Applies to both v1 and v2 (same order). ## Upgrade free trial page (`PlanRequiredV2` → `ChooseYourPlanV2` / `UpgradeFreeTrial`) The final step, full-screen under `BlankLayout` via `OnboardingV2Layout`, matching the Figma (billing card with the Stripe form, the "Basic / without credit card" option, trial + credits pills). Reuses the v1 `ChooseYourPlanContent` billing logic (`SubscriptionPaymentForm`, `useHandleCheckoutSession`). The "+N free credits" reward comes from `clientConfig.onboarding.upgradeCreditsReward` (sourced from `BILLING_FREE_WORKFLOW_CREDITS_FOR_TRIAL_PERIOD_WITH_CREDIT_CARD`). ## Also Fixes a latent staleness in the Apollo `onUnauthenticatedError` handler — it captured `location` from the memoized client, now read via a ref — so auth-path exemptions are correct after navigation. Note: the onboarding step order change affects v1 too (plan becomes its last step as well).
This commit is contained in:
@@ -539,4 +539,52 @@ describe('usePageChangeEffectNavigateLocation — onboarding V2', () => {
|
||||
|
||||
expect(usePageChangeEffectNavigateLocation()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('routes to PlanRequiredV2 from InviteTeamV2 when onboardingV2 is active and onboarding is completed', () => {
|
||||
setupOnboardingV2Case(AppPath.InviteTeamV2, OnboardingStatus.COMPLETED);
|
||||
|
||||
expect(usePageChangeEffectNavigateLocation()).toEqual(
|
||||
AppPath.PlanRequiredV2,
|
||||
);
|
||||
});
|
||||
|
||||
it('routes to PlanRequiredV2 from InviteTeamV2 when onboardingV2 is active and status is BOOK_ONBOARDING', () => {
|
||||
setupOnboardingV2Case(
|
||||
AppPath.InviteTeamV2,
|
||||
OnboardingStatus.BOOK_ONBOARDING,
|
||||
);
|
||||
|
||||
expect(usePageChangeEffectNavigateLocation()).toEqual(
|
||||
AppPath.PlanRequiredV2,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not redirect away from the PlanRequiredV2 page when onboarding is completed', () => {
|
||||
setupOnboardingV2Case(AppPath.PlanRequiredV2, OnboardingStatus.COMPLETED);
|
||||
|
||||
expect(usePageChangeEffectNavigateLocation()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('lets completed v2 users route normally away from the invite transition', () => {
|
||||
setupOnboardingV2Case(AppPath.Index, OnboardingStatus.COMPLETED);
|
||||
|
||||
expect(usePageChangeEffectNavigateLocation()).toEqual(defaultHomePagePath);
|
||||
});
|
||||
|
||||
it('routes to PlanRequiredV2 (never the v1 plan page) when onboardingV2 is active and status is PLAN_REQUIRED', () => {
|
||||
setupOnboardingV2Case(AppPath.Index, OnboardingStatus.PLAN_REQUIRED);
|
||||
|
||||
expect(usePageChangeEffectNavigateLocation()).toEqual(
|
||||
AppPath.PlanRequiredV2,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not redirect away from the PlanRequiredV2 page when a plan is required', () => {
|
||||
setupOnboardingV2Case(
|
||||
AppPath.PlanRequiredV2,
|
||||
OnboardingStatus.PLAN_REQUIRED,
|
||||
);
|
||||
|
||||
expect(usePageChangeEffectNavigateLocation()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,6 +93,7 @@ export const usePageChangeEffectNavigateLocation = () => {
|
||||
onboardingStatus === OnboardingStatus.PLAN_REQUIRED &&
|
||||
!someMatchingLocationOf([
|
||||
AppPath.PlanRequired,
|
||||
AppPath.PlanRequiredV2,
|
||||
AppPath.PlanRequiredSuccess,
|
||||
AppPath.BookCall,
|
||||
AppPath.BookCallDecision,
|
||||
@@ -104,7 +105,7 @@ export const usePageChangeEffectNavigateLocation = () => {
|
||||
) {
|
||||
return verifyEmailRedirectPath;
|
||||
}
|
||||
return AppPath.PlanRequired;
|
||||
return isOnboardingV2 ? AppPath.PlanRequiredV2 : AppPath.PlanRequired;
|
||||
}
|
||||
|
||||
if (isWorkspaceSuspended) {
|
||||
@@ -152,6 +153,19 @@ export const usePageChangeEffectNavigateLocation = () => {
|
||||
return isOnboardingV2 ? AppPath.InviteTeamV2 : AppPath.InviteTeam;
|
||||
}
|
||||
|
||||
if (
|
||||
isOnboardingV2 &&
|
||||
(onboardingStatus === OnboardingStatus.BOOK_ONBOARDING ||
|
||||
onboardingStatus === OnboardingStatus.COMPLETED)
|
||||
) {
|
||||
if (isMatchingLocation(location, AppPath.InviteTeamV2)) {
|
||||
return AppPath.PlanRequiredV2;
|
||||
}
|
||||
if (isMatchingLocation(location, AppPath.PlanRequiredV2)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
onboardingStatus === OnboardingStatus.BOOK_ONBOARDING &&
|
||||
!someMatchingLocationOf([AppPath.BookCallDecision, AppPath.BookCall])
|
||||
|
||||
Reference in New Issue
Block a user