From 566c3b662954de932677a3fefe69735a45fe55ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:12:34 +0200 Subject: [PATCH] Remove v1 onboarding and rely only on v2 (#22398) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/user-attachments/assets/a6bfaac3-6c79-4fd5-999a-e6a70cff8ac8 Removes the old (v1) signup and onboarding flow now that v2 is the only path, and drops the `isOnboardingV2` flag entirely. The surviving (formerly-v2) pages reclaim the canonical `AppPath` members and clean URLs (`/welcome`, `/verify`, `/workspace-activation`, `/create/profile`, `/sync/emails`, `/install-apps`, `/invite-team`, `/plan-required`). - Deletes the v1 pages, the v1 workspace-creation form, the `isOnboardingV2State` flag + `onboardingV2` URL-param plumbing, and `InstallAppsAutoSkipEffect`. - Collapses the router and page-change navigation matrix to a single set of paths, and renames the v2 components/stories to drop the `V2` suffix. Follow-up fixes so the single flow behaves correctly on every deployment: - Restore the captcha-token, query-param and pageview effects on the default (root) domain, and serve `/authorize` there so OAuth login keeps working. - Gate the invite-team → `/plan-required` interception on billing so billing-disabled instances aren't trapped on the upgrade page. - On a cold boot to an auth/onboarding path, show the onboarding loader instead of the CRM skeleton, and add `/verify-email` and `/plan-required/payment-success` to that loader path list. - Add a retry to PaymentSuccess after the confirmation timeout, fix the InstallApps icon crossfade, restyle the book-call pages for the full-page layout, and delete code orphaned by the v1 removal. - Extract the pageview/captcha/query-param logic out of `PageChangeEffect` into standalone Effect components shared by the root and workspace app trees. Review in cubic --------- Co-authored-by: Félix Malfait --- .../twenty-e2e-testing/lib/pom/loginPage.ts | 29 +- .../tests/authentication/onboarding.spec.ts | 70 +++ .../signup_invite_email.spec.ts | 2 + ...sePageChangeEffectNavigateLocation.test.ts | 258 +++-------- .../usePageChangeEffectNavigateLocation.ts | 41 +- .../components/UserOrMetadataLoader.tsx | 11 - .../components/TrackPageViewEffect.tsx | 46 ++ .../modules/apollo/hooks/useApolloFactory.ts | 1 - .../src/modules/app/components/App.tsx | 4 +- .../app/components/AppRouterProviders.tsx | 98 ----- .../modules/app/components/DomainShell.tsx | 40 ++ .../app/components/I18nActivationGate.tsx | 12 +- .../src/modules/app/components/RootApp.tsx | 7 + .../app/components/RootAppProviders.tsx | 55 +++ .../app/components/SharedAppProviders.tsx | 19 + .../{AppRouter.tsx => WorkspaceApp.tsx} | 15 +- .../app/components/WorkspaceAppProviders.tsx | 92 ++++ .../components/__tests__/DomainShell.test.tsx | 125 ++++++ .../InitializeQueryParamStateEffect.tsx | 15 + .../effect-components/PageChangeEffect.tsx | 65 +-- .../modules/app/hooks/useCreateAppRouter.tsx | 401 ------------------ .../app/hooks/useCreateRootAppRouter.tsx | 91 ++++ .../app/hooks/useCreateWorkspaceAppRouter.tsx | 341 +++++++++++++++ .../app/hooks/useInitializeQueryParamState.ts | 6 - .../src/modules/auth/components/AuthModal.tsx | 45 -- .../auth/components/AuthModalMountEffect.tsx | 19 - .../modules/auth/components/VerifyEmail.tsx | 33 ++ .../auth/components/VerifyEmailEffect.tsx | 36 +- .../components/VerifyLoginTokenEffect.tsx | 10 +- ...ct.stories.tsx => VerifyEmail.stories.tsx} | 22 +- ...ilEffect.test.tsx => VerifyEmail.test.tsx} | 16 +- .../__tests__/VerifyLoginTokenEffect.test.tsx | 72 ++++ .../auth/constants/AuthAndOnboardingPaths.ts | 9 + .../modules/auth/constants/AuthModalConfig.ts | 24 -- .../src/modules/auth/constants/AuthModalId.ts | 1 - .../modules/auth/constants/OnboardingPaths.ts | 7 +- .../constants/OnboardingTransitionPaths.ts | 15 + .../auth/constants/OnboardingV2Paths.ts | 12 - .../constants/OngoingUserCreationPaths.ts | 2 - .../auth/hooks/useIsOnAuthOrOnboardingPage.ts | 11 + .../auth/sign-in-up/components/FooterNote.tsx | 4 +- .../components/SignInUpGlobalScopeForm.tsx | 117 ++--- ...ontent.tsx => SignInUpStandardContent.tsx} | 29 +- .../SignInUpWorkspaceActivationV2.tsx | 77 ---- .../SignInUpWorkspaceCreationLoader.tsx | 28 -- .../SignInUpWorkspaceActivationV2.stories.tsx | 38 -- .../components/internal/LastUsedPill.tsx | 13 +- ...nUpTwoFactorAuthenticationVerification.tsx | 9 +- .../SignInUpWorkspaceCreationForm.tsx | 355 +++++++++++----- .../SignInUpWorkspaceCreationFormV2.tsx | 297 ------------- .../SignInUpWorkspaceCreationForm.test.tsx | 162 +++---- .../SignInUpWorkspaceCreationFormV2.test.tsx | 192 --------- .../useWorkspaceSubdomainField.test.tsx | 1 - .../hooks/useSignUpInNewWorkspace.ts | 7 +- .../hooks/useWorkspaceSubdomainField.ts | 12 - .../__tests__/isOnboardingV2State.test.ts | 49 --- .../auth/states/isOnboardingV2State.ts | 7 - .../modules/auth/utils/getAuthModalConfig.ts | 18 - .../utils/isOnOnboardingTransitionPath.ts | 9 + .../RequestFreshCaptchaTokenEffect.tsx | 21 + .../constants/CaptchaProtectedPaths.ts | 2 - ...useBuildSearchParamsFromUrlSyncedStates.ts | 3 - .../components/MinimalMetadataGate.tsx | 20 + .../components/MinimalMetadataGater.tsx | 49 --- .../MinimalMetadataLoadEffect.tsx | 6 +- .../components/OnboardingActivationOutlet.tsx | 23 + .../components/OnboardingActivationSteps.tsx | 62 +++ .../OnboardingActivationStepsEffect.tsx} | 14 +- .../OnboardingActivationStepsProgress.tsx | 57 +++ .../components/OnboardingAnimatedReveal.tsx | 41 ++ ...rdingV2Header.tsx => OnboardingHeader.tsx} | 36 +- ...rdingV2Layout.tsx => OnboardingLayout.tsx} | 10 +- .../components/OnboardingPageLoader.tsx | 2 +- .../components/OnboardingSkipButton.tsx | 34 ++ .../components/OnboardingStepAnimatedItem.tsx | 44 ++ .../components/OnboardingStepLayout.tsx | 13 + .../components/OnboardingStepPageLoader.tsx | 17 + .../OnboardingSyncEmailsSettingsCard.tsx | 42 -- ...let.tsx => OnboardingTransitionOutlet.tsx} | 24 +- .../components/OnboardingVerifyLayout.tsx | 27 ++ .../components/StyledOnboardingStepHeading.ts | 11 + .../components/StyledOnboardingStepPage.ts | 16 + .../StyledOnboardingStepSubtitle.ts | 9 + .../components/StyledOnboardingStepTagsRow.ts | 9 + .../components/StyledOnboardingStepTitle.ts | 10 + .../OnboardingActivationSteps.stories.tsx | 38 ++ .../OnboardingCreditsRewardTag.tsx | 4 +- .../OnboardingImportPreview.tsx | 9 +- .../OnboardingImportPreviewEmails.tsx | 37 +- .../OnboardingImportPreviewSyncBadge.tsx | 7 +- .../OnboardingImportPrivacyNote.tsx | 8 +- .../import-contacts/OnboardingTrustBadges.tsx | 29 +- .../upgrade-free-trial/OnboardingPlanCard.tsx | 89 +++- .../ImportContactsPreviewCalendarEvents.ts | 6 +- .../constants/OnboardingActivationMessages.ts | 7 + .../constants/OnboardingContentBlockWidth.ts | 1 + .../constants/OnboardingMotionSlideOffset.ts | 1 + .../constants/OnboardingMotionStaggerDelay.ts | 1 + .../constants/OnboardingSyncEmailsOptions.ts | 46 -- .../constants/UpgradeStepContentWidth.ts | 1 + .../WorkspaceActivationFirstMessageIndex.ts | 5 + .../InstallAppsAutoSkipEffect.tsx | 35 -- .../modules/onboarding/hooks/useInviteTeam.ts | 18 - .../hooks/useOnboardingContentWidth.ts | 13 + .../hooks/useOnboardingMotionTransition.ts | 11 + .../states/onboardingActivationFailedState.ts | 6 + .../settings/billing/components/TrialCard.tsx | 38 -- .../billing/hooks/useStripeAppearance.ts | 33 +- .../useSubmitSubscriptionPayment.ts} | 97 +---- .../BackgroundMockNavigationDrawer.tsx | 64 --- .../BackgroundMockNavigationItems.ts | 38 -- .../constants/BackgroundMockOtherItems.ts | 8 - .../src/modules/types/PageFocusId.ts | 1 - .../hooks/__tests__/useShowAuthModal.test.tsx | 65 --- .../ui/layout/hooks/useShowAuthModal.ts | 31 -- .../layout/page/components/AuthFlowLayout.tsx | 17 + .../layout/page/components/DefaultLayout.tsx | 49 +-- .../users/components/UserContextProvider.tsx | 16 + .../twenty-front/src/pages/auth/SignInUp.tsx | 108 ++--- .../src/pages/auth/SignInUpV2.tsx | 217 ---------- .../auth/__stories__/SignInUpV2.stories.tsx | 85 ---- .../src/pages/onboarding/BookCall.tsx | 49 ++- .../src/pages/onboarding/BookCallDecision.tsx | 17 +- .../src/pages/onboarding/ChooseYourPlan.tsx | 31 +- .../src/pages/onboarding/ChooseYourPlanV2.tsx | 33 -- .../src/pages/onboarding/CreateProfile.tsx | 267 ++++++------ .../src/pages/onboarding/CreateProfileV2.tsx | 313 -------------- .../src/pages/onboarding/ImportContacts.tsx | 158 +++---- .../src/pages/onboarding/InstallApps.tsx | 260 ++---------- .../pages/onboarding/InstallAppsContent.tsx | 208 +++++++++ .../src/pages/onboarding/InstallAppsV2.tsx | 56 --- .../src/pages/onboarding/InviteTeam.tsx | 198 ++++----- .../src/pages/onboarding/InviteTeamV2.tsx | 162 ------- .../src/pages/onboarding/PaymentSuccess.tsx | 137 +++--- .../src/pages/onboarding/SyncEmails.tsx | 226 ++++------ .../src/pages/onboarding/SyncEmailsV2.tsx | 116 ----- .../src/pages/onboarding/UpgradeFreeTrial.tsx | 346 +++++++++------ .../src/pages/onboarding/Verify.tsx | 17 + .../src/pages/onboarding/VerifyV2.tsx | 28 -- .../pages/onboarding/WorkspaceActivation.tsx | 192 ++++----- .../onboarding/WorkspaceActivationV2.tsx | 142 ------- .../__stories__/ChooseYourPlan.stories.tsx | 6 +- .../__stories__/ChooseYourPlanV2.stories.tsx | 50 --- .../__stories__/CreateProfile.stories.tsx | 4 +- .../__stories__/CreateProfileV2.stories.tsx | 49 --- ...V2.stories.tsx => InstallApps.stories.tsx} | 21 +- .../__stories__/InviteTeamV2.stories.tsx | 49 --- .../__stories__/PaymentSuccess.stories.tsx | 50 --- .../__stories__/SyncEmails.stories.tsx | 2 +- .../__stories__/SyncEmailsV2.stories.tsx | 49 --- .../WorkspaceActivation.stories.tsx | 51 --- .../internal/ChooseYourPlanContent.tsx | 183 -------- .../src/testing/constants/UntestedAppPaths.ts | 11 +- .../src/testing/decorators/PageDecorator.tsx | 10 +- .../twenty-front/src/utils/title-utils.ts | 3 - packages/twenty-shared/src/types/AppPath.ts | 9 +- .../data-display/Avatar/Avatar.module.scss | 4 + .../src/input/IconButton/IconButton.tsx | 3 + 158 files changed, 3579 insertions(+), 5374 deletions(-) create mode 100644 packages/twenty-e2e-testing/tests/authentication/onboarding.spec.ts create mode 100644 packages/twenty-front/src/modules/analytics/components/TrackPageViewEffect.tsx delete mode 100644 packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx create mode 100644 packages/twenty-front/src/modules/app/components/DomainShell.tsx create mode 100644 packages/twenty-front/src/modules/app/components/RootApp.tsx create mode 100644 packages/twenty-front/src/modules/app/components/RootAppProviders.tsx create mode 100644 packages/twenty-front/src/modules/app/components/SharedAppProviders.tsx rename packages/twenty-front/src/modules/app/components/{AppRouter.tsx => WorkspaceApp.tsx} (64%) create mode 100644 packages/twenty-front/src/modules/app/components/WorkspaceAppProviders.tsx create mode 100644 packages/twenty-front/src/modules/app/components/__tests__/DomainShell.test.tsx create mode 100644 packages/twenty-front/src/modules/app/effect-components/InitializeQueryParamStateEffect.tsx delete mode 100644 packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx create mode 100644 packages/twenty-front/src/modules/app/hooks/useCreateRootAppRouter.tsx create mode 100644 packages/twenty-front/src/modules/app/hooks/useCreateWorkspaceAppRouter.tsx delete mode 100644 packages/twenty-front/src/modules/auth/components/AuthModal.tsx delete mode 100644 packages/twenty-front/src/modules/auth/components/AuthModalMountEffect.tsx create mode 100644 packages/twenty-front/src/modules/auth/components/VerifyEmail.tsx rename packages/twenty-front/src/modules/auth/components/__stories__/{VerifyEmailEffect.stories.tsx => VerifyEmail.stories.tsx} (68%) rename packages/twenty-front/src/modules/auth/components/__tests__/{VerifyEmailEffect.test.tsx => VerifyEmail.test.tsx} (92%) create mode 100644 packages/twenty-front/src/modules/auth/components/__tests__/VerifyLoginTokenEffect.test.tsx create mode 100644 packages/twenty-front/src/modules/auth/constants/AuthAndOnboardingPaths.ts delete mode 100644 packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts delete mode 100644 packages/twenty-front/src/modules/auth/constants/AuthModalId.ts create mode 100644 packages/twenty-front/src/modules/auth/constants/OnboardingTransitionPaths.ts delete mode 100644 packages/twenty-front/src/modules/auth/constants/OnboardingV2Paths.ts create mode 100644 packages/twenty-front/src/modules/auth/hooks/useIsOnAuthOrOnboardingPage.ts rename packages/twenty-front/src/modules/auth/sign-in-up/components/{SignInUpV2StandardContent.tsx => SignInUpStandardContent.tsx} (68%) delete mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceActivationV2.tsx delete mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceCreationLoader.tsx delete mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/SignInUpWorkspaceActivationV2.stories.tsx delete mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationFormV2.tsx delete mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/components/internal/__tests__/SignInUpWorkspaceCreationFormV2.test.tsx delete mode 100644 packages/twenty-front/src/modules/auth/states/__tests__/isOnboardingV2State.test.ts delete mode 100644 packages/twenty-front/src/modules/auth/states/isOnboardingV2State.ts delete mode 100644 packages/twenty-front/src/modules/auth/utils/getAuthModalConfig.ts create mode 100644 packages/twenty-front/src/modules/auth/utils/isOnOnboardingTransitionPath.ts create mode 100644 packages/twenty-front/src/modules/captcha/components/RequestFreshCaptchaTokenEffect.tsx create mode 100644 packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGate.tsx delete mode 100644 packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGater.tsx create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingActivationOutlet.tsx create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingActivationSteps.tsx rename packages/twenty-front/src/modules/{auth/sign-in-up/components/internal/SignInUpWorkspaceActivationV2Effect.tsx => onboarding/components/OnboardingActivationStepsEffect.tsx} (55%) create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingActivationStepsProgress.tsx create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingAnimatedReveal.tsx rename packages/twenty-front/src/modules/onboarding/components/{OnboardingV2Header.tsx => OnboardingHeader.tsx} (78%) rename packages/twenty-front/src/modules/onboarding/components/{OnboardingV2Layout.tsx => OnboardingLayout.tsx} (65%) create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingSkipButton.tsx create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingStepAnimatedItem.tsx create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingStepLayout.tsx create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingStepPageLoader.tsx delete mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingSyncEmailsSettingsCard.tsx rename packages/twenty-front/src/modules/onboarding/components/{OnboardingV2TransitionOutlet.tsx => OnboardingTransitionOutlet.tsx} (61%) create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingVerifyLayout.tsx create mode 100644 packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepHeading.ts create mode 100644 packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepPage.ts create mode 100644 packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepSubtitle.ts create mode 100644 packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepTagsRow.ts create mode 100644 packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepTitle.ts create mode 100644 packages/twenty-front/src/modules/onboarding/components/__stories__/OnboardingActivationSteps.stories.tsx create mode 100644 packages/twenty-front/src/modules/onboarding/constants/OnboardingActivationMessages.ts create mode 100644 packages/twenty-front/src/modules/onboarding/constants/OnboardingContentBlockWidth.ts create mode 100644 packages/twenty-front/src/modules/onboarding/constants/OnboardingMotionSlideOffset.ts create mode 100644 packages/twenty-front/src/modules/onboarding/constants/OnboardingMotionStaggerDelay.ts delete mode 100644 packages/twenty-front/src/modules/onboarding/constants/OnboardingSyncEmailsOptions.ts create mode 100644 packages/twenty-front/src/modules/onboarding/constants/UpgradeStepContentWidth.ts create mode 100644 packages/twenty-front/src/modules/onboarding/constants/WorkspaceActivationFirstMessageIndex.ts delete mode 100644 packages/twenty-front/src/modules/onboarding/effect-components/InstallAppsAutoSkipEffect.tsx create mode 100644 packages/twenty-front/src/modules/onboarding/hooks/useOnboardingContentWidth.ts create mode 100644 packages/twenty-front/src/modules/onboarding/hooks/useOnboardingMotionTransition.ts create mode 100644 packages/twenty-front/src/modules/onboarding/states/onboardingActivationFailedState.ts delete mode 100644 packages/twenty-front/src/modules/settings/billing/components/TrialCard.tsx rename packages/twenty-front/src/modules/settings/billing/{components/SubscriptionPaymentForm.tsx => hooks/useSubmitSubscriptionPayment.ts} (53%) delete mode 100644 packages/twenty-front/src/modules/sign-in-background-mock/components/BackgroundMockNavigationDrawer.tsx delete mode 100644 packages/twenty-front/src/modules/sign-in-background-mock/constants/BackgroundMockNavigationItems.ts delete mode 100644 packages/twenty-front/src/modules/sign-in-background-mock/constants/BackgroundMockOtherItems.ts delete mode 100644 packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx delete mode 100644 packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts create mode 100644 packages/twenty-front/src/modules/ui/layout/page/components/AuthFlowLayout.tsx create mode 100644 packages/twenty-front/src/modules/users/components/UserContextProvider.tsx delete mode 100644 packages/twenty-front/src/pages/auth/SignInUpV2.tsx delete mode 100644 packages/twenty-front/src/pages/auth/__stories__/SignInUpV2.stories.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/ChooseYourPlanV2.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/CreateProfileV2.tsx create mode 100644 packages/twenty-front/src/pages/onboarding/InstallAppsContent.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/InstallAppsV2.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/InviteTeamV2.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/SyncEmailsV2.tsx create mode 100644 packages/twenty-front/src/pages/onboarding/Verify.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/VerifyV2.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/WorkspaceActivationV2.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/__stories__/ChooseYourPlanV2.stories.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/__stories__/CreateProfileV2.stories.tsx rename packages/twenty-front/src/pages/onboarding/__stories__/{InstallAppsV2.stories.tsx => InstallApps.stories.tsx} (75%) delete mode 100644 packages/twenty-front/src/pages/onboarding/__stories__/InviteTeamV2.stories.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/__stories__/PaymentSuccess.stories.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/__stories__/SyncEmailsV2.stories.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/__stories__/WorkspaceActivation.stories.tsx delete mode 100644 packages/twenty-front/src/pages/onboarding/internal/ChooseYourPlanContent.tsx diff --git a/packages/twenty-e2e-testing/lib/pom/loginPage.ts b/packages/twenty-e2e-testing/lib/pom/loginPage.ts index 1d51a8e14c..04b919fb5c 100644 --- a/packages/twenty-e2e-testing/lib/pom/loginPage.ts +++ b/packages/twenty-e2e-testing/lib/pom/loginPage.ts @@ -16,6 +16,9 @@ export class LoginPage { private readonly uploadImageButton: Locator; private readonly deleteImageButton: Locator; private readonly workspaceNameField: Locator; + private readonly subdomainField: Locator; + private readonly createWorkspaceButton: Locator; + private readonly skipOnboardingStepButton: Locator; private readonly firstNameField: Locator; private readonly lastNameField: Locator; private readonly syncEverythingWithGoogleRadio: Locator; @@ -54,9 +57,17 @@ export class LoginPage { this.previewImageButton = page.locator('.css-1qzw107'); // TODO: fix this.uploadImageButton = page.getByRole('button', { name: 'Upload' }); this.deleteImageButton = page.getByRole('button', { name: 'Remove' }); - this.workspaceNameField = page.getByPlaceholder('Apple'); - this.firstNameField = page.getByPlaceholder('Tim'); - this.lastNameField = page.getByPlaceholder('Cook'); + this.workspaceNameField = page.getByLabel('Name', { exact: true }); + this.subdomainField = page.getByLabel('Subdomain', { exact: true }); + this.createWorkspaceButton = page.getByRole('button', { + name: 'Create workspace', + }); + this.skipOnboardingStepButton = page.getByRole('button', { + name: 'Skip', + exact: true, + }); + this.firstNameField = page.getByLabel('First Name', { exact: true }); + this.lastNameField = page.getByLabel('Last name', { exact: true }); this.syncEverythingWithGoogleRadio = page.locator( 'input[value="SHARE_EVERYTHING"]', ); @@ -145,6 +156,18 @@ export class LoginPage { await this.workspaceNameField.fill(workspaceName); } + async typeSubdomain(subdomain: string) { + await this.subdomainField.fill(subdomain); + } + + async clickCreateWorkspaceButton() { + await this.createWorkspaceButton.click(); + } + + async clickSkipOnboardingStep() { + await this.skipOnboardingStepButton.click(); + } + async typeFirstName(firstName: string) { await this.firstNameField.fill(firstName); } diff --git a/packages/twenty-e2e-testing/tests/authentication/onboarding.spec.ts b/packages/twenty-e2e-testing/tests/authentication/onboarding.spec.ts new file mode 100644 index 0000000000..d61d175258 --- /dev/null +++ b/packages/twenty-e2e-testing/tests/authentication/onboarding.spec.ts @@ -0,0 +1,70 @@ +import { randomUUID } from 'crypto'; +import { expect, test } from './fixture'; + +test.use({ storageState: { cookies: [], origins: [] } }); + +test('New workspace signup goes through every onboarding stage', async ({ + page, + loginPage, +}) => { + test.setTimeout(180000); + + const suffix = randomUUID().replaceAll('-', '').slice(0, 12); + const email = `test${suffix}@apple.dev`; + const workspaceName = `Test ${suffix}`; + const subdomain = `test-${suffix}`; + + await test.step('Create a new account', async () => { + await page.goto('/welcome'); + await loginPage.clickLoginWithEmailIfVisible(); + await loginPage.typeEmail(email); + await loginPage.clickContinueButton(); + await loginPage.typePassword(process.env.DEFAULT_PASSWORD); + await loginPage.clickSignUpButton(); + }); + + await test.step('Create the workspace', async () => { + await expect(page.getByText('Create your workspace')).toBeVisible(); + await loginPage.typeWorkspaceName(workspaceName); + await loginPage.typeSubdomain(subdomain); + await loginPage.clickCreateWorkspaceButton(); + }); + + await test.step('Workspace activation stage', async () => { + await page.waitForURL('**/workspace-activation', { timeout: 90000 }); + }); + + await test.step('Sync-email stage (skip when shown)', async () => { + const syncEmailsHeading = page.getByText('Import your contacts'); + const installAppsHeading = page.getByText('Install your first apps'); + + await expect(syncEmailsHeading.or(installAppsHeading)).toBeVisible({ + timeout: 90000, + }); + + if (await syncEmailsHeading.isVisible()) { + await loginPage.clickSkipOnboardingStep(); + } + }); + + await test.step('Install-apps stage', async () => { + await expect(page.getByText('Install your first apps')).toBeVisible(); + await loginPage.clickSkipOnboardingStep(); + }); + + await test.step('Create-profile stage', async () => { + await expect(page.getByText('Create profile')).toBeVisible(); + await loginPage.typeFirstName('Ada'); + await loginPage.typeLastName('Lovelace'); + await loginPage.clickContinueButton(); + }); + + await test.step('Invite-team stage', async () => { + await expect(page.getByText('Invite your team')).toBeVisible(); + await loginPage.clickSkipOnboardingStep(); + }); + + await test.step('Lands in the app', async () => { + await expect(page.getByTestId('workspace-dropdown')).toBeVisible(); + }); +}); diff --git a/packages/twenty-e2e-testing/tests/authentication/signup_invite_email.spec.ts b/packages/twenty-e2e-testing/tests/authentication/signup_invite_email.spec.ts index 75e1790b96..bec2aae476 100644 --- a/packages/twenty-e2e-testing/tests/authentication/signup_invite_email.spec.ts +++ b/packages/twenty-e2e-testing/tests/authentication/signup_invite_email.spec.ts @@ -39,6 +39,8 @@ test('Sign up with invite link via email', async ({ await loginPage.clickContinueButton(); await loginPage.typePassword(process.env.DEFAULT_PASSWORD); await loginPage.clickSignUpButton(); + await expect(page.getByText('Create profile')).toBeVisible(); + await expect(page.getByPlaceholder('Head of Partnerships')).toBeVisible(); await loginPage.typeFirstName(firstName); await loginPage.typeLastName(lastName); await loginPage.clickContinueButton(); diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts index 23f2802e55..56e5944435 100644 --- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts +++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts @@ -85,16 +85,18 @@ const setupMockState = ( calendarBookingPageId?: string | null, returnToPath?: string, currentWorkspace: object | null = { id: 'mock-workspace-id' }, - isOnboardingV2 = false, + isBillingEnabled: boolean = true, + isMinimalMetadataReady: boolean = true, ) => { jest .mocked(useAtomStateValue) .mockReturnValueOnce(currentWorkspace) .mockReturnValueOnce(calendarBookingPageId ?? 'mock-calendar-id') + .mockReturnValueOnce({ isBillingEnabled }) .mockReturnValueOnce([{ namePlural: objectNamePlural ?? '' }]) + .mockReturnValueOnce(isMinimalMetadataReady) .mockReturnValueOnce(verifyEmailRedirectPath) - .mockReturnValueOnce(returnToPath ?? '') - .mockReturnValueOnce(isOnboardingV2); + .mockReturnValueOnce(returnToPath ?? ''); }; // prettier-ignore @@ -111,6 +113,8 @@ const testCases: { returnToPath?: string; pageLayoutId?: string; useQueryResult?: { data?: unknown; loading?: boolean }; + isBillingEnabled?: boolean; + isMinimalMetadataReady?: boolean; }[] = [ { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, @@ -118,7 +122,7 @@ const testCases: { { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, @@ -129,29 +133,18 @@ const testCases: { { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, - { loc: AppPath.SignInUpV2, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, - { loc: AppPath.SignInUpV2, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, - { loc: AppPath.SignInUpV2, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.SignInUpV2, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, - { loc: AppPath.SignInUpV2, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, - { loc: AppPath.SignInUpV2, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.SignInUpV2, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, - { loc: AppPath.SignInUpV2, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, - { loc: AppPath.SignInUpV2, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, - { loc: AppPath.SignInUpV2, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, - { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: '/plan-required' }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.Invite, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/workspace-activation' }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: '/create/profile' }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: '/sync/emails' }, - { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: '/invite-team' }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, @@ -162,7 +155,7 @@ const testCases: { { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/workspace-activation' }, { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: '/create/profile' }, { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: '/sync/emails' }, - { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: '/invite-team' }, { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, @@ -175,7 +168,7 @@ const testCases: { { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, @@ -186,7 +179,7 @@ const testCases: { { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined }, { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, @@ -197,7 +190,7 @@ const testCases: { { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: undefined }, { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, @@ -208,21 +201,32 @@ const testCases: { { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: undefined }, - { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, + { loc: AppPath.InstallApps, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, + { loc: AppPath.InstallApps, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, + { loc: AppPath.InstallApps, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.InstallApps, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, + { loc: AppPath.InstallApps, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, + { loc: AppPath.InstallApps, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, + { loc: AppPath.InstallApps, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: undefined }, + { loc: AppPath.InstallApps, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, + { loc: AppPath.InstallApps, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, + { loc: AppPath.InstallApps, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, + { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.InviteTeam, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: undefined }, - { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, - { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, + { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.PlanRequired }, + { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: AppPath.PlanRequired }, { loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined }, { loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, @@ -230,7 +234,7 @@ const testCases: { { loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined }, { loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: undefined }, { loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, @@ -241,7 +245,7 @@ const testCases: { { loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined }, { loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: undefined }, { loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, @@ -252,10 +256,10 @@ const testCases: { { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, - { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, - { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, + { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: undefined }, + { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, @@ -263,7 +267,7 @@ const testCases: { { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, @@ -274,7 +278,7 @@ const testCases: { { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, @@ -285,7 +289,7 @@ const testCases: { { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, @@ -296,7 +300,7 @@ const testCases: { { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, @@ -307,12 +311,13 @@ const testCases: { { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined, objectNamePluralFromParams: 'existing-object', objectNamePluralFromMetadata: 'existing-object' }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: AppPath.NotFound, objectNamePluralFromParams: 'non-existing-object', objectNamePluralFromMetadata: 'existing-object' }, + { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined, objectNamePluralFromParams: 'non-existing-object', objectNamePluralFromMetadata: 'existing-object', isMinimalMetadataReady: false }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, @@ -320,7 +325,7 @@ const testCases: { { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, @@ -331,7 +336,7 @@ const testCases: { { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, @@ -346,7 +351,7 @@ const testCases: { { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, @@ -357,7 +362,7 @@ const testCases: { { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, @@ -368,7 +373,7 @@ const testCases: { { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, @@ -379,7 +384,7 @@ const testCases: { { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, @@ -390,11 +395,17 @@ const testCases: { { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallAppsV2 }, + { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.APPS_INSTALLATION, res: AppPath.InstallApps }, { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, + // isBillingEnabled:false — no post-invite-team upgrade interception on billing-disabled instances + { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, isBillingEnabled: false, res: defaultHomePagePath }, + { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, isBillingEnabled: false, res: AppPath.BookCallDecision }, + { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, isBillingEnabled: false, res: defaultHomePagePath }, + { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, isBillingEnabled: false, res: AppPath.BookCallDecision }, + // returnToPath: should redirect to saved path instead of defaultHomePagePath { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, returnToPath: '/authorize?clientId=abc', res: '/authorize?clientId=abc' }, { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, returnToPath: '/objects/tasks', res: '/objects/tasks' }, @@ -420,6 +431,8 @@ describe('usePageChangeEffectNavigateLocation', () => { returnToPath, pageLayoutId, useQueryResult, + isBillingEnabled, + isMinimalMetadataReady, res, }) => { setupMockIsMatchingLocation(loc); @@ -434,6 +447,9 @@ describe('usePageChangeEffectNavigateLocation', () => { verifyEmailRedirectPath, undefined, returnToPath, + undefined, + isBillingEnabled ?? true, + isMinimalMetadataReady ?? true, ); expect(usePageChangeEffectNavigateLocation()).toEqual(res); @@ -448,6 +464,7 @@ describe('usePageChangeEffectNavigateLocation', () => { ['isWorkspaceSuspended:true', 'isWorkspaceSuspended:false'] .length) + ['nonExistingObjectInParam', 'existingObjectInParam:false'].length + + ['nonExistingObjectInParam:metadataNotReady'].length + ['caseWithRedirectionToVerifyEmailRedirectPath', 'caseWithout'] .length + [ @@ -458,7 +475,13 @@ describe('usePageChangeEffectNavigateLocation', () => { ].length + ['returnToPath:verify', 'returnToPath:signInUp', 'returnToPath:index'] .length + - ['notOnWorkspace:verify', 'notOnWorkspace:signInUp'].length, + ['notOnWorkspace:verify', 'notOnWorkspace:signInUp'].length + + [ + 'billingDisabled:inviteTeamCompleted', + 'billingDisabled:inviteTeamBookOnboarding', + 'billingDisabled:planRequiredCompleted', + 'billingDisabled:planRequiredBookOnboarding', + ].length, ); }); }); @@ -481,152 +504,3 @@ describe('usePageChangeEffectNavigateLocation — authenticated with no current }, ); }); - -describe('usePageChangeEffectNavigateLocation — onboarding V2', () => { - const setupOnboardingV2Case = ( - loc: AppPath, - onboardingStatus: OnboardingStatus, - ) => { - setupMockIsMatchingLocation(loc); - setupMockOnboardingStatus(onboardingStatus); - 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', () => { - setupOnboardingV2Case( - AppPath.SignInUpV2, - OnboardingStatus.WORKSPACE_ACTIVATION, - ); - - expect(usePageChangeEffectNavigateLocation()).toEqual( - AppPath.WorkspaceActivationV2, - ); - }); - - it('does not redirect away from the WorkspaceActivationV2 page during activation', () => { - setupOnboardingV2Case( - AppPath.WorkspaceActivationV2, - OnboardingStatus.WORKSPACE_ACTIVATION, - ); - - expect(usePageChangeEffectNavigateLocation()).toBeUndefined(); - }); - - it('routes to SyncEmailsV2 when onboardingV2 is active and status is SYNC_EMAIL', () => { - setupOnboardingV2Case(AppPath.Index, OnboardingStatus.SYNC_EMAIL); - - expect(usePageChangeEffectNavigateLocation()).toEqual(AppPath.SyncEmailsV2); - }); - - it('does not redirect away from the SyncEmailsV2 page', () => { - setupOnboardingV2Case(AppPath.SyncEmailsV2, OnboardingStatus.SYNC_EMAIL); - - expect(usePageChangeEffectNavigateLocation()).toBeUndefined(); - }); - - it('routes to CreateProfileV2 when onboardingV2 is active and status is PROFILE_CREATION', () => { - setupOnboardingV2Case(AppPath.Index, OnboardingStatus.PROFILE_CREATION); - - expect(usePageChangeEffectNavigateLocation()).toEqual( - AppPath.CreateProfileV2, - ); - }); - - it('routes to InstallAppsV2 when status is APPS_INSTALLATION', () => { - setupOnboardingV2Case(AppPath.Index, OnboardingStatus.APPS_INSTALLATION); - - expect(usePageChangeEffectNavigateLocation()).toEqual( - AppPath.InstallAppsV2, - ); - }); - - it('does not redirect away from the InstallAppsV2 page', () => { - setupOnboardingV2Case( - AppPath.InstallAppsV2, - OnboardingStatus.APPS_INSTALLATION, - ); - - expect(usePageChangeEffectNavigateLocation()).toBeUndefined(); - }); - - it('does not redirect away from the CreateProfileV2 page', () => { - setupOnboardingV2Case( - AppPath.CreateProfileV2, - OnboardingStatus.PROFILE_CREATION, - ); - - expect(usePageChangeEffectNavigateLocation()).toBeUndefined(); - }); - - it('routes to InviteTeamV2 when onboardingV2 is active and status is INVITE_TEAM', () => { - setupOnboardingV2Case(AppPath.Index, OnboardingStatus.INVITE_TEAM); - - expect(usePageChangeEffectNavigateLocation()).toEqual(AppPath.InviteTeamV2); - }); - - it('does not redirect away from the InviteTeamV2 page', () => { - setupOnboardingV2Case(AppPath.InviteTeamV2, OnboardingStatus.INVITE_TEAM); - - expect(usePageChangeEffectNavigateLocation()).toBeUndefined(); - }); - - it('routes to PlanRequiredV2 from InviteTeamV2 when onboardingV2 is active and onboarding is completed', () => { - setupOnboardingV2Case(AppPath.InviteTeamV2, OnboardingStatus.COMPLETED); - - expect(usePageChangeEffectNavigateLocation()).toEqual( - AppPath.PlanRequiredV2, - ); - }); - - it('routes to PlanRequiredV2 from InviteTeamV2 when onboardingV2 is active and status is BOOK_ONBOARDING', () => { - setupOnboardingV2Case( - AppPath.InviteTeamV2, - OnboardingStatus.BOOK_ONBOARDING, - ); - - expect(usePageChangeEffectNavigateLocation()).toEqual( - AppPath.PlanRequiredV2, - ); - }); - - it('does not redirect away from the PlanRequiredV2 page when onboarding is completed', () => { - setupOnboardingV2Case(AppPath.PlanRequiredV2, OnboardingStatus.COMPLETED); - - expect(usePageChangeEffectNavigateLocation()).toBeUndefined(); - }); - - it('lets completed v2 users route normally away from the invite transition', () => { - setupOnboardingV2Case(AppPath.Index, OnboardingStatus.COMPLETED); - - expect(usePageChangeEffectNavigateLocation()).toEqual(defaultHomePagePath); - }); - - it('routes to PlanRequiredV2 (never the v1 plan page) when onboardingV2 is active and status is PLAN_REQUIRED', () => { - setupOnboardingV2Case(AppPath.Index, OnboardingStatus.PLAN_REQUIRED); - - expect(usePageChangeEffectNavigateLocation()).toEqual( - AppPath.PlanRequiredV2, - ); - }); - - it('does not redirect away from the PlanRequiredV2 page when a plan is required', () => { - setupOnboardingV2Case( - AppPath.PlanRequiredV2, - OnboardingStatus.PLAN_REQUIRED, - ); - - expect(usePageChangeEffectNavigateLocation()).toBeUndefined(); - }); -}); diff --git a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts index 8d848684aa..94881e1243 100644 --- a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts +++ b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts @@ -3,10 +3,11 @@ 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 { billingState } from '@/client-config/states/billingState'; import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState'; import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; +import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState'; import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; @@ -43,6 +44,8 @@ export const usePageChangeEffectNavigateLocation = () => { const { defaultHomePagePath } = useDefaultHomePagePath(); const location = useLocation(); const calendarBookingPageId = useAtomStateValue(calendarBookingPageIdState); + const billing = useAtomStateValue(billingState); + const isBillingEnabled = billing?.isBillingEnabled ?? false; const someMatchingLocationOf = (appPaths: AppPath[]): boolean => appPaths.some((appPath) => isMatchingLocation(location, appPath)); @@ -54,6 +57,7 @@ export const usePageChangeEffectNavigateLocation = () => { const objectMetadataItem = objectMetadataItems?.find( (objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural, ); + const isMinimalMetadataReady = useAtomStateValue(isMinimalMetadataReadyState); const pageLayoutId = params.pageLayoutId; const isOnPageLayoutPage = isMatchingLocation( @@ -77,8 +81,6 @@ export const usePageChangeEffectNavigateLocation = () => { ? returnToPath : readReturnToPathFromUrlSearchParams(); - const isOnboardingV2 = useAtomStateValue(isOnboardingV2State); - if ( (!hasAccessTokenPair || !isOnAWorkspace || !isDefined(currentWorkspace)) && !someMatchingLocationOf([ @@ -93,7 +95,6 @@ export const usePageChangeEffectNavigateLocation = () => { onboardingStatus === OnboardingStatus.PLAN_REQUIRED && !someMatchingLocationOf([ AppPath.PlanRequired, - AppPath.PlanRequiredV2, AppPath.PlanRequiredSuccess, AppPath.BookCall, AppPath.BookCallDecision, @@ -105,7 +106,7 @@ export const usePageChangeEffectNavigateLocation = () => { ) { return verifyEmailRedirectPath; } - return isOnboardingV2 ? AppPath.PlanRequiredV2 : AppPath.PlanRequired; + return AppPath.PlanRequired; } if (isWorkspaceSuspended) { @@ -122,53 +123,50 @@ export const usePageChangeEffectNavigateLocation = () => { onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION && !someMatchingLocationOf([ AppPath.WorkspaceActivation, - AppPath.WorkspaceActivationV2, AppPath.BookCallDecision, AppPath.BookCall, ]) ) { - return isOnboardingV2 - ? AppPath.WorkspaceActivationV2 - : AppPath.WorkspaceActivation; + return AppPath.WorkspaceActivation; } if ( onboardingStatus === OnboardingStatus.PROFILE_CREATION && - !someMatchingLocationOf([AppPath.CreateProfile, AppPath.CreateProfileV2]) + !isMatchingLocation(location, AppPath.CreateProfile) ) { - return isOnboardingV2 ? AppPath.CreateProfileV2 : AppPath.CreateProfile; + return AppPath.CreateProfile; } if ( onboardingStatus === OnboardingStatus.SYNC_EMAIL && - !someMatchingLocationOf([AppPath.SyncEmails, AppPath.SyncEmailsV2]) + !isMatchingLocation(location, AppPath.SyncEmails) ) { - return isOnboardingV2 ? AppPath.SyncEmailsV2 : AppPath.SyncEmails; + return AppPath.SyncEmails; } if ( onboardingStatus === OnboardingStatus.APPS_INSTALLATION && - !isMatchingLocation(location, AppPath.InstallAppsV2) + !isMatchingLocation(location, AppPath.InstallApps) ) { - return AppPath.InstallAppsV2; + return AppPath.InstallApps; } if ( onboardingStatus === OnboardingStatus.INVITE_TEAM && - !someMatchingLocationOf([AppPath.InviteTeam, AppPath.InviteTeamV2]) + !isMatchingLocation(location, AppPath.InviteTeam) ) { - return isOnboardingV2 ? AppPath.InviteTeamV2 : AppPath.InviteTeam; + return AppPath.InviteTeam; } if ( - isOnboardingV2 && + isBillingEnabled && (onboardingStatus === OnboardingStatus.BOOK_ONBOARDING || onboardingStatus === OnboardingStatus.COMPLETED) ) { - if (isMatchingLocation(location, AppPath.InviteTeamV2)) { - return AppPath.PlanRequiredV2; + if (isMatchingLocation(location, AppPath.InviteTeam)) { + return AppPath.PlanRequired; } - if (isMatchingLocation(location, AppPath.PlanRequiredV2)) { + if (isMatchingLocation(location, AppPath.PlanRequired)) { return; } } @@ -201,6 +199,7 @@ export const usePageChangeEffectNavigateLocation = () => { } if ( + isMinimalMetadataReady && isMatchingLocation(location, AppPath.RecordIndexPage) && !isDefined(objectMetadataItem) ) { diff --git a/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx b/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx index 6b20e83a68..af49c98268 100644 --- a/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx +++ b/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx @@ -1,10 +1,7 @@ import { styled } from '@linaria/react'; -import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal'; -import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices'; import { NAVIGATION_DRAWER_CONSTRAINTS } from '@/ui/layout/resizable-panel/constants/NavigationDrawerConstraints'; import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants'; -import { ModalBackdrop } from 'twenty-ui/surfaces'; import { LeftPanelSkeletonLoader } from '~/loading/components/LeftPanelSkeletonLoader'; import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader'; @@ -28,16 +25,8 @@ const StyledLeftPanelWrapper = styled.div` `; export const UserOrMetadataLoader = () => { - const showAuthModal = useShowAuthModal(); - return ( - {showAuthModal && ( - - )} diff --git a/packages/twenty-front/src/modules/analytics/components/TrackPageViewEffect.tsx b/packages/twenty-front/src/modules/analytics/components/TrackPageViewEffect.tsx new file mode 100644 index 0000000000..d5330a5d9d --- /dev/null +++ b/packages/twenty-front/src/modules/analytics/components/TrackPageViewEffect.tsx @@ -0,0 +1,46 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { + setSessionId, + useEventTracker, +} from '@/analytics/hooks/useEventTracker'; +import { AnalyticsType } from '~/generated-metadata/graphql'; +import { getPageTitleFromPath } from '~/utils/title-utils'; + +const PAGEVIEW_TRACKING_DELAY_IN_MS = 500; + +const stripQueryAndHash = (url: string): string => { + try { + const { origin, pathname } = new URL(url); + return `${origin}${pathname}`; + } catch { + return ''; + } +}; + +export const TrackPageViewEffect = () => { + const location = useLocation(); + const eventTracker = useEventTracker(); + + useEffect(() => { + const timeoutId = setTimeout(() => { + setSessionId(); + eventTracker(AnalyticsType['PAGEVIEW'], { + name: getPageTitleFromPath(location.pathname), + properties: { + pathname: location.pathname, + locale: navigator.language, + userAgent: window.navigator.userAgent, + href: stripQueryAndHash(window.location.href), + referrer: stripQueryAndHash(document.referrer), + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + }); + }, PAGEVIEW_TRACKING_DELAY_IN_MS); + + return () => clearTimeout(timeoutId); + }, [eventTracker, location.pathname]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts b/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts index d52bf45d39..088f1362c7 100644 --- a/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts +++ b/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts @@ -76,7 +76,6 @@ export const useApolloFactory = (options: Partial = {}) => { setCurrentUserWorkspace(null); if ( !isMatchingLocation(locationRef.current, AppPath.Verify) && - !isMatchingLocation(locationRef.current, AppPath.VerifyV2) && !isMatchingLocation(locationRef.current, AppPath.SignInUp) && !isMatchingLocation(locationRef.current, AppPath.Invite) && !isMatchingLocation(locationRef.current, AppPath.ResetPassword) diff --git a/packages/twenty-front/src/modules/app/components/App.tsx b/packages/twenty-front/src/modules/app/components/App.tsx index bd42bb81b5..20fe0ee885 100644 --- a/packages/twenty-front/src/modules/app/components/App.tsx +++ b/packages/twenty-front/src/modules/app/components/App.tsx @@ -1,4 +1,4 @@ -import { AppRouter } from '@/app/components/AppRouter'; +import { DomainShell } from '@/app/components/DomainShell'; import { I18nActivationGate } from '@/app/components/I18nActivationGate'; import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; @@ -35,7 +35,7 @@ export const App = () => { - + diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx deleted file mode 100644 index df4b5e8be1..0000000000 --- a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { AgentChatProvider } from '@/ai/components/AgentChatProvider'; -import { ApolloProvider } from '@/apollo/components/ApolloProvider'; -import { CommandMenuConfirmationModalManager } from '@/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager'; -import { MinimalMetadataGater } from '@/metadata-store/components/MinimalMetadataGater'; -import { IsMinimalMetadataReadyEffect } from '@/metadata-store/effect-components/IsMinimalMetadataReadyEffect'; - -import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider'; -import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect'; -import { AuthProvider } from '@/auth/components/AuthProvider'; -import { SignOutOnOtherTabSignOutEffect } from '@/auth/effect-components/SignOutOnOtherTabSignOutEffect'; -import { CaptchaProvider } from '@/captcha/components/CaptchaProvider'; -import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; -import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; -import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider'; -import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffect'; -import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; -import { MinimalMetadataLoadEffect } from '@/metadata-store/effect-components/MinimalMetadataLoadEffect'; -import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect'; -import { ApolloCoreProvider } from '@/object-metadata/components/ApolloCoreProvider'; -import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider'; -import { ApolloAdminProvider } from '@/settings/admin-panel/apollo/components/ApolloAdminProvider'; -import { EndTrialAfterPaymentMethodGater } from '@/settings/billing/components/EndTrialAfterPaymentMethodGater'; - -import { CommandRunner } from '@/command-menu-item/engine-command/components/CommandRunner'; -import { SSEProvider } from '@/sse-db-event/components/SSEProvider'; -import { SupportChatEffect } from '@/support/components/SupportChatEffect'; -import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; -import { DialogComponentInstanceContext } from '@/ui/feedback/dialog-manager/contexts/DialogComponentInstanceContext'; -import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; -import { GlobalFilePreviewModal } from '@/ui/field/display/components/GlobalFilePreviewModal'; -import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider'; -import { UserThemeProviderEffect } from '@/ui/theme/components/UserThemeProviderEffect'; -import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon'; -import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; -import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect'; -import { StrictMode } from 'react'; -import { Outlet, useLocation } from 'react-router-dom'; -import { getPageTitleFromPath } from '~/utils/title-utils'; - -export const AppRouterProviders = () => { - const { pathname } = useLocation(); - const pageTitle = getPageTitleFromPath(pathname); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/app/components/DomainShell.tsx b/packages/twenty-front/src/modules/app/components/DomainShell.tsx new file mode 100644 index 0000000000..55ab0dd143 --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/DomainShell.tsx @@ -0,0 +1,40 @@ +import { BrowserRouter } from 'react-router-dom'; + +import { RootApp } from '@/app/components/RootApp'; +import { SharedAppProviders } from '@/app/components/SharedAppProviders'; +import { WorkspaceApp } from '@/app/components/WorkspaceApp'; +import { isOnOnboardingTransitionPath } from '@/auth/utils/isOnOnboardingTransitionPath'; +import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; +import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; +import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain'; +import { OnboardingPageLoader } from '@/onboarding/components/OnboardingPageLoader'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader'; + +export const DomainShell = () => { + const { isLoadedOnce } = useAtomStateValue(clientConfigApiStatusState); + const isMultiWorkspaceEnabled = useAtomStateValue( + isMultiWorkspaceEnabledState, + ); + const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain(); + + if (!isLoadedOnce) { + return ( + + + {isOnOnboardingTransitionPath(window.location.pathname) ? ( + + ) : ( + + )} + + + ); + } + + if (!isMultiWorkspaceEnabled) { + return ; + } + + return isDefaultDomain ? : ; +}; diff --git a/packages/twenty-front/src/modules/app/components/I18nActivationGate.tsx b/packages/twenty-front/src/modules/app/components/I18nActivationGate.tsx index c5faf40429..1ed3222dcc 100644 --- a/packages/twenty-front/src/modules/app/components/I18nActivationGate.tsx +++ b/packages/twenty-front/src/modules/app/components/I18nActivationGate.tsx @@ -1,10 +1,8 @@ -import { ONBOARDING_V2_PATHS } from '@/auth/constants/OnboardingV2Paths'; +import { isOnOnboardingTransitionPath } from '@/auth/utils/isOnOnboardingTransitionPath'; 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; @@ -29,11 +27,9 @@ export const I18nActivationGate = ({ children }: I18nActivationGateProps) => { }, []); if (!isLocaleActivated) { - const isOnboardingLocation = ONBOARDING_V2_PATHS.some((onboardingPath) => - isDefined(matchPath(onboardingPath, window.location.pathname)), - ); - - return isOnboardingLocation ? : null; + return isOnOnboardingTransitionPath(window.location.pathname) ? ( + + ) : null; } return <>{children}; diff --git a/packages/twenty-front/src/modules/app/components/RootApp.tsx b/packages/twenty-front/src/modules/app/components/RootApp.tsx new file mode 100644 index 0000000000..a687dcca0e --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/RootApp.tsx @@ -0,0 +1,7 @@ +import { RouterProvider } from 'react-router-dom'; + +import { useCreateRootAppRouter } from '@/app/hooks/useCreateRootAppRouter'; + +export const RootApp = () => { + return ; +}; diff --git a/packages/twenty-front/src/modules/app/components/RootAppProviders.tsx b/packages/twenty-front/src/modules/app/components/RootAppProviders.tsx new file mode 100644 index 0000000000..bde3d25c45 --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/RootAppProviders.tsx @@ -0,0 +1,55 @@ +import { StrictMode } from 'react'; +import { Outlet, useLocation } from 'react-router-dom'; + +import { TrackPageViewEffect } from '@/analytics/components/TrackPageViewEffect'; +import { SharedAppProviders } from '@/app/components/SharedAppProviders'; +import { InitializeQueryParamStateEffect } from '@/app/effect-components/InitializeQueryParamStateEffect'; +import { AuthProvider } from '@/auth/components/AuthProvider'; +import { SignOutOnOtherTabSignOutEffect } from '@/auth/effect-components/SignOutOnOtherTabSignOutEffect'; +import { CaptchaProvider } from '@/captcha/components/CaptchaProvider'; +import { RequestFreshCaptchaTokenEffect } from '@/captcha/components/RequestFreshCaptchaTokenEffect'; +import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffect'; +import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; +import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect'; +import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; +import { DialogComponentInstanceContext } from '@/ui/feedback/dialog-manager/contexts/DialogComponentInstanceContext'; +import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; +import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon'; +import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; +import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect'; +import { getPageTitleFromPath } from '~/utils/title-utils'; + +export const RootAppProviders = () => { + const { pathname } = useLocation(); + const pageTitle = getPageTitleFromPath(pathname); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/app/components/SharedAppProviders.tsx b/packages/twenty-front/src/modules/app/components/SharedAppProviders.tsx new file mode 100644 index 0000000000..e1b82528b7 --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/SharedAppProviders.tsx @@ -0,0 +1,19 @@ +import { type PropsWithChildren } from 'react'; + +import { ApolloProvider } from '@/apollo/components/ApolloProvider'; +import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; +import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; +import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider'; + +type SharedAppProvidersProps = PropsWithChildren; + +export const SharedAppProviders = ({ children }: SharedAppProvidersProps) => { + return ( + + + + {children} + + + ); +}; diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/WorkspaceApp.tsx similarity index 64% rename from packages/twenty-front/src/modules/app/components/AppRouter.tsx rename to packages/twenty-front/src/modules/app/components/WorkspaceApp.tsx index a9586102ec..5ab093fbc8 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouter.tsx +++ b/packages/twenty-front/src/modules/app/components/WorkspaceApp.tsx @@ -1,10 +1,10 @@ -import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter'; -import { currentUserState } from '@/auth/states/currentUserState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { RouterProvider } from 'react-router-dom'; -export const AppRouter = () => { - // We want to disable logic function settings but keep the code for now +import { useCreateWorkspaceAppRouter } from '@/app/hooks/useCreateWorkspaceAppRouter'; +import { currentUserState } from '@/auth/states/currentUserState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; + +export const WorkspaceApp = () => { const isFunctionSettingsEnabled = false; const currentUser = useAtomStateValue(currentUserState); @@ -15,7 +15,10 @@ export const AppRouter = () => { return ( ); }; diff --git a/packages/twenty-front/src/modules/app/components/WorkspaceAppProviders.tsx b/packages/twenty-front/src/modules/app/components/WorkspaceAppProviders.tsx new file mode 100644 index 0000000000..ca5b611c2e --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/WorkspaceAppProviders.tsx @@ -0,0 +1,92 @@ +import { StrictMode } from 'react'; +import { Outlet, useLocation } from 'react-router-dom'; + +import { AgentChatProvider } from '@/ai/components/AgentChatProvider'; +import { TrackPageViewEffect } from '@/analytics/components/TrackPageViewEffect'; +import { SharedAppProviders } from '@/app/components/SharedAppProviders'; +import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider'; +import { InitializeQueryParamStateEffect } from '@/app/effect-components/InitializeQueryParamStateEffect'; +import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect'; +import { AuthProvider } from '@/auth/components/AuthProvider'; +import { SignOutOnOtherTabSignOutEffect } from '@/auth/effect-components/SignOutOnOtherTabSignOutEffect'; +import { CaptchaProvider } from '@/captcha/components/CaptchaProvider'; +import { RequestFreshCaptchaTokenEffect } from '@/captcha/components/RequestFreshCaptchaTokenEffect'; +import { CommandMenuConfirmationModalManager } from '@/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager'; +import { CommandRunner } from '@/command-menu-item/engine-command/components/CommandRunner'; +import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider'; +import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffect'; +import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; +import { IsMinimalMetadataReadyEffect } from '@/metadata-store/effect-components/IsMinimalMetadataReadyEffect'; +import { MinimalMetadataLoadEffect } from '@/metadata-store/effect-components/MinimalMetadataLoadEffect'; +import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect'; +import { ApolloCoreProvider } from '@/object-metadata/components/ApolloCoreProvider'; +import { ApolloAdminProvider } from '@/settings/admin-panel/apollo/components/ApolloAdminProvider'; +import { EndTrialAfterPaymentMethodGater } from '@/settings/billing/components/EndTrialAfterPaymentMethodGater'; +import { SSEProvider } from '@/sse-db-event/components/SSEProvider'; +import { SupportChatEffect } from '@/support/components/SupportChatEffect'; +import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; +import { DialogComponentInstanceContext } from '@/ui/feedback/dialog-manager/contexts/DialogComponentInstanceContext'; +import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; +import { GlobalFilePreviewModal } from '@/ui/field/display/components/GlobalFilePreviewModal'; +import { UserThemeProviderEffect } from '@/ui/theme/components/UserThemeProviderEffect'; +import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon'; +import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; +import { UserContextProvider } from '@/users/components/UserContextProvider'; +import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect'; +import { getPageTitleFromPath } from '~/utils/title-utils'; + +export const WorkspaceAppProviders = () => { + const { pathname } = useLocation(); + const pageTitle = getPageTitleFromPath(pathname); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/app/components/__tests__/DomainShell.test.tsx b/packages/twenty-front/src/modules/app/components/__tests__/DomainShell.test.tsx new file mode 100644 index 0000000000..edc352c027 --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/__tests__/DomainShell.test.tsx @@ -0,0 +1,125 @@ +import { render, screen } from '@testing-library/react'; +import { Provider as JotaiProvider } from 'jotai'; + +import { DomainShell } from '@/app/components/DomainShell'; +import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; +import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; +import { + jotaiStore, + resetJotaiStore, +} from '@/ui/utilities/state/jotai/jotaiStore'; + +let isDefaultDomainValue = true; + +jest.mock('@/apollo/components/ApolloProvider', () => ({ + ApolloProvider: ({ children }: React.PropsWithChildren) => <>{children}, +})); + +jest.mock('@/ui/theme/components/BaseThemeProvider', () => ({ + BaseThemeProvider: ({ children }: React.PropsWithChildren) => <>{children}, +})); + +jest.mock('@/client-config/components/ClientConfigProviderEffect', () => ({ + ClientConfigProviderEffect: () => null, +})); + +jest.mock('@/client-config/components/ClientConfigProvider', () => ({ + ClientConfigProvider: ({ children }: React.PropsWithChildren) => ( + <>{children} + ), +})); + +jest.mock('@/app/components/RootApp', () => ({ + RootApp: () =>
ROOT_APP
, +})); + +jest.mock('@/app/components/WorkspaceApp', () => ({ + WorkspaceApp: () =>
WORKSPACE_APP
, +})); + +jest.mock('~/loading/components/UserOrMetadataLoader', () => ({ + UserOrMetadataLoader: () =>
LOADER
, +})); + +jest.mock('@/onboarding/components/OnboardingPageLoader', () => ({ + OnboardingPageLoader: () =>
ONBOARDING_LOADER
, +})); + +jest.mock('@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain', () => ({ + useIsCurrentLocationOnDefaultDomain: () => ({ + isDefaultDomain: isDefaultDomainValue, + }), +})); + +const renderShell = () => + render( + + + , + ); + +const setClientConfigLoaded = (isLoadedOnce: boolean) => { + jotaiStore.set(clientConfigApiStatusState.atom, { + isLoadedOnce, + isLoading: false, + isErrored: false, + isSaved: isLoadedOnce, + }); +}; + +describe('DomainShell', () => { + beforeEach(() => { + resetJotaiStore(); + isDefaultDomainValue = true; + window.history.pushState({}, '', '/'); + }); + + it('shows the loader until the client config has loaded', () => { + setClientConfigLoaded(false); + jotaiStore.set(isMultiWorkspaceEnabledState.atom, true); + + renderShell(); + + expect(screen.getByText('LOADER')).toBeInTheDocument(); + }); + + it('shows the onboarding loader on onboarding paths until the client config has loaded', () => { + setClientConfigLoaded(false); + jotaiStore.set(isMultiWorkspaceEnabledState.atom, true); + window.history.pushState({}, '', '/welcome'); + + renderShell(); + + expect(screen.getByText('ONBOARDING_LOADER')).toBeInTheDocument(); + }); + + it('mounts the workspace app directly in single-workspace mode', () => { + setClientConfigLoaded(true); + jotaiStore.set(isMultiWorkspaceEnabledState.atom, false); + isDefaultDomainValue = true; + + renderShell(); + + expect(screen.getByText('WORKSPACE_APP')).toBeInTheDocument(); + }); + + it('mounts the root app on the default domain in multi-workspace mode', () => { + setClientConfigLoaded(true); + jotaiStore.set(isMultiWorkspaceEnabledState.atom, true); + isDefaultDomainValue = true; + + renderShell(); + + expect(screen.getByText('ROOT_APP')).toBeInTheDocument(); + }); + + it('mounts the workspace app on a workspace domain in multi-workspace mode', () => { + setClientConfigLoaded(true); + jotaiStore.set(isMultiWorkspaceEnabledState.atom, true); + isDefaultDomainValue = false; + + renderShell(); + + expect(screen.getByText('WORKSPACE_APP')).toBeInTheDocument(); + }); +}); diff --git a/packages/twenty-front/src/modules/app/effect-components/InitializeQueryParamStateEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/InitializeQueryParamStateEffect.tsx new file mode 100644 index 0000000000..b309c63dde --- /dev/null +++ b/packages/twenty-front/src/modules/app/effect-components/InitializeQueryParamStateEffect.tsx @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState'; + +export const InitializeQueryParamStateEffect = () => { + const location = useLocation(); + const { initializeQueryParamState } = useInitializeQueryParamState(); + + useEffect(() => { + initializeQueryParamState(); + }, [initializeQueryParamState, location]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index e8a3d548bb..4c565aab1d 100644 --- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -1,15 +1,7 @@ -import { - setSessionId, - useEventTracker, -} from '@/analytics/hooks/useEventTracker'; import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksOnAnyLocationChange'; import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState'; -import { ONBOARDING_PATHS } from '@/auth/constants/OnboardingPaths'; -import { ONGOING_USER_CREATION_PATHS } from '@/auth/constants/OngoingUserCreationPaths'; import { useReturnToPath } from '@/auth/hooks/useReturnToPath'; -import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken'; -import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState'; -import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath'; +import { useIsOnAuthOrOnboardingPage } from '@/auth/hooks/useIsOnAuthOrOnboardingPage'; import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState'; import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState'; @@ -43,18 +35,9 @@ import { } from 'react-router-dom'; import { AppBasePath, AppPath, SidePanelPages } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { AnalyticsType } from '~/generated-metadata/graphql'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; import { getPageLayoutIdForLocation } from '~/modules/app/utils/getPageLayoutIdForLocation'; -import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState'; import { isMatchingLocation } from '~/utils/isMatchingLocation'; -import { getPageTitleFromPath } from '~/utils/title-utils'; - -const AUTH_AND_ONBOARDING_PATHS = [ - ...ONGOING_USER_CREATION_PATHS, - ...ONBOARDING_PATHS, - AppPath.ResetPassword, -]; // TODO: break down into smaller functions and / or hooks // - moved usePageChangeEffectNavigateLocation into dedicated hook @@ -69,10 +52,6 @@ export const PageChangeEffect = () => { const pageChangeEffectNavigateLocation = usePageChangeEffectNavigateLocation(); - const eventTracker = useEventTracker(); - - const { initializeQueryParamState } = useInitializeQueryParamState(); - //TODO: refactor useResetTableRowSelection hook to not throw when the argument `recordTableId` is an empty string // - replace CoreObjectNamePlural.Person const objectNamePlural = @@ -114,9 +93,7 @@ export const PageChangeEffect = () => { const { saveReturnToPath, getReturnToPath, clearReturnToPath } = useReturnToPath(); - const isOnAuthOrOnboardingPage = AUTH_AND_ONBOARDING_PATHS.some((appPath) => - isMatchingLocation(location, appPath), - ); + const isOnAuthOrOnboardingPage = useIsOnAuthOrOnboardingPage(); const closeSidePanelUnlessNotRelevant = useCallback(() => { const currentPage = store.get(sidePanelPageState.atom); @@ -159,8 +136,6 @@ export const PageChangeEffect = () => { }, [location, previousLocation, executeTasksOnAnyLocationChange, store]); useEffect(() => { - initializeQueryParamState(); - if ( isDefined(pageChangeEffectNavigateLocation) && isAppEffectRedirectEnabled @@ -186,7 +161,6 @@ export const PageChangeEffect = () => { }, [ navigate, pageChangeEffectNavigateLocation, - initializeQueryParamState, isAppEffectRedirectEnabled, isOnAuthOrOnboardingPage, saveReturnToPath, @@ -253,8 +227,7 @@ export const PageChangeEffect = () => { } break; } - case isMatchingLocation(location, AppPath.SignInUp): - case isMatchingLocation(location, AppPath.SignInUpV2): { + case isMatchingLocation(location, AppPath.SignInUp): { resetFocusStackToFocusItem({ focusStackItem: { focusId: PageFocusId.SignInUp, @@ -302,8 +275,7 @@ export const PageChangeEffect = () => { }); break; } - case isMatchingLocation(location, AppPath.WorkspaceActivation): - case isMatchingLocation(location, AppPath.WorkspaceActivationV2): { + case isMatchingLocation(location, AppPath.WorkspaceActivation): { resetFocusStackToFocusItem({ focusStackItem: { focusId: PageFocusId.WorkspaceActivation, @@ -335,8 +307,7 @@ export const PageChangeEffect = () => { }); break; } - case isMatchingLocation(location, AppPath.InviteTeam): - case isMatchingLocation(location, AppPath.InviteTeamV2): { + case isMatchingLocation(location, AppPath.InviteTeam): { resetFocusStackToFocusItem({ focusStackItem: { focusId: PageFocusId.InviteTeam, @@ -401,31 +372,5 @@ export const PageChangeEffect = () => { store, ]); - useEffect(() => { - setTimeout(() => { - setSessionId(); - eventTracker(AnalyticsType['PAGEVIEW'], { - name: getPageTitleFromPath(location.pathname), - properties: { - pathname: location.pathname, - locale: navigator.language, - userAgent: window.navigator.userAgent, - href: window.location.href, - referrer: document.referrer, - timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }, - }); - }, 500); - }, [eventTracker, location.pathname]); - - const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken(); - const isCaptchaScriptLoaded = useAtomStateValue(isCaptchaScriptLoadedState); - - useEffect(() => { - if (isCaptchaScriptLoaded && isCaptchaRequiredForPath(location.pathname)) { - requestFreshCaptchaToken(); - } - }, [isCaptchaScriptLoaded, location.pathname, requestFreshCaptchaToken]); - return <>; }; diff --git a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx deleted file mode 100644 index 41a434ae36..0000000000 --- a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx +++ /dev/null @@ -1,401 +0,0 @@ -import { AppRouterProviders } from '@/app/components/AppRouterProviders'; -import { LazyRoute } from '@/app/components/LazyRoute'; -import { SettingsRoutes } from '@/app/components/SettingsRoutes'; -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'; -import { MainAppLayoutWithSidePanel } from '@/ui/layout/page/components/MainAppLayoutWithSidePanel'; -import { AppPath, SettingsPath } from 'twenty-shared/types'; -import { getSettingsPath } from 'twenty-shared/utils'; - -import { lazy } from 'react'; -import { - createBrowserRouter, - createRoutesFromElements, - Navigate, - Route, -} from 'react-router-dom'; - -const RecordIndexPage = lazy(() => - import('~/pages/object-record/RecordIndexPage').then((module) => ({ - default: module.RecordIndexPage, - })), -); - -const RecordShowPage = lazy(() => - import('~/pages/object-record/RecordShowPage').then((module) => ({ - default: module.RecordShowPage, - })), -); - -const SignInUp = lazy(() => - import('~/pages/auth/SignInUp').then((module) => ({ - default: module.SignInUp, - })), -); - -const SignInUpV2 = lazy(() => - import('~/pages/auth/SignInUpV2').then((module) => ({ - default: module.SignInUpV2, - })), -); - -const PasswordReset = lazy(() => - import('~/pages/auth/PasswordReset').then((module) => ({ - default: module.PasswordReset, - })), -); - -const Authorize = lazy(() => - import('~/pages/auth/Authorize').then((module) => ({ - default: module.Authorize, - })), -); - -const WorkspaceActivation = lazy(() => - import('~/pages/onboarding/WorkspaceActivation').then((module) => ({ - default: module.WorkspaceActivation, - })), -); - -const WorkspaceActivationV2 = lazyWithPreload(() => - import('~/pages/onboarding/WorkspaceActivationV2').then((module) => ({ - default: module.WorkspaceActivationV2, - })), -); - -const CreateProfile = lazy(() => - import('~/pages/onboarding/CreateProfile').then((module) => ({ - default: module.CreateProfile, - })), -); - -const CreateProfileV2 = lazyWithPreload(() => - import('~/pages/onboarding/CreateProfileV2').then((module) => ({ - default: module.CreateProfileV2, - })), -); - -const SyncEmails = lazy(() => - import('~/pages/onboarding/SyncEmails').then((module) => ({ - default: module.SyncEmails, - })), -); - -const SyncEmailsV2 = lazyWithPreload(() => - import('~/pages/onboarding/SyncEmailsV2').then((module) => ({ - default: module.SyncEmailsV2, - })), -); - -const InstallAppsV2 = lazyWithPreload(() => - import('~/pages/onboarding/InstallAppsV2').then((module) => ({ - default: module.InstallAppsV2, - })), -); - -const InviteTeam = lazy(() => - import('~/pages/onboarding/InviteTeam').then((module) => ({ - default: module.InviteTeam, - })), -); - -const InviteTeamV2 = lazyWithPreload(() => - import('~/pages/onboarding/InviteTeamV2').then((module) => ({ - default: module.InviteTeamV2, - })), -); - -const ChooseYourPlan = lazy(() => - import('~/pages/onboarding/ChooseYourPlan').then((module) => ({ - default: module.ChooseYourPlan, - })), -); - -const ChooseYourPlanV2 = lazyWithPreload(() => - import('~/pages/onboarding/ChooseYourPlanV2').then((module) => ({ - default: module.ChooseYourPlanV2, - })), -); - -const PaymentSuccess = lazy(() => - import('~/pages/onboarding/PaymentSuccess').then((module) => ({ - default: module.PaymentSuccess, - })), -); - -const BookCallDecision = lazy(() => - import('~/pages/onboarding/BookCallDecision').then((module) => ({ - default: module.BookCallDecision, - })), -); - -const BookCall = lazy(() => - import('~/pages/onboarding/BookCall').then((module) => ({ - default: module.BookCall, - })), -); - -const StandalonePageLayoutPage = lazy(() => - import('~/pages/page-layout/StandalonePageLayoutPage').then((module) => ({ - default: module.StandalonePageLayoutPage, - })), -); - -const NotFound = lazy(() => - import('~/pages/not-found/NotFound').then((module) => ({ - default: module.NotFound, - })), -); - -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, -) => - createBrowserRouter( - createRoutesFromElements( - } - // To switch state to `loading` temporarily to enable us - // to set scroll position before the page is rendered - loader={async () => Promise.resolve(null)} - > - }> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - }> - } /> - }> - - - } - /> - - - - } - /> - - - - } - /> - - } - /> - {/* Deep link for twenty.com/dpa → in-app generator. This route is - inside the authenticated layout, so an unauthenticated hit is - login-gated and returns here after sign-in. */} - - } - /> - - - - } - /> - - - }> - } - loader={preloadOnboardingV2Pages} - > - }> - - - } - /> - } /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - - - - - } - /> - - , - ), - ); diff --git a/packages/twenty-front/src/modules/app/hooks/useCreateRootAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateRootAppRouter.tsx new file mode 100644 index 0000000000..eb57c820c9 --- /dev/null +++ b/packages/twenty-front/src/modules/app/hooks/useCreateRootAppRouter.tsx @@ -0,0 +1,91 @@ +import { lazy, useState } from 'react'; +import { + createBrowserRouter, + createRoutesFromElements, + Navigate, + Route, +} from 'react-router-dom'; +import { AppPath } from 'twenty-shared/types'; + +import { LazyRoute } from '@/app/components/LazyRoute'; +import { RootAppProviders } from '@/app/components/RootAppProviders'; +import { VerifyEmail } from '@/auth/components/VerifyEmail'; +import { OnboardingPageLoader } from '@/onboarding/components/OnboardingPageLoader'; +import { OnboardingTransitionOutlet } from '@/onboarding/components/OnboardingTransitionOutlet'; +import { AuthFlowLayout } from '@/ui/layout/page/components/AuthFlowLayout'; +import { BlankLayout } from '@/ui/layout/page/components/BlankLayout'; + +const SignInUp = lazy(() => + import('~/pages/auth/SignInUp').then((module) => ({ + default: module.SignInUp, + })), +); + +const Authorize = lazy(() => + import('~/pages/auth/Authorize').then((module) => ({ + default: module.Authorize, + })), +); + +const PasswordReset = lazy(() => + import('~/pages/auth/PasswordReset').then((module) => ({ + default: module.PasswordReset, + })), +); + +const createRootAppRouter = () => + createBrowserRouter( + createRoutesFromElements( + } loader={async () => null}> + }> + }> + }> + + + } + /> + }> + + + } + /> + + + }> + } /> + + + + } + /> + + + + + } + /> + } + /> + , + ), + ); + +export const useCreateRootAppRouter = () => { + const [router] = useState(createRootAppRouter); + + return router; +}; diff --git a/packages/twenty-front/src/modules/app/hooks/useCreateWorkspaceAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateWorkspaceAppRouter.tsx new file mode 100644 index 0000000000..703139d353 --- /dev/null +++ b/packages/twenty-front/src/modules/app/hooks/useCreateWorkspaceAppRouter.tsx @@ -0,0 +1,341 @@ +import { lazy, useMemo } from 'react'; +import { + createBrowserRouter, + createRoutesFromElements, + Navigate, + Route, +} from 'react-router-dom'; +import { AppPath, SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath } from 'twenty-shared/utils'; + +import { LazyRoute } from '@/app/components/LazyRoute'; +import { SettingsRoutes } from '@/app/components/SettingsRoutes'; +import { WorkspaceAppProviders } from '@/app/components/WorkspaceAppProviders'; +import { VerifyEmail } from '@/auth/components/VerifyEmail'; +import { MinimalMetadataGate } from '@/metadata-store/components/MinimalMetadataGate'; +import indexAppPath from '@/navigation/utils/indexAppPath'; +import { OnboardingActivationOutlet } from '@/onboarding/components/OnboardingActivationOutlet'; +import { OnboardingPageLoader } from '@/onboarding/components/OnboardingPageLoader'; +import { OnboardingStepLayout } from '@/onboarding/components/OnboardingStepLayout'; +import { OnboardingStepPageLoader } from '@/onboarding/components/OnboardingStepPageLoader'; +import { OnboardingTransitionOutlet } from '@/onboarding/components/OnboardingTransitionOutlet'; +import { RecordIndexSkeletonLoader } from '@/object-record/record-index/components/RecordIndexSkeletonLoader'; +import { AuthFlowLayout } from '@/ui/layout/page/components/AuthFlowLayout'; +import { BlankLayout } from '@/ui/layout/page/components/BlankLayout'; +import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout'; +import { MainAppLayoutWithSidePanel } from '@/ui/layout/page/components/MainAppLayoutWithSidePanel'; +import { Verify } from '~/pages/onboarding/Verify'; +import { lazyWithPreload } from '~/utils/lazyWithPreload'; + +const RecordIndexPage = lazy(() => + import('~/pages/object-record/RecordIndexPage').then((module) => ({ + default: module.RecordIndexPage, + })), +); + +const RecordShowPage = lazy(() => + import('~/pages/object-record/RecordShowPage').then((module) => ({ + default: module.RecordShowPage, + })), +); + +const SignInUp = lazy(() => + import('~/pages/auth/SignInUp').then((module) => ({ + default: module.SignInUp, + })), +); + +const PasswordReset = lazy(() => + import('~/pages/auth/PasswordReset').then((module) => ({ + default: module.PasswordReset, + })), +); + +const Authorize = lazy(() => + import('~/pages/auth/Authorize').then((module) => ({ + default: module.Authorize, + })), +); + +const WorkspaceActivation = lazyWithPreload(() => + import('~/pages/onboarding/WorkspaceActivation').then((module) => ({ + default: module.WorkspaceActivation, + })), +); + +const CreateProfile = lazyWithPreload(() => + import('~/pages/onboarding/CreateProfile').then((module) => ({ + default: module.CreateProfile, + })), +); + +const SyncEmails = lazyWithPreload(() => + import('~/pages/onboarding/SyncEmails').then((module) => ({ + default: module.SyncEmails, + })), +); + +const InstallApps = lazyWithPreload(() => + import('~/pages/onboarding/InstallApps').then((module) => ({ + default: module.InstallApps, + })), +); + +const InviteTeam = lazyWithPreload(() => + import('~/pages/onboarding/InviteTeam').then((module) => ({ + default: module.InviteTeam, + })), +); + +const ChooseYourPlan = lazyWithPreload(() => + import('~/pages/onboarding/ChooseYourPlan').then((module) => ({ + default: module.ChooseYourPlan, + })), +); + +const PaymentSuccess = lazy(() => + import('~/pages/onboarding/PaymentSuccess').then((module) => ({ + default: module.PaymentSuccess, + })), +); + +const BookCallDecision = lazy(() => + import('~/pages/onboarding/BookCallDecision').then((module) => ({ + default: module.BookCallDecision, + })), +); + +const BookCall = lazy(() => + import('~/pages/onboarding/BookCall').then((module) => ({ + default: module.BookCall, + })), +); + +const StandalonePageLayoutPage = lazy(() => + import('~/pages/page-layout/StandalonePageLayoutPage').then((module) => ({ + default: module.StandalonePageLayoutPage, + })), +); + +const NotFound = lazy(() => + import('~/pages/not-found/NotFound').then((module) => ({ + default: module.NotFound, + })), +); + +const preloadOnboardingPages = () => { + void WorkspaceActivation.preload(); + void CreateProfile.preload(); + void SyncEmails.preload(); + void InstallApps.preload(); + void InviteTeam.preload(); + void ChooseYourPlan.preload(); + + return null; +}; + +const createWorkspaceAppRouter = ( + isFunctionSettingsEnabled?: boolean, + isAdminPageEnabled?: boolean, +) => + createBrowserRouter( + createRoutesFromElements( + } + loader={async () => Promise.resolve(null)} + > + }> + }> + }> + } /> + }> + + + } + /> + + + + } + /> + + + + } + /> + + } + /> + + } + /> + + + + } + /> + + + + }> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + }> + } + loader={preloadOnboardingPages} + > + }> + + + } + /> + }> + + + } + /> + + } + loader={preloadOnboardingPages} + > + } /> + + + + } + /> + + } + loader={preloadOnboardingPages} + > + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + + + + + } + /> + + , + ), + ); + +export const useCreateWorkspaceAppRouter = ( + isFunctionSettingsEnabled?: boolean, + isAdminPageEnabled?: boolean, +) => + useMemo( + () => + createWorkspaceAppRouter(isFunctionSettingsEnabled, isAdminPageEnabled), + [isFunctionSettingsEnabled, isAdminPageEnabled], + ); diff --git a/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts b/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts index 33f1faeb15..2bc96a5f5d 100644 --- a/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts +++ b/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState'; -import { isOnboardingV2State } from '@/auth/states/isOnboardingV2State'; import { returnToPathState } from '@/auth/states/returnToPathState'; import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type'; import { isValidReturnToPath } from '@/auth/utils/isValidReturnToPath'; @@ -51,11 +50,6 @@ export const useInitializeQueryParamState = () => { store.set(returnToPathState.atom, value); } }, - onboardingV2: (value: string) => { - if (value === 'true') { - store.set(isOnboardingV2State.atom, true); - } - }, }; const queryParams = new URLSearchParams(window.location.search); diff --git a/packages/twenty-front/src/modules/auth/components/AuthModal.tsx b/packages/twenty-front/src/modules/auth/components/AuthModal.tsx deleted file mode 100644 index 7107efdc97..0000000000 --- a/packages/twenty-front/src/modules/auth/components/AuthModal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { AuthModalMountEffect } from '@/auth/components/AuthModalMountEffect'; -import { AUTH_MODAL_ID } from '@/auth/constants/AuthModalId'; -import { getAuthModalConfig } from '@/auth/utils/getAuthModalConfig'; -import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper'; -import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; -import { styled } from '@linaria/react'; -import React from 'react'; -import { useLocation } from 'react-router-dom'; - -const StyledContent = styled.div` - align-items: center; - display: flex; - flex-direction: column; - justify-content: center; - min-height: 320px; -`; - -type AuthModalProps = { - children: React.ReactNode; -}; - -export const AuthModal = ({ children }: AuthModalProps) => { - const location = useLocation(); - const config = getAuthModalConfig(location); - - return ( - <> - - - {config.showScrollWrapper ? ( - - {children} - - ) : ( - <>{children} - )} - - - ); -}; diff --git a/packages/twenty-front/src/modules/auth/components/AuthModalMountEffect.tsx b/packages/twenty-front/src/modules/auth/components/AuthModalMountEffect.tsx deleted file mode 100644 index 6505698522..0000000000 --- a/packages/twenty-front/src/modules/auth/components/AuthModalMountEffect.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useModal } from '@/ui/layout/modal/hooks/useModal'; -import { useEffect } from 'react'; - -import { AUTH_MODAL_ID } from '@/auth/constants/AuthModalId'; - -// TODO: Remove this component when we refactor the auth modal to open it directly in the PageChangeEffect -export const AuthModalMountEffect = () => { - const { openModal, closeModal } = useModal(); - - useEffect(() => { - openModal(AUTH_MODAL_ID); - - return () => { - closeModal(AUTH_MODAL_ID); - }; - }, [openModal, closeModal]); - - return null; -}; diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEmail.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEmail.tsx new file mode 100644 index 0000000000..86cfb5899f --- /dev/null +++ b/packages/twenty-front/src/modules/auth/components/VerifyEmail.tsx @@ -0,0 +1,33 @@ +import { SubTitle } from '@/auth/components/SubTitle'; +import { VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect'; +import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent'; +import { OnboardingVerifyLayout } from '@/onboarding/components/OnboardingVerifyLayout'; +import { useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { ModalContent } from 'twenty-ui/surfaces'; + +export const VerifyEmail = () => { + const { t } = useLingui(); + const [searchParams] = useSearchParams(); + const [isError, setIsError] = useState(false); + + const email = searchParams.get('email'); + + if (isError) { + return ( + + + + ); + } + + return ( + <> + setIsError(true)} /> + + {t`Verifying your email`} + + + ); +}; diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx index 00260f061a..130448346f 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx @@ -1,26 +1,27 @@ -import { useAuth } from '@/auth/hooks/useAuth'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { CombinedGraphQLErrors } from '@apollo/client/errors'; -import { AppPath } from 'twenty-shared/types'; - import { verifyEmailRedirectPathState } from '@/app/states/verifyEmailRedirectPathState'; +import { useAuth } from '@/auth/hooks/useAuth'; import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin'; import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; -import { ModalContent } from 'twenty-ui/surfaces'; -import { useLingui } from '@lingui/react/macro'; -import { useEffect, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; +import { CombinedGraphQLErrors } from '@apollo/client/errors'; +import { useLingui } from '@lingui/react/macro'; +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { AppPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { useNavigateApp } from '~/hooks/useNavigateApp'; import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; import { isGraphqlErrorOfType } from '~/utils/is-graphql-error-of-type.util'; -import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -export const VerifyEmailEffect = () => { +type VerifyEmailEffectProps = { + onError: () => void; +}; + +export const VerifyEmailEffect = ({ onError }: VerifyEmailEffectProps) => { const { verifyEmailAndGetLoginToken, verifyEmailAndGetWorkspaceAgnosticToken, @@ -29,7 +30,6 @@ export const VerifyEmailEffect = () => { const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); const [searchParams] = useSearchParams(); - const [isError, setIsError] = useState(false); const setVerifyEmailRedirectPath = useSetAtomState( verifyEmailRedirectPathState, @@ -109,7 +109,7 @@ export const VerifyEmailEffect = () => { navigate(AppPath.SignInUp); } - setIsError(true); + onError(); } }; @@ -123,13 +123,5 @@ export const VerifyEmailEffect = () => { // oxlint-disable-next-line react-hooks/exhaustive-deps }, [clientConfigApiStatus.isLoadedOnce]); - if (isError) { - return ( - - - - ); - } - return <>; }; diff --git a/packages/twenty-front/src/modules/auth/components/VerifyLoginTokenEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyLoginTokenEffect.tsx index 1bf270b56f..6bac7ad32f 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyLoginTokenEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyLoginTokenEffect.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair'; @@ -21,17 +21,21 @@ export const VerifyLoginTokenEffect = () => { clientConfigApiStatusState, ); + // oxlint-disable-next-line twenty/no-state-useref + const hasVerifiedRef = useRef(false); + useEffect(() => { - if (!clientConfigLoaded) { + if (!clientConfigLoaded || hasVerifiedRef.current) { return; } + hasVerifiedRef.current = true; + if (isDefined(loginToken)) { verifyLoginToken(loginToken); } else if (!hasAccessTokenPair) { navigate(AppPath.SignInUp); } - // Verify only needs to run once at mount // oxlint-disable-next-line react-hooks/exhaustive-deps }, [clientConfigLoaded]); diff --git a/packages/twenty-front/src/modules/auth/components/__stories__/VerifyEmailEffect.stories.tsx b/packages/twenty-front/src/modules/auth/components/__stories__/VerifyEmail.stories.tsx similarity index 68% rename from packages/twenty-front/src/modules/auth/components/__stories__/VerifyEmailEffect.stories.tsx rename to packages/twenty-front/src/modules/auth/components/__stories__/VerifyEmail.stories.tsx index ce04af70a9..9af93e928b 100644 --- a/packages/twenty-front/src/modules/auth/components/__stories__/VerifyEmailEffect.stories.tsx +++ b/packages/twenty-front/src/modules/auth/components/__stories__/VerifyEmail.stories.tsx @@ -1,14 +1,14 @@ -import { type VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect'; +import { type VerifyEmail } from '@/auth/components/VerifyEmail'; import { type Meta, type StoryObj } from '@storybook/react-vite'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; -// Mock component that just renders the error state of VerifyEmailEffect directly -// (since normal VerifyEmailEffect has async logic that's hard to test in Storybook) +// Mock component that just renders the error state of VerifyEmail directly +// (since normal VerifyEmail has async logic that's hard to test in Storybook) import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent'; import { ModalContent } from 'twenty-ui/surfaces'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; -const VerifyEmailEffectErrorState = ({ email = 'user@example.com' }) => { +const VerifyEmailErrorState = ({ email = 'user@example.com' }) => { return ( @@ -16,9 +16,9 @@ const VerifyEmailEffectErrorState = ({ email = 'user@example.com' }) => { ); }; -const meta: Meta = { - title: 'Modules/Auth/VerifyEmailEffect', - component: VerifyEmailEffectErrorState, +const meta: Meta = { + title: 'Modules/Auth/VerifyEmail', + component: VerifyEmailErrorState, decorators: [ (Story) => (
@@ -29,13 +29,13 @@ const meta: Meta = { ], parameters: { codeSection: { - docs: 'IMPORTANT: When rendering EmailVerificationSent from VerifyEmailEffect, always wrap it with ModalContent to maintain consistent styling.', + docs: 'IMPORTANT: When rendering EmailVerificationSent from VerifyEmail, always wrap it with ModalContent to maintain consistent styling.', }, }, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const ErrorState: Story = { args: { @@ -43,7 +43,7 @@ export const ErrorState: Story = { }, }; -export const IntegratedExample: StoryObj = { +export const IntegratedExample: StoryObj = { render: () => ( = { } + element={} /> diff --git a/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmailEffect.test.tsx b/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmail.test.tsx similarity index 92% rename from packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmailEffect.test.tsx rename to packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmail.test.tsx index 677982459a..e4c2b2ed0a 100644 --- a/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmailEffect.test.tsx +++ b/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmail.test.tsx @@ -7,7 +7,7 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations'; import { AppPath } from 'twenty-shared/types'; import { ThemeProvider } from 'twenty-ui/theme-constants'; -import { VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect'; +import { VerifyEmail } from '@/auth/components/VerifyEmail'; import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; import { jotaiStore, @@ -60,7 +60,7 @@ jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({ }), })); -// Rendered by VerifyEmailEffect in the error state; isolate it from Apollo. +// Rendered by VerifyEmail in the error state; isolate it from Apollo. jest.mock( '@/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken', () => ({ @@ -76,20 +76,20 @@ dynamicActivate(SOURCE_LOCALE); const VERIFY_EMAIL_URL = '/verify-email?email=user%40example.com&emailVerificationToken=valid-token'; -const renderEffect = (initialEntry: string) => +const renderVerifyEmail = (initialEntry: string) => render( - + , ); -describe('VerifyEmailEffect', () => { +describe('VerifyEmail', () => { beforeEach(() => { jest.clearAllMocks(); resetJotaiStore(); @@ -106,7 +106,7 @@ describe('VerifyEmailEffect', () => { it('navigates to the SignInUp page after a successful workspace-agnostic verification on the central domain', async () => { verifyEmailAndGetWorkspaceAgnosticTokenMock.mockResolvedValue(undefined); - renderEffect(VERIFY_EMAIL_URL); + renderVerifyEmail(VERIFY_EMAIL_URL); await waitFor(() => { expect(verifyEmailAndGetWorkspaceAgnosticTokenMock).toHaveBeenCalledWith( @@ -128,7 +128,7 @@ describe('VerifyEmailEffect', () => { new Error('verification failed'), ); - renderEffect(VERIFY_EMAIL_URL); + renderVerifyEmail(VERIFY_EMAIL_URL); await waitFor(() => { expect(enqueueErrorSnackBarMock).toHaveBeenCalled(); @@ -143,7 +143,7 @@ describe('VerifyEmailEffect', () => { workspaceUrls: { subdomainUrl: 'https://foo.twenty.com/' }, }); - renderEffect(VERIFY_EMAIL_URL); + renderVerifyEmail(VERIFY_EMAIL_URL); await waitFor(() => { expect(verifyEmailAndGetLoginTokenMock).toHaveBeenCalledWith( diff --git a/packages/twenty-front/src/modules/auth/components/__tests__/VerifyLoginTokenEffect.test.tsx b/packages/twenty-front/src/modules/auth/components/__tests__/VerifyLoginTokenEffect.test.tsx new file mode 100644 index 0000000000..723c78f69a --- /dev/null +++ b/packages/twenty-front/src/modules/auth/components/__tests__/VerifyLoginTokenEffect.test.tsx @@ -0,0 +1,72 @@ +import { act, render, waitFor } from '@testing-library/react'; +import { Provider as JotaiProvider } from 'jotai'; +import { StrictMode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import { VerifyLoginTokenEffect } from '@/auth/components/VerifyLoginTokenEffect'; +import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; +import { + jotaiStore, + resetJotaiStore, +} from '@/ui/utilities/state/jotai/jotaiStore'; + +const verifyLoginTokenMock = jest.fn(); +const navigateMock = jest.fn(); + +jest.mock('@/auth/hooks/useVerifyLogin', () => ({ + useVerifyLogin: () => ({ verifyLoginToken: verifyLoginTokenMock }), +})); + +jest.mock('~/hooks/useNavigateApp', () => ({ + useNavigateApp: () => navigateMock, +})); + +jest.mock('@/auth/hooks/useHasAccessTokenPair', () => ({ + useHasAccessTokenPair: () => false, +})); + +const setClientConfigSaved = (isSaved: boolean) => { + jotaiStore.set(clientConfigApiStatusState.atom, { + isLoadedOnce: true, + isLoading: false, + isErrored: false, + isSaved, + }); +}; + +const renderEffect = (initialEntry: string) => + render( + + + + + + + , + ); + +describe('VerifyLoginTokenEffect', () => { + beforeEach(() => { + jest.clearAllMocks(); + resetJotaiStore(); + setClientConfigSaved(true); + }); + + it('verifies the login token at most once even when the gating config re-triggers the effect', async () => { + renderEffect('/verify?loginToken=login-token'); + + await waitFor(() => { + expect(verifyLoginTokenMock).toHaveBeenCalledWith('login-token'); + }); + expect(verifyLoginTokenMock).toHaveBeenCalledTimes(1); + + await act(async () => { + setClientConfigSaved(false); + }); + await act(async () => { + setClientConfigSaved(true); + }); + + expect(verifyLoginTokenMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/twenty-front/src/modules/auth/constants/AuthAndOnboardingPaths.ts b/packages/twenty-front/src/modules/auth/constants/AuthAndOnboardingPaths.ts new file mode 100644 index 0000000000..4602abbba1 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/constants/AuthAndOnboardingPaths.ts @@ -0,0 +1,9 @@ +import { ONBOARDING_PATHS } from '@/auth/constants/OnboardingPaths'; +import { ONGOING_USER_CREATION_PATHS } from '@/auth/constants/OngoingUserCreationPaths'; +import { AppPath } from 'twenty-shared/types'; + +export const AUTH_AND_ONBOARDING_PATHS = [ + ...ONGOING_USER_CREATION_PATHS, + ...ONBOARDING_PATHS, + AppPath.ResetPassword, +]; diff --git a/packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts b/packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts deleted file mode 100644 index a0eac56aa2..0000000000 --- a/packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { type ModalOverlay, type ModalSize } from 'twenty-ui/surfaces'; -import { AppPath } from 'twenty-shared/types'; - -type AuthModalConfigType = { - size: ModalSize; - overlay: ModalOverlay; - showScrollWrapper: boolean; -}; - -export const AUTH_MODAL_CONFIG: { - default: AuthModalConfigType; - [key: string]: AuthModalConfigType; -} = { - default: { - size: 'medium', - overlay: 'dark', - showScrollWrapper: true, - }, - [AppPath.BookCall]: { - size: 'extraLarge', - overlay: 'transparent', - showScrollWrapper: false, - }, -}; diff --git a/packages/twenty-front/src/modules/auth/constants/AuthModalId.ts b/packages/twenty-front/src/modules/auth/constants/AuthModalId.ts deleted file mode 100644 index 934f56df24..0000000000 --- a/packages/twenty-front/src/modules/auth/constants/AuthModalId.ts +++ /dev/null @@ -1 +0,0 @@ -export const AUTH_MODAL_ID = 'auth-modal'; diff --git a/packages/twenty-front/src/modules/auth/constants/OnboardingPaths.ts b/packages/twenty-front/src/modules/auth/constants/OnboardingPaths.ts index d7bcf74bf1..7c84b997b9 100644 --- a/packages/twenty-front/src/modules/auth/constants/OnboardingPaths.ts +++ b/packages/twenty-front/src/modules/auth/constants/OnboardingPaths.ts @@ -2,16 +2,11 @@ import { AppPath } from 'twenty-shared/types'; export const ONBOARDING_PATHS = [ AppPath.WorkspaceActivation, - AppPath.WorkspaceActivationV2, AppPath.CreateProfile, - AppPath.CreateProfileV2, AppPath.SyncEmails, - AppPath.SyncEmailsV2, - AppPath.InstallAppsV2, + AppPath.InstallApps, AppPath.InviteTeam, - AppPath.InviteTeamV2, AppPath.PlanRequired, - AppPath.PlanRequiredV2, AppPath.PlanRequiredSuccess, AppPath.BookCallDecision, AppPath.BookCall, diff --git a/packages/twenty-front/src/modules/auth/constants/OnboardingTransitionPaths.ts b/packages/twenty-front/src/modules/auth/constants/OnboardingTransitionPaths.ts new file mode 100644 index 0000000000..5c399c74e1 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/constants/OnboardingTransitionPaths.ts @@ -0,0 +1,15 @@ +import { AppPath } from 'twenty-shared/types'; + +export const ONBOARDING_TRANSITION_PATHS = [ + AppPath.SignInUp, + AppPath.Invite, + AppPath.Verify, + AppPath.VerifyEmail, + AppPath.WorkspaceActivation, + AppPath.CreateProfile, + AppPath.SyncEmails, + AppPath.InstallApps, + AppPath.InviteTeam, + AppPath.PlanRequired, + AppPath.PlanRequiredSuccess, +]; diff --git a/packages/twenty-front/src/modules/auth/constants/OnboardingV2Paths.ts b/packages/twenty-front/src/modules/auth/constants/OnboardingV2Paths.ts deleted file mode 100644 index bb858f0cb8..0000000000 --- a/packages/twenty-front/src/modules/auth/constants/OnboardingV2Paths.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { AppPath } from 'twenty-shared/types'; - -export const ONBOARDING_V2_PATHS = [ - AppPath.SignInUpV2, - AppPath.VerifyV2, - AppPath.WorkspaceActivationV2, - AppPath.CreateProfileV2, - AppPath.SyncEmailsV2, - AppPath.InstallAppsV2, - AppPath.InviteTeamV2, - AppPath.PlanRequiredV2, -]; diff --git a/packages/twenty-front/src/modules/auth/constants/OngoingUserCreationPaths.ts b/packages/twenty-front/src/modules/auth/constants/OngoingUserCreationPaths.ts index da2d31434f..db499b4c50 100644 --- a/packages/twenty-front/src/modules/auth/constants/OngoingUserCreationPaths.ts +++ b/packages/twenty-front/src/modules/auth/constants/OngoingUserCreationPaths.ts @@ -3,8 +3,6 @@ import { AppPath } from 'twenty-shared/types'; export const ONGOING_USER_CREATION_PATHS = [ AppPath.Invite, AppPath.SignInUp, - AppPath.SignInUpV2, AppPath.VerifyEmail, AppPath.Verify, - AppPath.VerifyV2, ]; diff --git a/packages/twenty-front/src/modules/auth/hooks/useIsOnAuthOrOnboardingPage.ts b/packages/twenty-front/src/modules/auth/hooks/useIsOnAuthOrOnboardingPage.ts new file mode 100644 index 0000000000..f798598fe4 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/hooks/useIsOnAuthOrOnboardingPage.ts @@ -0,0 +1,11 @@ +import { AUTH_AND_ONBOARDING_PATHS } from '@/auth/constants/AuthAndOnboardingPaths'; +import { useLocation } from 'react-router-dom'; +import { isMatchingLocation } from '~/utils/isMatchingLocation'; + +export const useIsOnAuthOrOnboardingPage = () => { + const location = useLocation(); + + return AUTH_AND_ONBOARDING_PATHS.some((appPath) => + isMatchingLocation(location, appPath), + ); +}; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx index 9e843aca5f..94bd5d1677 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx @@ -3,13 +3,15 @@ import { Trans } from '@lingui/react/macro'; import { useWorkspaceBypass } from '@/auth/sign-in-up/hooks/useWorkspaceBypass'; import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; +import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth'; import { themeCssVariables } from 'twenty-ui/theme-constants'; const StyledCopyContainer = styled.div` align-items: center; color: ${themeCssVariables.font.color.tertiary}; font-size: ${themeCssVariables.font.size.sm}; - max-width: 280px; + line-height: 1.4; + max-width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px; text-align: center; & > a { diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx index 38881e7d96..abeb172ffe 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx @@ -8,6 +8,8 @@ import { FormProvider } from 'react-hook-form'; import { ClickToActionLink, UndecoratedLink } from 'twenty-ui/navigation'; import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer'; +import { OnboardingStepAnimatedItem } from '@/onboarding/components/OnboardingStepAnimatedItem'; +import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth'; import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/internal/SignInUpWithCredentials'; import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle'; import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft'; @@ -36,6 +38,11 @@ import { import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl'; +const StyledContentContainer = styled(StyledOnboardingContentContainer)` + max-width: 100%; + width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px; +`; + const StyledWorkspaceContainer = styled.div` background-color: ${themeCssVariables.background.secondary}; border: 1px solid ${themeCssVariables.border.color.light}; @@ -156,70 +163,82 @@ export const SignInUpGlobalScopeForm = () => { ); }; + const availableWorkspacesList = [ + ...availableWorkspaces.availableWorkspacesForSignIn, + ...availableWorkspaces.availableWorkspacesForSignUp, + ]; + return ( <> {signInUpStep === SignInUpStep.WorkspaceSelection && ( - + - {[ - ...availableWorkspaces.availableWorkspacesForSignIn, - ...availableWorkspaces.availableWorkspacesForSignUp, - ].map((availableWorkspace) => ( - ( + - + + + + + + + {availableWorkspace.displayName || + availableWorkspace.id} + + + { + new URL( + getWorkspaceUrl(availableWorkspace.workspaceUrls), + ).hostname + } + + + + + + + + + + ))} + {!isDDLLocked && ( + + + setSignInUpStep(SignInUpStep.WorkspaceCreation) + } + > - + + + - - {availableWorkspace.displayName || - availableWorkspace.id} - - - { - new URL( - getWorkspaceUrl(availableWorkspace.workspaceUrls), - ).hostname - } - + {t`Create a workspace`} - - ))} - {!isDDLLocked && ( - setSignInUpStep(SignInUpStep.WorkspaceCreation)} - > - - - - - - {t`Create a workspace`} - - - - - - + )} - + )} {signInUpStep !== SignInUpStep.WorkspaceSelection && ( - + {authProviders.google && ( { /> )} {(authProviders.google || authProviders.microsoft) && ( - + )} {/* oxlint-disable-next-line react/jsx-props-no-spreading */} @@ -248,7 +269,7 @@ export const SignInUpGlobalScopeForm = () => { )} - + )} ); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpV2StandardContent.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpStandardContent.tsx similarity index 68% rename from packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpV2StandardContent.tsx rename to packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpStandardContent.tsx index 40427385b8..f7b6bb9243 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpV2StandardContent.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpStandardContent.tsx @@ -3,13 +3,28 @@ import { Title } from '@/auth/components/Title'; import { FooterNote } from '@/auth/sign-in-up/components/FooterNote'; import { WorkspaceSelectionFooter } from '@/auth/sign-in-up/components/WorkspaceSelectionFooter'; import { SignInUpStep } from '@/auth/states/signInUpStepState'; +import { styled } from '@linaria/react'; import { type JSX } from 'react'; import { AppPath } from 'twenty-shared/types'; import { AnimatedEaseIn } from 'twenty-ui/layout'; import { ModalContent } from 'twenty-ui/surfaces'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; import { type PublicWorkspaceData } from '~/generated-metadata/graphql'; -type SignInUpV2StandardContentProps = { +const StyledTitleContainer = styled.div` + line-height: 1.2; + margin-top: ${themeCssVariables.spacing[10]}; +`; + +const StyledFormContainer = styled.div` + align-items: center; + display: flex; + flex-direction: column; + margin-bottom: ${themeCssVariables.spacing[6]}; + margin-top: ${themeCssVariables.spacing[6]}; +`; + +type SignInUpStandardContentProps = { workspacePublicData: PublicWorkspaceData | null; signInUpForm: JSX.Element | null; signInUpStep: SignInUpStep; @@ -17,13 +32,13 @@ type SignInUpV2StandardContentProps = { onClickOnLogo: () => void; }; -export const SignInUpV2StandardContent = ({ +export const SignInUpStandardContent = ({ workspacePublicData, signInUpForm, signInUpStep, title, onClickOnLogo, -}: SignInUpV2StandardContentProps) => { +}: SignInUpStandardContentProps) => { return ( @@ -31,11 +46,13 @@ export const SignInUpV2StandardContent = ({ secondaryLogo={workspacePublicData?.logo} placeholder={workspacePublicData?.displayName} onClick={onClickOnLogo} - to={AppPath.SignInUpV2} + to={AppPath.SignInUp} /> - {title} - {signInUpForm} + + {title} + + {signInUpForm} {signInUpStep === SignInUpStep.WorkspaceSelection && ( )} diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceActivationV2.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceActivationV2.tsx deleted file mode 100644 index 7146279550..0000000000 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceActivationV2.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { SubTitle } from '@/auth/components/SubTitle'; -import { WORKSPACE_ACTIVATION_MESSAGES } from '@/auth/sign-in-up/constants/WorkspaceActivationMessages'; -import { OnboardingPulsingLogo } from '@/onboarding/components/OnboardingPulsingLogo'; -import { styled } from '@linaria/react'; -import { useLingui } from '@lingui/react/macro'; -import { motion, useReducedMotion } from 'framer-motion'; -import { useContext } from 'react'; -import { ThemeContext } from 'twenty-ui/theme-constants'; - -const STEP_OPACITIES = [1, 0.4, 0.12]; -const VISIBLE_STEP_COUNT = STEP_OPACITIES.length; -const STEP_HEIGHT_IN_PX = 28; -const STEPS_CONTAINER_HEIGHT_IN_PX = STEP_HEIGHT_IN_PX * VISIBLE_STEP_COUNT; - -const StyledStepsContainer = styled.div` - height: ${STEPS_CONTAINER_HEIGHT_IN_PX}px; - position: relative; - width: 100%; -`; - -const StyledStepBase = styled.div` - left: 0; - position: absolute; - right: 0; - top: 0; -`; - -const StyledStep = motion.create(StyledStepBase); - -type SignInUpWorkspaceActivationV2Props = { - messageIndex: number; -}; - -export const SignInUpWorkspaceActivationV2 = ({ - messageIndex, -}: SignInUpWorkspaceActivationV2Props) => { - const { i18n } = useLingui(); - const { theme } = useContext(ThemeContext); - const shouldReduceMotion = useReducedMotion(); - - const messages = WORKSPACE_ACTIVATION_MESSAGES.map((message) => - i18n._(message), - ); - - return ( - <> - - - {messages.map((message, index) => { - const stepOffset = index - messageIndex; - const isVisible = stepOffset >= 0 && stepOffset < VISIBLE_STEP_COUNT; - - return ( - - {message} - - ); - })} - - - ); -}; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceCreationLoader.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceCreationLoader.tsx deleted file mode 100644 index 9fa4928120..0000000000 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceCreationLoader.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { SignInUpWorkspaceActivationV2 } from '@/auth/sign-in-up/components/SignInUpWorkspaceActivationV2'; -import { SignInUpWorkspaceActivationV2Effect } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceActivationV2Effect'; -import { styled } from '@linaria/react'; -import { useState } from 'react'; - -const StyledContainer = styled.div` - align-items: center; - display: flex; - flex: 1; - flex-direction: column; - justify-content: center; - min-height: 100%; - width: 100%; -`; - -export const SignInUpWorkspaceCreationLoader = () => { - const [messageIndex, setMessageIndex] = useState(0); - - return ( - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/SignInUpWorkspaceActivationV2.stories.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/SignInUpWorkspaceActivationV2.stories.tsx deleted file mode 100644 index e3801b9854..0000000000 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/SignInUpWorkspaceActivationV2.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { type Meta, type StoryObj } from '@storybook/react-vite'; - -import { SignInUpWorkspaceActivationV2 } from '@/auth/sign-in-up/components/SignInUpWorkspaceActivationV2'; -import { SignInUpWorkspaceActivationV2Effect } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceActivationV2Effect'; -import { useState } from 'react'; -import { ModalContent } from 'twenty-ui/surfaces'; -import { ComponentDecorator } from 'twenty-ui/testing'; - -const RenderWithModalContent = () => { - const [messageIndex, setMessageIndex] = useState(0); - - return ( - - - - - ); -}; - -const meta: Meta = { - title: 'Modules/Auth/SignInUpWorkspaceActivationV2', - component: SignInUpWorkspaceActivationV2, - decorators: [ComponentDecorator], - parameters: { - codeSection: { - docs: 'This component should always be wrapped with ModalContent in the app.\n\nCorrect usage:\n```tsx\n\n \n\n```\n', - }, - }, - render: RenderWithModalContent, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx index 112478d26a..efb02e7747 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx @@ -5,15 +5,16 @@ import { themeCssVariables } from 'twenty-ui/theme-constants'; const StyledPillContainer = styled.span` position: absolute; - right: calc(-1 * ${themeCssVariables.spacing[5]}); - top: calc(-1 * ${themeCssVariables.spacing[2]}); + right: -14px; + top: -10px; > span { - background: ${themeCssVariables.color.blue3}; - border: 1px solid ${themeCssVariables.color.blue5}; - border-radius: ${themeCssVariables.border.radius.pill}; - color: ${themeCssVariables.color.blue}; + background: ${themeCssVariables.accent.accent3}; + border: 1px solid ${themeCssVariables.accent.accent5}; + border-radius: ${themeCssVariables.border.radius.md}; + color: ${themeCssVariables.accent.accent9}; font-weight: ${themeCssVariables.font.weight.semiBold}; + height: ${themeCssVariables.spacing[5]}; } `; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification.tsx index 7449b31a46..4ab2e519e4 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification.tsx @@ -17,13 +17,11 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { OTPInput, type SlotProps } from 'input-otp'; import { useState } from 'react'; import { Controller } from 'react-hook-form'; -import { useLocation } from 'react-router-dom'; import { AppPath } from 'twenty-shared/types'; import { MainButton } from 'twenty-ui/input'; import { ClickToActionLink } from 'twenty-ui/navigation'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { useNavigateApp } from '~/hooks/useNavigateApp'; -import { isMatchingLocation } from '~/utils/isMatchingLocation'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; @@ -183,7 +181,6 @@ export const SignInUpTOTPVerification = () => { const { enqueueErrorSnackBar } = useSnackBar(); const navigate = useNavigateApp(); - const location = useLocation(); const { readCaptchaToken } = useReadCaptchaToken(); const { isCaptchaReady } = useCaptcha(); const loginToken = useAtomStateValue(loginTokenState); @@ -206,11 +203,7 @@ export const SignInUpTOTPVerification = () => { const captchaToken = readCaptchaToken(); if (!loginToken) { - return navigate( - isMatchingLocation(location, AppPath.SignInUpV2) - ? AppPath.SignInUpV2 - : AppPath.SignInUp, - ); + return navigate(AppPath.SignInUp); } await getAuthTokensFromOTP(values.otp, loginToken, captchaToken); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx index 518fdd919b..ab149988d6 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx @@ -1,51 +1,137 @@ -import { SubTitle } from '@/auth/components/SubTitle'; -import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer'; import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace'; +import { OnboardingAnimatedReveal } from '@/onboarding/components/OnboardingAnimatedReveal'; +import { OnboardingStepAnimatedItem } from '@/onboarding/components/OnboardingStepAnimatedItem'; +import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth'; import { useWorkspaceSubdomainField } from '@/auth/sign-in-up/hooks/useWorkspaceSubdomainField'; -import { isOnboardingV2State } from '@/auth/states/isOnboardingV2State'; +import { isCreatingWorkspaceState } from '@/auth/states/isCreatingWorkspaceState'; import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; -import { ImageInput } from '@/ui/input/components/ImageInput'; -import { InputHint } from '@/ui/input/components/InputHint'; -import { InputLabel } from '@/ui/input/components/InputLabel'; import { TextInput } from '@/ui/input/components/TextInput'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -import { useLingui } from '@lingui/react/macro'; import { styled } from '@linaria/react'; +import { useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Key } from 'ts-key-enum'; import { isDefined } from 'twenty-shared/utils'; -import { Loader } from 'twenty-ui/feedback'; -import { MainButton } from 'twenty-ui/input'; -import { ClickToActionLink } from 'twenty-ui/navigation'; +import { Avatar } from 'twenty-ui/data-display'; +import { IconTrash, IconUpload } from 'twenty-ui/icon'; +import { Button, LightIconButton, MainButton } from 'twenty-ui/input'; import { themeCssVariables } from 'twenty-ui/theme-constants'; -const StyledSection = styled.div` - margin-top: ${themeCssVariables.spacing[4]}; - width: 100%; -`; - -const StyledButtonContainer = styled.div` - margin-top: ${themeCssVariables.spacing[6]}; - width: 100%; -`; - -const StyledAvailableHint = styled.div` - color: ${themeCssVariables.color.green}; - font-size: ${themeCssVariables.font.size.xs}; - font-weight: ${themeCssVariables.font.weight.regular}; - margin-top: ${themeCssVariables.spacing[0.5]}; -`; - -const StyledUnavailableHint = styled.div` - color: ${themeCssVariables.color.red}; +const StyledContentContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[14]}; + max-width: 100%; + width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px; +`; + +const StyledHeading = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[4]}; +`; + +const StyledTitle = styled.div` + color: ${themeCssVariables.font.color.primary}; + font-size: ${themeCssVariables.font.size.xl}; + font-weight: ${themeCssVariables.font.weight.semiBold}; + line-height: 1.2; +`; + +const StyledSubtitle = styled.div` + color: ${themeCssVariables.font.color.secondary}; + font-size: ${themeCssVariables.font.size.md}; + line-height: 1.4; +`; + +const StyledFormSection = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[8]}; + padding-bottom: ${themeCssVariables.spacing[4]}; + width: 100%; +`; + +const StyledLogoRow = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[2]}; +`; + +const StyledLogoAvatar = styled(Avatar)` + height: ${themeCssVariables.spacing[8]}; + width: ${themeCssVariables.spacing[8]}; +`; + +const StyledLogoButtons = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing['0.5']}; +`; + +const StyledHiddenFileInput = styled.input` + display: none; +`; + +const StyledSubdomainSection = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[2]}; + width: 100%; +`; + +const StyledAlternativesBox = styled.div` + background-color: ${themeCssVariables.background.transparent.lighter}; + border: 1px solid ${themeCssVariables.border.color.medium}; + border-radius: ${themeCssVariables.border.radius.md}; + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[2]}; + padding: ${themeCssVariables.spacing[3]}; +`; + +const StyledAlternativesLabel = styled.span` + color: ${themeCssVariables.font.color.secondary}; + font-size: ${themeCssVariables.font.size.sm}; + font-weight: ${themeCssVariables.font.weight.medium}; + padding-bottom: ${themeCssVariables.spacing[1]}; +`; + +const StyledAlternativeRows = styled.div` display: flex; flex-direction: column; - font-size: ${themeCssVariables.font.size.xs}; gap: ${themeCssVariables.spacing[1]}; - margin-top: ${themeCssVariables.spacing[0.5]}; +`; + +const StyledAlternativeRow = styled.button` + align-items: center; + background: transparent; + border: none; + color: ${themeCssVariables.color.green}; + cursor: pointer; + display: flex; + font-size: ${themeCssVariables.font.size.xs}; + font-weight: ${themeCssVariables.font.weight.semiBold}; + gap: ${themeCssVariables.spacing[1]}; + padding: 2px 0; + text-align: left; +`; + +const StyledAvailabilityDotBox = styled.div` + display: flex; + padding: ${themeCssVariables.spacing[1]}; +`; + +const StyledAvailabilityDot = styled.div` + background-color: ${themeCssVariables.color.green}; + border-radius: 50%; + box-shadow: 0 0 0 3px ${themeCssVariables.color.green5}; + flex-shrink: 0; + height: 6px; + width: 6px; `; export const SignInUpWorkspaceCreationForm = () => { @@ -56,33 +142,37 @@ export const SignInUpWorkspaceCreationForm = () => { isMultiWorkspaceEnabledState, ); - const setIsOnboardingV2 = useSetAtomState(isOnboardingV2State); - - const [isSubmitting, setIsSubmitting] = useState(false); + const isCreatingWorkspace = useAtomStateValue(isCreatingWorkspaceState); + const setIsCreatingWorkspace = useSetAtomState(isCreatingWorkspaceState); const [logo, setLogo] = useState(undefined); const [logoPreviewUrl, setLogoPreviewUrl] = useState( undefined, ); + const hiddenFileInputRef = useRef(null); const { workspaceName, subdomain, status, errorMessage, - suggestion, + suggestions, isAvailable, handleWorkspaceNameChange, handleSubdomainChange, - applySuggestion, + applySuggestionValue, } = useWorkspaceSubdomainField({ isSubdomainEnabled: isMultiWorkspaceEnabled, }); const isContinueDisabled = workspaceName.trim() === '' || - isSubmitting || + isCreatingWorkspace || (isMultiWorkspaceEnabled && !isAvailable); + const openFilePicker = () => { + hiddenFileInputRef.current?.click(); + }; + const handleLogoUpload = (file: File) => { if (!isDefined(file)) { return; @@ -111,16 +201,16 @@ export const SignInUpWorkspaceCreationForm = () => { return; } - setIsSubmitting(true); - setIsOnboardingV2(false); - try { - await createWorkspace({ - displayName: workspaceName.trim(), - ...(isMultiWorkspaceEnabled ? { subdomain } : {}), - logo, - }); - } finally { - setIsSubmitting(false); + setIsCreatingWorkspace(true); + + const isWorkspaceCreated = await createWorkspace({ + displayName: workspaceName.trim(), + ...(isMultiWorkspaceEnabled ? { subdomain } : {}), + logo, + }); + + if (!isWorkspaceCreated) { + setIsCreatingWorkspace(false); } }; @@ -144,75 +234,122 @@ export const SignInUpWorkspaceCreationForm = () => { : undefined; return ( - - - {isMultiWorkspaceEnabled - ? t`Pick a name and a web address for your new workspace.` - : t`Pick a name and a logo for your new workspace.`} - - - {t`Workspace logo`} - - - - - - {isMultiWorkspaceEnabled && ( - + + + + {t`Create your workspace`} + + + + {t`Move work forward across teams and agents`} + + + + + + + + { + const file = event.target.files?.[0]; + if (isDefined(file)) { + handleLogoUpload(file); + } + event.target.value = ''; + }} + /> + + ); };