fix(onboarding): show the connect step after workspace creation (#21701)
## 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)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21701?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+1
-16
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user