Only show the install-apps onboarding step to workspace creators (#22990)

New users joining an existing workspace through an invite link were
routed through the "Install your first apps" onboarding step, which is
meant for workspace creation only.

#22347 set `ONBOARDING_INSTALL_APPS_PENDING` inside
`activateOnboardingForUser`, which is shared by both sign-up paths. Gate
it per path like the connect-account step: `true` on
`signUpOnNewWorkspace`, `false` on `signInUpOnExistingWorkspace`.

Verified locally: invited users now go straight from create-profile into
the workspace, and workspace creators still get the install-apps step.
Covered by a unit test on the invite path and integration tests
asserting the onboarding status per sign-up path (invite →
`PROFILE_CREATION`; workspace creation → `SYNC_EMAIL` then
`APPS_INSTALLATION`).
This commit is contained in:
Charles Bochet
2026-07-17 15:33:42 +02:00
committed by GitHub
parent 3303bdd258
commit 39082cf787
7 changed files with 251 additions and 70 deletions
@@ -1,3 +1,5 @@
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import {
AuthException,
AuthExceptionCode,
@@ -57,6 +59,20 @@ const createSignInUpServiceForTests = () => {
release: jest.fn(),
};
const mockUserWorkspaceService = {
create: jest.fn(),
checkUserWorkspaceExists: jest.fn(),
addUserToWorkspaceIfUserNotInWorkspace: jest.fn(),
};
const mockOnboardingService = {
setOnboardingConnectAccountPending: jest.fn(),
setOnboardingCreateProfilePending: jest.fn(),
setOnboardingInstallAppsPending: jest.fn(),
setOnboardingInviteTeamPending: jest.fn(),
createOnboardingStatusForWorkspaceMember: jest.fn(),
};
const service = new SignInUpService(
mockUserRepository as any,
mockWorkspaceRepository as any,
@@ -64,15 +80,8 @@ const createSignInUpServiceForTests = () => {
validatePersonalInvitation: jest.fn(),
invalidateWorkspaceInvitation: jest.fn(),
} as any,
{
create: jest.fn(),
checkUserWorkspaceExists: jest.fn(),
} as any,
{
setOnboardingCreateProfilePending: jest.fn(),
setOnboardingInviteTeamPending: jest.fn(),
createOnboardingStatusForWorkspaceMember: jest.fn(),
} as any,
mockUserWorkspaceService as any,
mockOnboardingService as any,
{
emitCustomBatchEvent: jest.fn(),
} as any,
@@ -121,6 +130,7 @@ const createSignInUpServiceForTests = () => {
mockUserRepository,
mockWorkspaceRepository,
mockConfigurationValues,
mockOnboardingService,
};
};
@@ -309,3 +319,34 @@ describe('SignInUpService workspace-creation policy', () => {
});
});
});
describe('SignInUpService onboarding steps', () => {
it('does not flag the install-apps step for a new user joining an existing workspace', async () => {
const { service, mockOnboardingService } = createSignInUpServiceForTests();
await service.signInUpOnExistingWorkspace({
workspace: {
id: 'existing-workspace-id',
activationStatus: WorkspaceActivationStatus.ACTIVE,
} as any,
userData: {
type: 'newUserWithPicture',
newUserWithPicture: {
email: 'invited.user@acme.dev',
firstName: 'Invited',
lastName: 'User',
},
},
});
expect(
mockOnboardingService.setOnboardingCreateProfilePending,
).toHaveBeenCalledWith(expect.objectContaining({ value: true }), undefined);
expect(
mockOnboardingService.setOnboardingInstallAppsPending,
).not.toHaveBeenCalled();
expect(
mockOnboardingService.setOnboardingConnectAccountPending,
).not.toHaveBeenCalled();
});
});
@@ -332,6 +332,7 @@ export class SignInUpService {
user,
workspace: params.workspace,
shouldShowConnectAccountStep: false,
shouldShowInstallAppsStep: false,
});
await this.userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace(
@@ -364,10 +365,12 @@ export class SignInUpService {
user,
workspace,
shouldShowConnectAccountStep,
shouldShowInstallAppsStep,
}: {
user: Pick<UserEntity, 'id' | 'firstName' | 'lastName'>;
workspace: WorkspaceEntity;
shouldShowConnectAccountStep: boolean;
shouldShowInstallAppsStep: boolean;
},
queryRunner?: QueryRunner,
) {
@@ -391,14 +394,16 @@ export class SignInUpService {
queryRunner,
);
await this.onboardingService.setOnboardingInstallAppsPending(
{
userId: user.id,
workspaceId: workspace.id,
value: true,
},
queryRunner,
);
if (shouldShowInstallAppsStep) {
await this.onboardingService.setOnboardingInstallAppsPending(
{
userId: user.id,
workspaceId: workspace.id,
value: true,
},
queryRunner,
);
}
}
private async saveNewUser(
@@ -670,6 +675,7 @@ export class SignInUpService {
user,
workspace,
shouldShowConnectAccountStep: true,
shouldShowInstallAppsStep: true,
},
queryRunner,
);