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:
@@ -0,0 +1,27 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledLogo = styled.img`
|
||||
animation: onboardingPulsingLogo 0.8s ease-in-out infinite alternate;
|
||||
height: ${themeCssVariables.spacing[12]};
|
||||
margin-bottom: ${themeCssVariables.spacing[8]};
|
||||
width: ${themeCssVariables.spacing[12]};
|
||||
|
||||
@keyframes onboardingPulsingLogo {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const OnboardingPulsingLogo = () => (
|
||||
<StyledLogo src="/images/integrations/twenty-logo.svg" alt="" />
|
||||
);
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { isValidElement, type ReactNode } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Radio } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledCard = styled.div`
|
||||
background-color: ${themeCssVariables.background.primary};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.button<{ hasBody: boolean }>`
|
||||
align-items: flex-start;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: ${({ hasBody }) =>
|
||||
hasBody ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
justify-content: space-between;
|
||||
padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[3]};
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledHeaderLeft = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const StyledTitleRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.span`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledTitleSuffix = styled.span`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.regular};
|
||||
`;
|
||||
|
||||
const StyledNote = styled.span`
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.regular};
|
||||
`;
|
||||
|
||||
const StyledHeaderRight = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledBadge = styled.span`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.tertiary};
|
||||
border-radius: ${themeCssVariables.border.radius.pill};
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
padding: ${themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledBody = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
type OnboardingPlanCardProps = {
|
||||
title: string;
|
||||
titleSuffix?: string;
|
||||
note?: string;
|
||||
badge?: string;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const OnboardingPlanCard = ({
|
||||
title,
|
||||
titleSuffix,
|
||||
note,
|
||||
badge,
|
||||
selected,
|
||||
onSelect,
|
||||
children,
|
||||
}: OnboardingPlanCardProps) => {
|
||||
const hasBody = isValidElement(children);
|
||||
|
||||
return (
|
||||
<StyledCard>
|
||||
<StyledHeader type="button" hasBody={hasBody} onClick={onSelect}>
|
||||
<StyledHeaderLeft>
|
||||
<StyledTitleRow>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
{isDefined(titleSuffix) && (
|
||||
<StyledTitleSuffix>{titleSuffix}</StyledTitleSuffix>
|
||||
)}
|
||||
</StyledTitleRow>
|
||||
{isDefined(note) && <StyledNote>{note}</StyledNote>}
|
||||
</StyledHeaderLeft>
|
||||
<StyledHeaderRight>
|
||||
{isDefined(badge) && <StyledBadge>{badge}</StyledBadge>}
|
||||
<Radio checked={selected} />
|
||||
</StyledHeaderRight>
|
||||
</StyledHeader>
|
||||
{hasBody && <StyledBody>{children}</StyledBody>}
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconCalendarEvent } from 'twenty-ui/icon';
|
||||
import { themeCssVariables, useTheme } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledTag = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.color.green3};
|
||||
border: 1px solid ${themeCssVariables.color.green4};
|
||||
border-radius: ${themeCssVariables.border.radius.pill};
|
||||
box-sizing: border-box;
|
||||
color: ${themeCssVariables.color.green9};
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
height: ${themeCssVariables.spacing[6]};
|
||||
padding: 0 ${themeCssVariables.spacing[2]} 0
|
||||
${themeCssVariables.spacing['1.5']};
|
||||
`;
|
||||
|
||||
const StyledPrefix = styled.span`
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.regular};
|
||||
`;
|
||||
|
||||
const StyledDuration = styled.span`
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledSuffix = styled.span`
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.regular};
|
||||
`;
|
||||
|
||||
type OnboardingTrialExtensionTagProps = {
|
||||
duration: number;
|
||||
};
|
||||
|
||||
export const OnboardingTrialExtensionTag = ({
|
||||
duration,
|
||||
}: OnboardingTrialExtensionTagProps) => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledTag>
|
||||
<IconCalendarEvent
|
||||
size={theme.icon.size.md}
|
||||
color={themeCssVariables.color.green9}
|
||||
/>
|
||||
<StyledPrefix>{t`Extended`}</StyledPrefix>
|
||||
<StyledDuration>{duration}</StyledDuration>
|
||||
<StyledSuffix>{t`days trial`}</StyledSuffix>
|
||||
</StyledTag>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user