Add smooth page transitions to onboarding v2 (#22392)
## Before https://github.com/user-attachments/assets/d2fcd5ce-7e34-4f07-9a52-cac8acdc37cd ## After https://github.com/user-attachments/assets/5c245949-cff9-41b2-802d-3deeb562efa6 On a full-page load of a v2 onboarding URL (the post-signup workspace-subdomain redirect), Lingui's `I18nProvider` renders `null` until the locale chunk async-activates, so the app is blank for ~2s before the verify step appears. Steps also hard-cut and flashed a loader between each other. - Show a pulsing-logo loader until the locale activates (a gate above `I18nProvider`), scoped to onboarding v2 paths so every other page is unchanged. - Cross-fade between steps and preload their chunks on entry, so navigating never flashes the loader. Frontend-only; i18n loading itself is untouched. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22392?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:
@@ -1,4 +1,5 @@
|
||||
import { AppRouter } from '@/app/components/AppRouter';
|
||||
import { I18nActivationGate } from '@/app/components/I18nActivationGate';
|
||||
import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect';
|
||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||
import { AppRootErrorFallback } from '@/error-handler/components/AppRootErrorFallback';
|
||||
@@ -22,24 +23,26 @@ export const App = () => {
|
||||
resetOnLocationChange={false}
|
||||
FallbackComponent={AppRootErrorFallback}
|
||||
>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<ApolloDevLogEffect />
|
||||
<SnackBarComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'snack-bar-manager' }}
|
||||
>
|
||||
<IconsProvider>
|
||||
<ExceptionHandlerProvider>
|
||||
<HelmetProvider>
|
||||
<ClickOutsideListenerContext.Provider
|
||||
value={{ excludedClickOutsideId: undefined }}
|
||||
>
|
||||
<AppRouter />
|
||||
</ClickOutsideListenerContext.Provider>
|
||||
</HelmetProvider>
|
||||
</ExceptionHandlerProvider>
|
||||
</IconsProvider>
|
||||
</SnackBarComponentInstanceContext.Provider>
|
||||
</I18nProvider>
|
||||
<I18nActivationGate>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<ApolloDevLogEffect />
|
||||
<SnackBarComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'snack-bar-manager' }}
|
||||
>
|
||||
<IconsProvider>
|
||||
<ExceptionHandlerProvider>
|
||||
<HelmetProvider>
|
||||
<ClickOutsideListenerContext.Provider
|
||||
value={{ excludedClickOutsideId: undefined }}
|
||||
>
|
||||
<AppRouter />
|
||||
</ClickOutsideListenerContext.Provider>
|
||||
</HelmetProvider>
|
||||
</ExceptionHandlerProvider>
|
||||
</IconsProvider>
|
||||
</SnackBarComponentInstanceContext.Provider>
|
||||
</I18nProvider>
|
||||
</I18nActivationGate>
|
||||
</AppErrorBoundary>
|
||||
</JotaiProvider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ONBOARDING_V2_PATHS } from '@/auth/constants/OnboardingV2Paths';
|
||||
import { OnboardingPageLoader } from '@/onboarding/components/OnboardingPageLoader';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type ReactNode, useEffect, useState } from 'react';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type I18nActivationGateProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const I18nActivationGate = ({ children }: I18nActivationGateProps) => {
|
||||
const [isLocaleActivated, setIsLocaleActivated] = useState(() =>
|
||||
isNonEmptyString(i18n.locale),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleLocaleChange = () => {
|
||||
if (isNonEmptyString(i18n.locale)) {
|
||||
setIsLocaleActivated(true);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = i18n.on('change', handleLocaleChange);
|
||||
handleLocaleChange();
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
if (!isLocaleActivated) {
|
||||
const isOnboardingLocation = ONBOARDING_V2_PATHS.some((onboardingPath) =>
|
||||
isDefined(matchPath(onboardingPath, window.location.pathname)),
|
||||
);
|
||||
|
||||
return isOnboardingLocation ? <OnboardingPageLoader /> : null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -5,7 +5,10 @@ import { VerifyLoginTokenEffect } from '@/auth/components/VerifyLoginTokenEffect
|
||||
|
||||
import { VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect';
|
||||
import indexAppPath from '@/navigation/utils/indexAppPath';
|
||||
import { OnboardingPageLoader } from '@/onboarding/components/OnboardingPageLoader';
|
||||
import { OnboardingV2TransitionOutlet } from '@/onboarding/components/OnboardingV2TransitionOutlet';
|
||||
import { VerifyV2 } from '~/pages/onboarding/VerifyV2';
|
||||
import { lazyWithPreload } from '~/utils/lazyWithPreload';
|
||||
import { RecordIndexSkeletonLoader } from '@/object-record/record-index/components/RecordIndexSkeletonLoader';
|
||||
import { BlankLayout } from '@/ui/layout/page/components/BlankLayout';
|
||||
import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout';
|
||||
@@ -63,7 +66,7 @@ const WorkspaceActivation = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const WorkspaceActivationV2 = lazy(() =>
|
||||
const WorkspaceActivationV2 = lazyWithPreload(() =>
|
||||
import('~/pages/onboarding/WorkspaceActivationV2').then((module) => ({
|
||||
default: module.WorkspaceActivationV2,
|
||||
})),
|
||||
@@ -75,7 +78,7 @@ const CreateProfile = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const CreateProfileV2 = lazy(() =>
|
||||
const CreateProfileV2 = lazyWithPreload(() =>
|
||||
import('~/pages/onboarding/CreateProfileV2').then((module) => ({
|
||||
default: module.CreateProfileV2,
|
||||
})),
|
||||
@@ -87,13 +90,13 @@ const SyncEmails = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SyncEmailsV2 = lazy(() =>
|
||||
const SyncEmailsV2 = lazyWithPreload(() =>
|
||||
import('~/pages/onboarding/SyncEmailsV2').then((module) => ({
|
||||
default: module.SyncEmailsV2,
|
||||
})),
|
||||
);
|
||||
|
||||
const InstallAppsV2 = lazy(() =>
|
||||
const InstallAppsV2 = lazyWithPreload(() =>
|
||||
import('~/pages/onboarding/InstallAppsV2').then((module) => ({
|
||||
default: module.InstallAppsV2,
|
||||
})),
|
||||
@@ -105,7 +108,7 @@ const InviteTeam = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const InviteTeamV2 = lazy(() =>
|
||||
const InviteTeamV2 = lazyWithPreload(() =>
|
||||
import('~/pages/onboarding/InviteTeamV2').then((module) => ({
|
||||
default: module.InviteTeamV2,
|
||||
})),
|
||||
@@ -117,7 +120,7 @@ const ChooseYourPlan = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const ChooseYourPlanV2 = lazy(() =>
|
||||
const ChooseYourPlanV2 = lazyWithPreload(() =>
|
||||
import('~/pages/onboarding/ChooseYourPlanV2').then((module) => ({
|
||||
default: module.ChooseYourPlanV2,
|
||||
})),
|
||||
@@ -153,6 +156,17 @@ const NotFound = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const preloadOnboardingV2Pages = () => {
|
||||
void WorkspaceActivationV2.preload();
|
||||
void CreateProfileV2.preload();
|
||||
void SyncEmailsV2.preload();
|
||||
void InstallAppsV2.preload();
|
||||
void InviteTeamV2.preload();
|
||||
void ChooseYourPlanV2.preload();
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useCreateAppRouter = (
|
||||
isFunctionSettingsEnabled?: boolean,
|
||||
isAdminPageEnabled?: boolean,
|
||||
@@ -312,62 +326,67 @@ export const useCreateAppRouter = (
|
||||
</Route>
|
||||
<Route element={<BlankLayout />}>
|
||||
<Route
|
||||
path={AppPath.SignInUpV2}
|
||||
element={
|
||||
<LazyRoute fallback={null}>
|
||||
<SignInUpV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route path={AppPath.VerifyV2} element={<VerifyV2 />} />
|
||||
<Route
|
||||
path={AppPath.WorkspaceActivationV2}
|
||||
element={
|
||||
<LazyRoute fallback={null}>
|
||||
<WorkspaceActivationV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.CreateProfileV2}
|
||||
element={
|
||||
<LazyRoute fallback={null}>
|
||||
<CreateProfileV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.SyncEmailsV2}
|
||||
element={
|
||||
<LazyRoute fallback={null}>
|
||||
<SyncEmailsV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.InstallAppsV2}
|
||||
element={
|
||||
<LazyRoute fallback={null}>
|
||||
<InstallAppsV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.InviteTeamV2}
|
||||
element={
|
||||
<LazyRoute fallback={null}>
|
||||
<InviteTeamV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.PlanRequiredV2}
|
||||
element={
|
||||
<LazyRoute fallback={null}>
|
||||
<ChooseYourPlanV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
element={<OnboardingV2TransitionOutlet />}
|
||||
loader={preloadOnboardingV2Pages}
|
||||
>
|
||||
<Route
|
||||
path={AppPath.SignInUpV2}
|
||||
element={
|
||||
<LazyRoute fallback={<OnboardingPageLoader />}>
|
||||
<SignInUpV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route path={AppPath.VerifyV2} element={<VerifyV2 />} />
|
||||
<Route
|
||||
path={AppPath.WorkspaceActivationV2}
|
||||
element={
|
||||
<LazyRoute fallback={<OnboardingPageLoader />}>
|
||||
<WorkspaceActivationV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.CreateProfileV2}
|
||||
element={
|
||||
<LazyRoute fallback={<OnboardingPageLoader />}>
|
||||
<CreateProfileV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.SyncEmailsV2}
|
||||
element={
|
||||
<LazyRoute fallback={<OnboardingPageLoader />}>
|
||||
<SyncEmailsV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.InstallAppsV2}
|
||||
element={
|
||||
<LazyRoute fallback={<OnboardingPageLoader />}>
|
||||
<InstallAppsV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.InviteTeamV2}
|
||||
element={
|
||||
<LazyRoute fallback={<OnboardingPageLoader />}>
|
||||
<InviteTeamV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.PlanRequiredV2}
|
||||
element={
|
||||
<LazyRoute fallback={<OnboardingPageLoader />}>
|
||||
<ChooseYourPlanV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={AppPath.Authorize}
|
||||
element={
|
||||
|
||||
Reference in New Issue
Block a user