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:
Raphaël Bosi
2026-06-29 18:32:30 +02:00
committed by GitHub
parent 2bb7fb2e9b
commit facdbb5ba8
28 changed files with 701 additions and 51 deletions
@@ -89,6 +89,7 @@ describe('ClientConfigController', () => {
importContactsCreditsReward: 2,
inviteTeamMaxCreditsReward: 9,
inviteTeamCreditsRewardPerUser: 3,
upgradeCreditsReward: 5,
},
isAttachmentPreviewEnabled: true,
analyticsEnabled: false,
@@ -214,6 +214,8 @@ export class OnboardingConfig {
inviteTeamMaxCreditsReward: number;
inviteTeamCreditsRewardPerUser: number;
upgradeCreditsReward: number;
}
@ObjectType()
@@ -95,6 +95,7 @@ describe('ClientConfigService', () => {
ONBOARDING_IMPORT_CONTACTS_CREDITS_REWARD: 2_000_000,
ONBOARDING_INVITE_TEAM_MAX_CREDITS_REWARD: 9_000_000,
ONBOARDING_INVITE_TEAM_CREDITS_REWARD_PER_USER: 3_000_000,
BILLING_FREE_WORKFLOW_CREDITS_FOR_TRIAL_PERIOD_WITH_CREDIT_CARD: 5_000_000,
IS_ATTACHMENT_PREVIEW_ENABLED: true,
ANALYTICS_ENABLED: true,
MESSAGING_PROVIDER_MICROSOFT_ENABLED: false,
@@ -172,6 +173,7 @@ describe('ClientConfigService', () => {
importContactsCreditsReward: 2,
inviteTeamMaxCreditsReward: 9,
inviteTeamCreditsRewardPerUser: 3,
upgradeCreditsReward: 5,
},
isAttachmentPreviewEnabled: true,
analyticsEnabled: true,
@@ -235,6 +235,11 @@ export class ClientConfigService {
'ONBOARDING_INVITE_TEAM_CREDITS_REWARD_PER_USER',
),
),
upgradeCreditsReward: toDisplayCredits(
this.twentyConfigService.get(
'BILLING_FREE_WORKFLOW_CREDITS_FOR_TRIAL_PERIOD_WITH_CREDIT_CARD',
),
),
},
isAttachmentPreviewEnabled: this.twentyConfigService.get(
'IS_ATTACHMENT_PREVIEW_ENABLED',
@@ -64,14 +64,6 @@ export class OnboardingService {
return null;
}
if (
await this.billingService.isSubscriptionIncompleteOnboardingStatus(
workspace.id,
)
) {
return OnboardingStatus.PLAN_REQUIRED;
}
if (this.isWorkspaceActivationPending(workspace)) {
return OnboardingStatus.WORKSPACE_ACTIVATION;
}
@@ -108,6 +100,14 @@ export class OnboardingService {
return OnboardingStatus.INVITE_TEAM;
}
if (
await this.billingService.isSubscriptionIncompleteOnboardingStatus(
workspace.id,
)
) {
return OnboardingStatus.PLAN_REQUIRED;
}
if (isBookOnboardingPending) {
const calendarBookingPageId = this.twentyConfigService.get(
'CALENDAR_BOOKING_PAGE_ID',