Add v2 onboarding loading screen while creating workspace (#22152)

https://github.com/user-attachments/assets/cc7b1d10-7495-4f21-9311-4c22c0f14771

Adds the full-screen loading screen shown while a new workspace is being
created in the v2 sign-up flow (`SignInUpV2`), building on the v2
"Create your workspace" step.

How it works:
- Submitting the v2 create-workspace form marks the flow as v2
(`isOnboardingV2State`) and creates the workspace. The flag is carried
across the cross-subdomain redirect with an `onboardingV2=true` URL
param, so v2 users land on a new `/workspace-activation-v2` route
instead of v1's `/workspace-activation`.
- `WorkspaceActivationV2` runs the real `activateWorkspace` mutation on
mount and renders the loader: a pulsing Twenty logomark above a stack of
status messages that shift up one at a time, cycling once per second.
There is no faked/minimum duration; it advances to the next onboarding
step as soon as the workspace is activated.
- On activation failure it shows a "Workspace creation failed" screen
with a Retry button.

v1 onboarding is unchanged. Storybook:
`Modules/Auth/SignInUpWorkspaceActivationV2`.

Note: The flashes will be fixed in later PRs

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22152?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-06-26 10:46:34 +02:00
committed by GitHub
parent ce4d0f3447
commit cb49a7a053
24 changed files with 508 additions and 24 deletions
@@ -85,6 +85,7 @@ const setupMockState = (
calendarBookingPageId?: string | null,
returnToPath?: string,
currentWorkspace: object | null = { id: 'mock-workspace-id' },
isOnboardingV2 = false,
) => {
jest
.mocked(useAtomStateValue)
@@ -92,7 +93,8 @@ const setupMockState = (
.mockReturnValueOnce(calendarBookingPageId ?? 'mock-calendar-id')
.mockReturnValueOnce([{ namePlural: objectNamePlural ?? '' }])
.mockReturnValueOnce(verifyEmailRedirectPath)
.mockReturnValueOnce(returnToPath ?? '');
.mockReturnValueOnce(returnToPath ?? '')
.mockReturnValueOnce(isOnboardingV2);
};
// prettier-ignore
@@ -454,3 +456,37 @@ describe('usePageChangeEffectNavigateLocation — authenticated with no current
},
);
});
describe('usePageChangeEffectNavigateLocation — onboarding V2', () => {
const setupWorkspaceActivationV2Case = (loc: AppPath) => {
setupMockIsMatchingLocation(loc);
setupMockOnboardingStatus(OnboardingStatus.WORKSPACE_ACTIVATION);
setupMockIsWorkspaceActivationStatusEqualsTo(false);
setupMockHasAccessTokenPair(true);
setupMockIsOnAWorkspace(true);
setupMockUseQuery();
setupMockUseParams();
setupMockState(
undefined,
undefined,
undefined,
undefined,
{ id: 'mock-workspace-id' },
true,
);
};
it('routes to WorkspaceActivationV2 when onboardingV2 is active and status is WORKSPACE_ACTIVATION', () => {
setupWorkspaceActivationV2Case(AppPath.SignInUpV2);
expect(usePageChangeEffectNavigateLocation()).toEqual(
AppPath.WorkspaceActivationV2,
);
});
it('does not redirect away from the WorkspaceActivationV2 page during activation', () => {
setupWorkspaceActivationV2Case(AppPath.WorkspaceActivationV2);
expect(usePageChangeEffectNavigateLocation()).toBeUndefined();
});
});
@@ -3,6 +3,7 @@ import { ONBOARDING_PATHS } from '@/auth/constants/OnboardingPaths';
import { ONGOING_USER_CREATION_PATHS } from '@/auth/constants/OngoingUserCreationPaths';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isOnboardingV2State } from '@/auth/states/isOnboardingV2State';
import { returnToPathState } from '@/auth/states/returnToPathState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
@@ -76,6 +77,8 @@ export const usePageChangeEffectNavigateLocation = () => {
? returnToPath
: readReturnToPathFromUrlSearchParams();
const isOnboardingV2 = useAtomStateValue(isOnboardingV2State);
if (
(!hasAccessTokenPair || !isOnAWorkspace || !isDefined(currentWorkspace)) &&
!someMatchingLocationOf([
@@ -118,11 +121,14 @@ export const usePageChangeEffectNavigateLocation = () => {
onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION &&
!someMatchingLocationOf([
AppPath.WorkspaceActivation,
AppPath.WorkspaceActivationV2,
AppPath.BookCallDecision,
AppPath.BookCall,
])
) {
return AppPath.WorkspaceActivation;
return isOnboardingV2
? AppPath.WorkspaceActivationV2
: AppPath.WorkspaceActivation;
}
if (