Polish onboarding v2 verify animation and step screens (#22585)

https://github.com/user-attachments/assets/1d9b8dc2-ef01-4202-a97e-b41cab048f87





A few polish tweaks to onboarding v2:

- **Verify/workspace-creation animation:** emphasize the key phrase of
each message in medium weight, the rest regular (e.g. "Creating your
**workspace**…").
- **Wider content column:** 340px → 440px. Collapses to full width on
mobile via the existing `max-width: 100%` on every consumer.
- **Sticky disabled buttons:** step submit buttons now stay disabled
from submit through navigation instead of briefly re-enabling once the
mutation resolves.
- **Fewer pulse loaders:** stop the pulsing logo from flashing when
navigating between onboarding steps (removed the step-page Suspense
fallback loader). The verify animation, cold-boot gates, and sign-in
fallbacks are unchanged.

Note: the reworded activation messages get new Lingui catalog IDs, so
non-English locales fall back to English until catalogs are
re-extracted.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22585?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Raphaël Bosi
2026-07-06 16:44:44 +02:00
committed by GitHub
parent 6c40c7b91a
commit 62c7e8f6b4
25 changed files with 162 additions and 106 deletions
+6 -6
View File
@@ -57,23 +57,23 @@
</script>
<style>
:root {
/* THEME_DARK.background.tertiary */
--theme-dark-background-tertiary: #1d1d1d;
/* THEME_LIGHT.background.tertiary */
--theme-light-background-tertiary: #f1f1f1;
/* THEME_DARK.background.secondary */
--theme-dark-background-secondary: #1b1b1b;
/* THEME_LIGHT.background.secondary */
--theme-light-background-secondary: #fcfcfc;
}
@media (prefers-color-scheme: dark) {
html,
body {
background-color: var(--theme-dark-background-tertiary);
background-color: var(--theme-dark-background-secondary);
}
}
@media (prefers-color-scheme: light) {
html,
body {
background-color: var(--theme-light-background-tertiary);
background-color: var(--theme-light-background-secondary);
}
}
</style>
@@ -0,0 +1,6 @@
import { AppPath } from 'twenty-shared/types';
export const ONBOARDING_VERIFY_PATHS = [
AppPath.Verify,
AppPath.WorkspaceActivation,
];
@@ -1,8 +0,0 @@
import { msg } from '@lingui/core/macro';
export const WORKSPACE_ACTIVATION_MESSAGES = [
msg`Creating your workspace...`,
msg`Setting up your database...`,
msg`Creating your data model...`,
msg`Prefilling your workspace data...`,
];
@@ -0,0 +1,54 @@
import { StyledOnboardingActivationMessageEmphasis } from '@/onboarding/components/StyledOnboardingActivationMessageEmphasis';
import { type OnboardingActivationMessage } from '@/onboarding/types/OnboardingActivationMessage';
import { Trans } from '@lingui/react/macro';
export const WORKSPACE_ACTIVATION_MESSAGES: OnboardingActivationMessage[] = [
{
id: 'creating-workspace',
content: (
<Trans>
Creating your{' '}
<StyledOnboardingActivationMessageEmphasis>
workspace
</StyledOnboardingActivationMessageEmphasis>
...
</Trans>
),
},
{
id: 'setting-up-database',
content: (
<Trans>
Setting up your{' '}
<StyledOnboardingActivationMessageEmphasis>
database
</StyledOnboardingActivationMessageEmphasis>
...
</Trans>
),
},
{
id: 'creating-data-model',
content: (
<Trans>
Creating your{' '}
<StyledOnboardingActivationMessageEmphasis>
data model
</StyledOnboardingActivationMessageEmphasis>
...
</Trans>
),
},
{
id: 'prefilling-workspace-data',
content: (
<Trans>
Prefilling your{' '}
<StyledOnboardingActivationMessageEmphasis>
workspace data
</StyledOnboardingActivationMessageEmphasis>
...
</Trans>
),
},
];
@@ -0,0 +1,9 @@
import { matchPath } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { ONBOARDING_VERIFY_PATHS } from '@/auth/constants/OnboardingVerifyPaths';
export const isOnOnboardingVerifyPath = (pathname: string) =>
ONBOARDING_VERIFY_PATHS.some((verifyPath) =>
isDefined(matchPath(verifyPath, pathname)),
);
@@ -1,8 +1,7 @@
import { SubTitle } from '@/auth/components/SubTitle';
import { useOnboardingMotionTransition } from '@/onboarding/hooks/useOnboardingMotionTransition';
import { type OnboardingActivationMessage } from '@/onboarding/types/OnboardingActivationMessage';
import { styled } from '@linaria/react';
import { type MessageDescriptor } from '@lingui/core';
import { useLingui } from '@lingui/react/macro';
import { motion } from 'framer-motion';
const STEP_OPACITIES = [1, 0.4, 0.12];
@@ -26,7 +25,7 @@ const StyledStepBase = styled.div`
const StyledStep = motion.create(StyledStepBase);
type OnboardingActivationStepsProps = {
messages: MessageDescriptor[];
messages: OnboardingActivationMessage[];
messageIndex: number;
};
@@ -34,7 +33,6 @@ export const OnboardingActivationSteps = ({
messages,
messageIndex,
}: OnboardingActivationStepsProps) => {
const { i18n } = useLingui();
const transition = useOnboardingMotionTransition();
return (
@@ -53,7 +51,7 @@ export const OnboardingActivationSteps = ({
}}
transition={transition}
>
<SubTitle>{i18n._(message)}</SubTitle>
<SubTitle>{message.content}</SubTitle>
</StyledStep>
);
})}
@@ -1,7 +1,5 @@
import { useOnboardingContentWidth } from '@/onboarding/hooks/useOnboardingContentWidth';
import { useOnboardingMotionTransition } from '@/onboarding/hooks/useOnboardingMotionTransition';
import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { styled } from '@linaria/react';
import { motion } from 'framer-motion';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, IconCoins, IconInfoCircle } from 'twenty-ui/icon';
@@ -30,10 +28,10 @@ const StyledLeftSide = styled(StyledSide)`
padding-right: ${themeCssVariables.spacing[1]};
`;
const StyledCenter = styled.div<{ contentWidth: number }>`
const StyledCenter = styled.div`
align-items: center;
display: flex;
flex: 0 1 ${({ contentWidth }) => `${contentWidth}px`};
flex: 0 1 ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
justify-content: flex-start;
min-width: 0;
`;
@@ -43,7 +41,7 @@ const StyledRightSide = styled(StyledSide)`
padding-left: ${themeCssVariables.spacing[1]};
`;
const StyledLogoBase = styled.div`
const StyledLogo = styled.div`
background-image: url('/images/integrations/twenty-logo.svg');
background-size: cover;
height: ${themeCssVariables.spacing[6]};
@@ -51,8 +49,6 @@ const StyledLogoBase = styled.div`
width: ${themeCssVariables.spacing[6]};
`;
const StyledLogo = motion.create(StyledLogoBase);
const StyledFreeCredits = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.tertiary};
@@ -109,8 +105,6 @@ export const OnboardingHeader = ({
}: OnboardingHeaderProps) => {
const { t } = useLingui();
const theme = useTheme();
const contentWidth = useOnboardingContentWidth();
const transition = useOnboardingMotionTransition();
return (
<StyledHeader>
@@ -125,8 +119,8 @@ export const OnboardingHeader = ({
/>
)}
</StyledLeftSide>
<StyledCenter contentWidth={contentWidth}>
<StyledLogo layout transition={transition} />
<StyledCenter>
<StyledLogo />
</StyledCenter>
<StyledRightSide>
{isDefined(freeCredits) && (
@@ -1,3 +1,4 @@
import { isOnOnboardingVerifyPath } from '@/auth/utils/isOnOnboardingVerifyPath';
import { OnboardingPulsingLogo } from '@/onboarding/components/OnboardingPulsingLogo';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -12,8 +13,12 @@ const StyledContainer = styled.div`
width: 100%;
`;
// The verify screen renders its own pulsing logo, so the loader shown before it
// omits the logo to avoid a double-logo flash on cold boot into the verify step.
export const OnboardingPageLoader = () => (
<StyledContainer>
<OnboardingPulsingLogo />
{!isOnOnboardingVerifyPath(window.location.pathname) && (
<OnboardingPulsingLogo />
)}
</StyledContainer>
);
@@ -1,17 +1,11 @@
import { OnboardingPulsingLogo } from '@/onboarding/components/OnboardingPulsingLogo';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div`
align-items: center;
display: flex;
background: ${themeCssVariables.background.secondary};
flex: 1;
justify-content: center;
min-height: 0;
width: 100%;
`;
export const OnboardingStepPageLoader = () => (
<StyledContainer>
<OnboardingPulsingLogo />
</StyledContainer>
);
export const OnboardingStepPageLoader = () => <StyledContainer />;
@@ -0,0 +1,7 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const StyledOnboardingActivationMessageEmphasis = styled.span`
color: ${themeCssVariables.font.color.primary};
font-weight: ${themeCssVariables.font.weight.medium};
`;
@@ -0,0 +1,9 @@
import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { styled } from '@linaria/react';
export const StyledOnboardingContentBlock = styled.div`
display: flex;
flex-direction: column;
max-width: 100%;
width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
`;
@@ -1,11 +1,7 @@
import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { StyledOnboardingContentBlock } from '@/onboarding/components/StyledOnboardingContentBlock';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const StyledOnboardingStepHeading = styled.div`
display: flex;
flex-direction: column;
export const StyledOnboardingStepHeading = styled(StyledOnboardingContentBlock)`
gap: ${themeCssVariables.spacing[4]};
max-width: 100%;
width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
`;
@@ -1,7 +0,0 @@
import { WORKSPACE_ACTIVATION_MESSAGES } from '@/auth/sign-in-up/constants/WorkspaceActivationMessages';
import { msg } from '@lingui/core/macro';
export const ONBOARDING_ACTIVATION_MESSAGES = [
msg`Verifying your login token`,
...WORKSPACE_ACTIVATION_MESSAGES,
];
@@ -0,0 +1,19 @@
import { WORKSPACE_ACTIVATION_MESSAGES } from '@/auth/sign-in-up/constants/WorkspaceActivationMessages';
import { StyledOnboardingActivationMessageEmphasis } from '@/onboarding/components/StyledOnboardingActivationMessageEmphasis';
import { type OnboardingActivationMessage } from '@/onboarding/types/OnboardingActivationMessage';
import { Trans } from '@lingui/react/macro';
export const ONBOARDING_ACTIVATION_MESSAGES: OnboardingActivationMessage[] = [
{
id: 'verifying-login-token',
content: (
<Trans>
Verifying your{' '}
<StyledOnboardingActivationMessageEmphasis>
login token
</StyledOnboardingActivationMessageEmphasis>
</Trans>
),
},
...WORKSPACE_ACTIVATION_MESSAGES,
];
@@ -1 +1 @@
export const ONBOARDING_CONTENT_BLOCK_WIDTH = 340;
export const ONBOARDING_CONTENT_BLOCK_WIDTH = 440;
@@ -1 +0,0 @@
export const UPGRADE_STEP_CONTENT_WIDTH = 440;
@@ -31,6 +31,8 @@ export const useInviteTeam = () => {
const setOnboardingFreeCredits = useSetAtomState(onboardingFreeCreditsState);
const onboardingConfig = useAtomStateValue(onboardingConfigState);
const [isNavigating, setIsNavigating] = useState(false);
const {
control,
handleSubmit,
@@ -152,6 +154,7 @@ export const useInviteTeam = () => {
}
setNextOnboardingStatus();
setIsNavigating(true);
},
[
enqueueSuccessSnackBar,
@@ -186,5 +189,6 @@ export const useInviteTeam = () => {
getPlaceholder,
isValid,
isSubmitting,
isNavigating,
};
};
@@ -1,13 +0,0 @@
import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { UPGRADE_STEP_CONTENT_WIDTH } from '@/onboarding/constants/UpgradeStepContentWidth';
import { useLocation } from 'react-router-dom';
import { AppPath } from 'twenty-shared/types';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const useOnboardingContentWidth = () => {
const location = useLocation();
return isMatchingLocation(location, AppPath.PlanRequired)
? UPGRADE_STEP_CONTENT_WIDTH
: ONBOARDING_CONTENT_BLOCK_WIDTH;
};
@@ -0,0 +1,6 @@
import { type ReactNode } from 'react';
export type OnboardingActivationMessage = {
id: string;
content: ReactNode;
};
@@ -3,7 +3,7 @@ import { Outlet } from 'react-router-dom';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledLayout = styled.div`
background: ${themeCssVariables.background.noisy};
background: ${themeCssVariables.background.secondary};
display: flex;
flex-direction: column;
height: 100dvh;
@@ -1,6 +1,5 @@
import { billingState } from '@/client-config/states/billingState';
import { onboardingConfigState } from '@/client-config/states/onboardingConfigState';
import { OnboardingPageLoader } from '@/onboarding/components/OnboardingPageLoader';
import { usePlans } from '@/settings/billing/hooks/usePlans';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isDefined } from 'twenty-shared/utils';
@@ -16,7 +15,5 @@ export const ChooseYourPlan = () => {
billing={billing}
creditsReward={onboardingConfig?.upgradeCreditsReward}
/>
) : (
<OnboardingPageLoader />
);
) : null;
};
@@ -87,6 +87,8 @@ export const CreateProfile = () => {
);
const { updateWorkspaceMemberSettings } = useUpdateWorkspaceMemberSettings();
const [isNavigating, setIsNavigating] = useState(false);
const {
control,
handleSubmit,
@@ -151,7 +153,9 @@ export const CreateProfile = () => {
});
setNextOnboardingStatus();
setIsNavigating(true);
} catch (error: any) {
setIsNavigating(false);
enqueueErrorSnackBar({
apolloError: CombinedGraphQLErrors.is(error) ? error : undefined,
});
@@ -279,7 +283,7 @@ export const CreateProfile = () => {
<MainButton
title={t`Continue`}
onClick={handleSubmit(onSubmit)}
disabled={!isValid || isSubmitting}
disabled={!isValid || isSubmitting || isNavigating}
fullWidth
/>
</StyledButtonContainer>
@@ -51,6 +51,7 @@ export const InviteTeam = () => {
getPlaceholder,
isValid,
isSubmitting,
isNavigating,
} = useInviteTeam();
const onboardingConfig = useAtomStateValue(onboardingConfigState);
const creditsRewardPerUser = onboardingConfig?.inviteTeamCreditsRewardPerUser;
@@ -123,11 +124,14 @@ export const InviteTeam = () => {
<StyledFooter>
<MainButton
title={t`Invite`}
disabled={!isValid || isSubmitting}
disabled={!isValid || isSubmitting || isNavigating}
onClick={handleSubmit(onSubmit)}
fullWidth
/>
<OnboardingSkipButton onClick={handleSkip} />
<OnboardingSkipButton
onClick={handleSkip}
disabled={isSubmitting || isNavigating}
/>
</StyledFooter>
</OnboardingStepAnimatedItem>
</StyledOnboardingStepPage>
@@ -4,7 +4,6 @@ import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMe
import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicrosoftCalendarEnabledState';
import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState';
import { onboardingConfigState } from '@/client-config/states/onboardingConfigState';
import { OnboardingPageLoader } from '@/onboarding/components/OnboardingPageLoader';
import { SyncEmailsAutoSkipEffect } from '@/onboarding/effect-components/SyncEmailsAutoSkipEffect';
import { useSkipSyncEmailOnboardingStep } from '@/onboarding/hooks/useSkipSyncEmailOnboardingStep';
import { onboardingFreeCreditsState } from '@/onboarding/states/onboardingFreeCreditsState';
@@ -86,16 +85,11 @@ export const SyncEmails = () => {
}, []);
if (!isClientConfigLoaded) {
return <OnboardingPageLoader />;
return null;
}
if (!hasProviderEnabled && !hasAutoSkipFailed) {
return (
<>
<SyncEmailsAutoSkipEffect onError={handleAutoSkipError} />
<OnboardingPageLoader />
</>
);
return <SyncEmailsAutoSkipEffect onError={handleAutoSkipError} />;
}
return (
@@ -3,6 +3,8 @@ import { useAuth } from '@/auth/hooks/useAuth';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { currentUserState } from '@/auth/states/currentUserState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { StyledOnboardingContentBlock } from '@/onboarding/components/StyledOnboardingContentBlock';
import { StyledOnboardingStepHeading } from '@/onboarding/components/StyledOnboardingStepHeading';
import { StyledOnboardingStepPage } from '@/onboarding/components/StyledOnboardingStepPage';
import { StyledOnboardingStepSubtitle } from '@/onboarding/components/StyledOnboardingStepSubtitle';
import { StyledOnboardingStepTagsRow } from '@/onboarding/components/StyledOnboardingStepTagsRow';
@@ -11,7 +13,6 @@ import { OnboardingStepAnimatedItem } from '@/onboarding/components/OnboardingSt
import { OnboardingCreditsRewardTag } from '@/onboarding/components/import-contacts/OnboardingCreditsRewardTag';
import { OnboardingPlanCard } from '@/onboarding/components/upgrade-free-trial/OnboardingPlanCard';
import { OnboardingTrialExtensionTag } from '@/onboarding/components/upgrade-free-trial/OnboardingTrialExtensionTag';
import { UPGRADE_STEP_CONTENT_WIDTH } from '@/onboarding/constants/UpgradeStepContentWidth';
import { useBaseLicensedPriceByPlanKeyAndInterval } from '@/settings/billing/hooks/useBaseLicensedPriceByPlanKeyAndInterval';
import { useHandleCheckoutSession } from '@/settings/billing/hooks/useHandleCheckoutSession';
import { useStripeAppearance } from '@/settings/billing/hooks/useStripeAppearance';
@@ -39,29 +40,13 @@ const StyledPage = styled(StyledOnboardingStepPage)`
padding: ${themeCssVariables.spacing[6]} ${themeCssVariables.spacing[8]};
`;
const StyledHeading = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[4]};
max-width: 100%;
width: ${UPGRADE_STEP_CONTENT_WIDTH}px;
`;
const StyledCards = styled.div`
display: flex;
flex-direction: column;
const StyledCards = styled(StyledOnboardingContentBlock)`
gap: ${themeCssVariables.spacing['1.5']};
max-width: 100%;
width: ${UPGRADE_STEP_CONTENT_WIDTH}px;
`;
const StyledFooter = styled.div`
const StyledFooter = styled(StyledOnboardingContentBlock)`
align-items: center;
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing['1.5']};
max-width: 100%;
width: ${UPGRADE_STEP_CONTENT_WIDTH}px;
`;
const StyledLinkGroup = styled.div`
@@ -282,7 +267,7 @@ export const UpgradeFreeTrial = ({
return (
<StyledPage>
<StyledHeading>
<StyledOnboardingStepHeading>
<OnboardingStepAnimatedItem index={0}>
<StyledOnboardingStepTitle>{t`Upgrade your free trial`}</StyledOnboardingStepTitle>
</OnboardingStepAnimatedItem>
@@ -303,7 +288,7 @@ export const UpgradeFreeTrial = ({
)}
</StyledOnboardingStepTagsRow>
</OnboardingStepAnimatedItem>
</StyledHeading>
</StyledOnboardingStepHeading>
{isDefined(stripePromise) && isDefined(baseProductPrice) ? (
<Elements