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 {
|
import {
|
||||||
OnboardingStatus,
|
OnboardingStatus,
|
||||||
PermissionFlagType,
|
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +30,6 @@ const renderHooks = (
|
|||||||
onboardingStatus: OnboardingStatus,
|
onboardingStatus: OnboardingStatus,
|
||||||
withCurrentBillingSubscription: boolean,
|
withCurrentBillingSubscription: boolean,
|
||||||
withOneWorkspaceMember = true,
|
withOneWorkspaceMember = true,
|
||||||
permissionFlags = mockedUserData.currentUserWorkspace.permissionFlags,
|
|
||||||
) => {
|
) => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => {
|
() => {
|
||||||
@@ -55,10 +53,7 @@ const renderHooks = (
|
|||||||
);
|
);
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.setCurrentUser({ ...mockedUserData, onboardingStatus });
|
result.current.setCurrentUser({ ...mockedUserData, onboardingStatus });
|
||||||
result.current.setCurrentUserWorkspace({
|
result.current.setCurrentUserWorkspace(mockedUserData.currentUserWorkspace);
|
||||||
...mockedUserData.currentUserWorkspace,
|
|
||||||
permissionFlags,
|
|
||||||
});
|
|
||||||
result.current.setCurrentWorkspace({
|
result.current.setCurrentWorkspace({
|
||||||
...mockCurrentWorkspace,
|
...mockCurrentWorkspace,
|
||||||
currentBillingSubscription: withCurrentBillingSubscription
|
currentBillingSubscription: withCurrentBillingSubscription
|
||||||
@@ -92,16 +87,6 @@ describe('useSetNextOnboardingStatus', () => {
|
|||||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.SYNC_EMAIL);
|
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', () => {
|
it('should create profile after syncing emails', () => {
|
||||||
const nextOnboardingStatus = renderHooks(
|
const nextOnboardingStatus = renderHooks(
|
||||||
OnboardingStatus.SYNC_EMAIL,
|
OnboardingStatus.SYNC_EMAIL,
|
||||||
|
|||||||
@@ -9,33 +9,25 @@ import {
|
|||||||
currentWorkspaceState,
|
currentWorkspaceState,
|
||||||
} from '@/auth/states/currentWorkspaceState';
|
} from '@/auth/states/currentWorkspaceState';
|
||||||
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
|
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
|
||||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
|
||||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import {
|
import { OnboardingStatus } from '~/generated-metadata/graphql';
|
||||||
OnboardingStatus,
|
|
||||||
PermissionFlagType,
|
|
||||||
} from '~/generated-metadata/graphql';
|
|
||||||
import { useStore } from 'jotai';
|
import { useStore } from 'jotai';
|
||||||
|
|
||||||
type GetNextOnboardingStatusArgs = {
|
type GetNextOnboardingStatusArgs = {
|
||||||
currentUser: CurrentUser | null;
|
currentUser: CurrentUser | null;
|
||||||
currentWorkspace: CurrentWorkspace | null;
|
currentWorkspace: CurrentWorkspace | null;
|
||||||
calendarBookingPageId: string | null;
|
calendarBookingPageId: string | null;
|
||||||
isAccountSyncEnabled: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNextOnboardingStatus = ({
|
const getNextOnboardingStatus = ({
|
||||||
currentUser,
|
currentUser,
|
||||||
currentWorkspace,
|
currentWorkspace,
|
||||||
calendarBookingPageId,
|
calendarBookingPageId,
|
||||||
isAccountSyncEnabled,
|
|
||||||
}: GetNextOnboardingStatusArgs) => {
|
}: GetNextOnboardingStatusArgs) => {
|
||||||
if (currentUser?.onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION) {
|
if (currentUser?.onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION) {
|
||||||
return isAccountSyncEnabled
|
return OnboardingStatus.SYNC_EMAIL;
|
||||||
? OnboardingStatus.SYNC_EMAIL
|
|
||||||
: OnboardingStatus.PROFILE_CREATION;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentUser?.onboardingStatus === OnboardingStatus.SYNC_EMAIL) {
|
if (currentUser?.onboardingStatus === OnboardingStatus.SYNC_EMAIL) {
|
||||||
@@ -63,16 +55,12 @@ export const useSetNextOnboardingStatus = () => {
|
|||||||
const currentUser = useAtomStateValue(currentUserState);
|
const currentUser = useAtomStateValue(currentUserState);
|
||||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||||
const calendarBookingPageId = useAtomStateValue(calendarBookingPageIdState);
|
const calendarBookingPageId = useAtomStateValue(calendarBookingPageIdState);
|
||||||
const permissionMap = usePermissionFlagMap();
|
|
||||||
const isAccountSyncEnabled =
|
|
||||||
permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS];
|
|
||||||
|
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
const nextOnboardingStatus = getNextOnboardingStatus({
|
const nextOnboardingStatus = getNextOnboardingStatus({
|
||||||
currentUser,
|
currentUser,
|
||||||
currentWorkspace,
|
currentWorkspace,
|
||||||
calendarBookingPageId,
|
calendarBookingPageId,
|
||||||
isAccountSyncEnabled,
|
|
||||||
});
|
});
|
||||||
store.set(currentUserState.atom, (current) => {
|
store.set(currentUserState.atom, (current) => {
|
||||||
if (isDefined(current)) {
|
if (isDefined(current)) {
|
||||||
@@ -83,11 +71,5 @@ export const useSetNextOnboardingStatus = () => {
|
|||||||
}
|
}
|
||||||
return current;
|
return current;
|
||||||
});
|
});
|
||||||
}, [
|
}, [currentUser, currentWorkspace, calendarBookingPageId, store]);
|
||||||
currentUser,
|
|
||||||
currentWorkspace,
|
|
||||||
calendarBookingPageId,
|
|
||||||
isAccountSyncEnabled,
|
|
||||||
store,
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user