From 74b56ba66c9fba4f5ccb1615841359ce049856df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Tue, 16 Jun 2026 23:37:51 +0200 Subject: [PATCH] fix(onboarding): show the connect step after workspace creation (#21701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Creating a new workspace as an existing user (e.g. someone who already has other workspaces) skips the **Connect account / Sync emails** step entirely — the connect modal never appears. ## Root cause (regression from #21640) `#21640` moved the `CONNECTED_ACCOUNTS` permission gate onto the `WORKSPACE_ACTIVATION → SYNC_EMAIL` transition in `getNextOnboardingStatus`: ```ts // before #21640: WORKSPACE_ACTIVATION → PROFILE_CREATION (no permission read) // after #21640: if (WORKSPACE_ACTIVATION) { return isAccountSyncEnabled ? SYNC_EMAIL : PROFILE_CREATION; } ``` That transition fires from `CreateWorkspace.tsx` immediately after `activateWorkspace()` + `loadCurrentUser()`. But `setNextOnboardingStatus` is a memoized callback that captured `isAccountSyncEnabled` at render time — *before* activation, when the brand-new workspace has no roles/permissions yet, so `currentUserWorkspace.permissionFlags` is empty and the flag reads `false`. `loadCurrentUser()` refreshes the atoms, but the executing callback still holds the stale `false`. So it optimistically routes to `PROFILE_CREATION`, skipping `SYNC_EMAIL` and overriding the backend status (which correctly says `SYNC_EMAIL`). For an existing user the profile step is also a no-op (name already set), so they sail straight into the app. Before #21640 the same permission gate lived on the `PROFILE_CREATION → SYNC_EMAIL` transition, which fires from the profile step — long after activation, when permissions are loaded — so it never misfired. ## Fix The backend (`OnboardingService.getOnboardingStatus`) decides the connect step purely from `ONBOARDING_CONNECT_ACCOUNT_PENDING` and never consults the permission. Drop the permission gate from the frontend transition so the two agree — `WORKSPACE_ACTIVATION` always advances to `SYNC_EMAIL`. The `WORKSPACE_ACTIVATION` branch only ever runs for workspace creators (who are admins with the permission), so the gate was only ever reachable via the stale read. Removes the now-unused `isAccountSyncEnabled` / `usePermissionFlagMap` plumbing and the obsolete "skip SyncEmail when account sync is disabled" unit test. ## Test plan - [ ] Create a new workspace as an existing user (with other workspaces) → the Connect account / Sync emails step now appears - [ ] Fresh signup → onboarding still flows `Workspace activation → Sync emails → Create profile → Invite team` - [x] `useSetNextOnboardingStatus` unit tests updated (the "after workspace activation → SYNC_EMAIL" case is retained and now unconditional) - [x] `npx nx typecheck twenty-front` — clean for changed files (only the pre-existing, unrelated `idb-keyval` module-resolution errors remain in this environment) - [x] `npx nx lint:diff-with-main twenty-front` — changed files clean > Note: the unit test file couldn't be executed in my sandbox because `idb-keyval` (a declared dependency, pulled in transitively via `jotaiStore`) isn't installed here; it runs normally in CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY --- _Generated by [Claude Code](https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY)_ Review in cubic Co-authored-by: Claude --- .../useSetNextOnboardingStatus.test.ts | 17 +------------ .../hooks/useSetNextOnboardingStatus.ts | 24 +++---------------- 2 files changed, 4 insertions(+), 37 deletions(-) diff --git a/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts b/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts index f5209481ac..77d5431ab1 100644 --- a/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts +++ b/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts @@ -16,7 +16,6 @@ import { import { OnboardingStatus, - PermissionFlagType, SubscriptionStatus, } from '~/generated-metadata/graphql'; import { @@ -31,7 +30,6 @@ const renderHooks = ( onboardingStatus: OnboardingStatus, withCurrentBillingSubscription: boolean, withOneWorkspaceMember = true, - permissionFlags = mockedUserData.currentUserWorkspace.permissionFlags, ) => { const { result } = renderHook( () => { @@ -55,10 +53,7 @@ const renderHooks = ( ); act(() => { result.current.setCurrentUser({ ...mockedUserData, onboardingStatus }); - result.current.setCurrentUserWorkspace({ - ...mockedUserData.currentUserWorkspace, - permissionFlags, - }); + result.current.setCurrentUserWorkspace(mockedUserData.currentUserWorkspace); result.current.setCurrentWorkspace({ ...mockCurrentWorkspace, currentBillingSubscription: withCurrentBillingSubscription @@ -92,16 +87,6 @@ describe('useSetNextOnboardingStatus', () => { expect(nextOnboardingStatus).toEqual(OnboardingStatus.SYNC_EMAIL); }); - it('should skip SyncEmail when account sync is disabled', () => { - const nextOnboardingStatus = renderHooks( - OnboardingStatus.WORKSPACE_ACTIVATION, - false, - true, - [PermissionFlagType.WORKSPACE_MEMBERS], - ); - expect(nextOnboardingStatus).toEqual(OnboardingStatus.PROFILE_CREATION); - }); - it('should create profile after syncing emails', () => { const nextOnboardingStatus = renderHooks( OnboardingStatus.SYNC_EMAIL, diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts b/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts index a3a5ea1bac..0ea27cbaad 100644 --- a/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts +++ b/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts @@ -9,33 +9,25 @@ import { currentWorkspaceState, } from '@/auth/states/currentWorkspaceState'; import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState'; -import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useCallback } from 'react'; -import { - OnboardingStatus, - PermissionFlagType, -} from '~/generated-metadata/graphql'; +import { OnboardingStatus } from '~/generated-metadata/graphql'; import { useStore } from 'jotai'; type GetNextOnboardingStatusArgs = { currentUser: CurrentUser | null; currentWorkspace: CurrentWorkspace | null; calendarBookingPageId: string | null; - isAccountSyncEnabled: boolean; }; const getNextOnboardingStatus = ({ currentUser, currentWorkspace, calendarBookingPageId, - isAccountSyncEnabled, }: GetNextOnboardingStatusArgs) => { if (currentUser?.onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION) { - return isAccountSyncEnabled - ? OnboardingStatus.SYNC_EMAIL - : OnboardingStatus.PROFILE_CREATION; + return OnboardingStatus.SYNC_EMAIL; } if (currentUser?.onboardingStatus === OnboardingStatus.SYNC_EMAIL) { @@ -63,16 +55,12 @@ export const useSetNextOnboardingStatus = () => { const currentUser = useAtomStateValue(currentUserState); const currentWorkspace = useAtomStateValue(currentWorkspaceState); const calendarBookingPageId = useAtomStateValue(calendarBookingPageIdState); - const permissionMap = usePermissionFlagMap(); - const isAccountSyncEnabled = - permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS]; return useCallback(() => { const nextOnboardingStatus = getNextOnboardingStatus({ currentUser, currentWorkspace, calendarBookingPageId, - isAccountSyncEnabled, }); store.set(currentUserState.atom, (current) => { if (isDefined(current)) { @@ -83,11 +71,5 @@ export const useSetNextOnboardingStatus = () => { } return current; }); - }, [ - currentUser, - currentWorkspace, - calendarBookingPageId, - isAccountSyncEnabled, - store, - ]); + }, [currentUser, currentWorkspace, calendarBookingPageId, store]); };