From 2e6077383b3d2d13e2bc06db4fa246202f6f3a85 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Wed, 1 Jul 2026 14:25:16 +0200
Subject: [PATCH] Add install your first apps onboarding V2 step (#22347)
https://github.com/user-attachments/assets/5326d48f-1842-4db1-bc7c-94852145c035
Adds an "Install your first apps" step to the V2 onboarding, shown right
after import-contacts. It lets users opt into installing marketplace
apps (Call recorder and People Data Labs for now) during onboarding.
- New backend `OnboardingStatus.APPS_INSTALLATION` (between SYNC_EMAIL
and PROFILE_CREATION); V1 auto-skips it.
- The primary button sends the selected app ids to the server via
`triggerInstallAppsOnboardingStep`, which enqueues a dedicated job that
installs them asynchronously so onboarding isn't blocked. Skip continues
without installing.
- The workspace is credited per app on successful installation. Credits
are env-driven via `ONBOARDING_INSTALL_APPS_CREDITS_REWARD_PER_APP`,
shown as "Earn +N free credits (1 per tool)".
---
.../src/metadata/generated/schema.graphql | 2 +
.../src/metadata/generated/schema.ts | 5 +-
.../src/metadata/generated/types.ts | 9 +
.../src/generated-metadata/graphql.ts | 15 ++
...sePageChangeEffectNavigateLocation.test.ts | 42 ++++
.../usePageChangeEffectNavigateLocation.ts | 7 +
.../modules/app/hooks/useCreateAppRouter.tsx | 14 ++
.../modules/auth/constants/OnboardingPaths.ts | 1 +
.../client-config/types/OnboardingConfig.ts | 1 +
.../constants/OnboardingInstallableApps.ts | 15 ++
.../InstallAppsAutoSkipEffect.tsx | 35 +++
.../triggerInstallAppsOnboardingStep.ts | 11 +
.../useInstallOnboardingApps.test.ts | 162 ++++++++++++
.../useSetNextOnboardingStatus.test.ts | 9 +-
.../hooks/useInstallOnboardingApps.ts | 55 ++++
.../modules/onboarding/hooks/useInviteTeam.ts | 8 +-
.../hooks/useOnboardingFreeCreditsTotal.ts | 4 +-
.../hooks/useSetNextOnboardingStatus.ts | 4 +
.../useTriggerInstallAppsOnboardingStep.ts | 21 ++
.../states/onboardingFreeCreditsState.ts | 5 +-
.../onboarding/types/OnboardingFreeCredits.ts | 1 +
.../types/OnboardingInstallableApp.ts | 7 +
.../src/pages/onboarding/InstallApps.tsx | 234 ++++++++++++++++++
.../src/pages/onboarding/InstallAppsV2.tsx | 56 +++++
.../onboarding/WorkspaceActivationV2.tsx | 6 +-
.../__stories__/InstallAppsV2.stories.tsx | 67 +++++
.../src/testing/constants/UntestedAppPaths.ts | 1 +
.../src/testing/mock-data/config.ts | 1 +
.../auth/services/sign-in-up.service.ts | 9 +
.../client-config.controller.spec.ts | 1 +
.../client-config/client-config.entity.ts | 2 +
.../services/client-config.service.spec.ts | 2 +
.../services/client-config.service.ts | 5 +
.../core-modules/message-queue/jobs.module.ts | 8 +
...g-installable-app-universal-identifiers.ts | 4 +
.../enums/onboarding-status.enum.ts | 1 +
.../install-onboarding-apps.job-constants.ts | 6 +
.../jobs/install-onboarding-apps.job.spec.ts | 158 ++++++++++++
.../jobs/install-onboarding-apps.job.ts | 73 ++++++
.../onboarding/onboarding.resolver.ts | 19 +-
.../onboarding/onboarding.service.spec.ts | 102 ++++++++
.../onboarding/onboarding.service.ts | 129 ++++++++++
.../twenty-config/config-variables.ts | 11 +
...er-settings-onboarding.integration-spec.ts | 23 +-
packages/twenty-shared/src/types/AppPath.ts | 1 +
45 files changed, 1336 insertions(+), 16 deletions(-)
create mode 100644 packages/twenty-front/src/modules/onboarding/constants/OnboardingInstallableApps.ts
create mode 100644 packages/twenty-front/src/modules/onboarding/effect-components/InstallAppsAutoSkipEffect.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/graphql/mutations/triggerInstallAppsOnboardingStep.ts
create mode 100644 packages/twenty-front/src/modules/onboarding/hooks/__tests__/useInstallOnboardingApps.test.ts
create mode 100644 packages/twenty-front/src/modules/onboarding/hooks/useInstallOnboardingApps.ts
create mode 100644 packages/twenty-front/src/modules/onboarding/hooks/useTriggerInstallAppsOnboardingStep.ts
create mode 100644 packages/twenty-front/src/modules/onboarding/types/OnboardingInstallableApp.ts
create mode 100644 packages/twenty-front/src/pages/onboarding/InstallApps.tsx
create mode 100644 packages/twenty-front/src/pages/onboarding/InstallAppsV2.tsx
create mode 100644 packages/twenty-front/src/pages/onboarding/__stories__/InstallAppsV2.stories.tsx
create mode 100644 packages/twenty-server/src/engine/core-modules/onboarding/constants/onboarding-installable-app-universal-identifiers.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job-constants.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job.spec.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job.ts
diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
index 7db61ae21f..b6b9ff17de 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
@@ -1022,6 +1022,7 @@ enum OnboardingStatus {
WORKSPACE_ACTIVATION
PROFILE_CREATION
SYNC_EMAIL
+ APPS_INSTALLATION
INVITE_TEAM
BOOK_ONBOARDING
COMPLETED
@@ -3314,6 +3315,7 @@ type Mutation {
assignRoleToApiKey(apiKeyId: UUID!, roleId: UUID!): Boolean!
skipSyncEmailOnboardingStep: OnboardingStepSuccess!
skipBookOnboardingStep: OnboardingStepSuccess!
+ triggerInstallAppsOnboardingStep(universalIdentifiers: [String!]!): OnboardingStepSuccess!
updateOneApplicationVariable(key: String!, value: String!, applicationId: UUID!): Boolean!
checkoutSession(recurringInterval: SubscriptionInterval!, plan: BillingPlanKey! = PRO, requirePaymentMethod: Boolean! = true, successUrlPath: String): BillingSession!
createSubscriptionPaymentIntent(recurringInterval: SubscriptionInterval!, plan: BillingPlanKey! = PRO, requirePaymentMethod: Boolean! = true, successUrlPath: String, idempotencyKey: String!): BillingPaymentIntent!
diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
index 61fbf816a3..7463f3a6d5 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
@@ -731,7 +731,7 @@ export interface User {
/** Onboarding status */
-export type OnboardingStatus = 'PLAN_REQUIRED' | 'WORKSPACE_ACTIVATION' | 'PROFILE_CREATION' | 'SYNC_EMAIL' | 'INVITE_TEAM' | 'BOOK_ONBOARDING' | 'COMPLETED'
+export type OnboardingStatus = 'PLAN_REQUIRED' | 'WORKSPACE_ACTIVATION' | 'PROFILE_CREATION' | 'SYNC_EMAIL' | 'APPS_INSTALLATION' | 'INVITE_TEAM' | 'BOOK_ONBOARDING' | 'COMPLETED'
export interface RatioAggregateConfig {
fieldMetadataId: Scalars['UUID']
@@ -2835,6 +2835,7 @@ export interface Mutation {
assignRoleToApiKey: Scalars['Boolean']
skipSyncEmailOnboardingStep: OnboardingStepSuccess
skipBookOnboardingStep: OnboardingStepSuccess
+ triggerInstallAppsOnboardingStep: OnboardingStepSuccess
updateOneApplicationVariable: Scalars['Boolean']
checkoutSession: BillingSession
createSubscriptionPaymentIntent: BillingPaymentIntent
@@ -6047,6 +6048,7 @@ export interface MutationGenqlSelection{
assignRoleToApiKey?: { __args: {apiKeyId: Scalars['UUID'], roleId: Scalars['UUID']} }
skipSyncEmailOnboardingStep?: OnboardingStepSuccessGenqlSelection
skipBookOnboardingStep?: OnboardingStepSuccessGenqlSelection
+ triggerInstallAppsOnboardingStep?: (OnboardingStepSuccessGenqlSelection & { __args: {universalIdentifiers: Scalars['String'][]} })
updateOneApplicationVariable?: { __args: {key: Scalars['String'], value: Scalars['String'], applicationId: Scalars['UUID']} }
checkoutSession?: (BillingSessionGenqlSelection & { __args: {recurringInterval: SubscriptionInterval, plan: BillingPlanKey, requirePaymentMethod: Scalars['Boolean'], successUrlPath?: (Scalars['String'] | null)} })
createSubscriptionPaymentIntent?: (BillingPaymentIntentGenqlSelection & { __args: {recurringInterval: SubscriptionInterval, plan: BillingPlanKey, requirePaymentMethod: Scalars['Boolean'], successUrlPath?: (Scalars['String'] | null), idempotencyKey: Scalars['String']} })
@@ -8966,6 +8968,7 @@ export const enumOnboardingStatus = {
WORKSPACE_ACTIVATION: 'WORKSPACE_ACTIVATION' as const,
PROFILE_CREATION: 'PROFILE_CREATION' as const,
SYNC_EMAIL: 'SYNC_EMAIL' as const,
+ APPS_INSTALLATION: 'APPS_INSTALLATION' as const,
INVITE_TEAM: 'INVITE_TEAM' as const,
BOOK_ONBOARDING: 'BOOK_ONBOARDING' as const,
COMPLETED: 'COMPLETED' as const
diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts
index a3d06304bc..2fc2d07041 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/types.ts
+++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts
@@ -7416,6 +7416,15 @@ export default {
"skipBookOnboardingStep": [
156
],
+ "triggerInstallAppsOnboardingStep": [
+ 156,
+ {
+ "universalIdentifiers": [
+ 1,
+ "[String!]!"
+ ]
+ }
+ ],
"updateOneApplicationVariable": [
6,
{
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index cbff9fd2fb..a1a0e1b433 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -2634,6 +2634,7 @@ export type Mutation = {
syncMarketplaceCatalog: Scalars['Boolean']['output'];
trackAnalytics: Analytics;
transferApplicationRegistrationOwnership: ApplicationRegistration;
+ triggerInstallAppsOnboardingStep: OnboardingStepSuccess;
unarchiveChatThread: AgentChatThread;
uninstallApplication: Scalars['Boolean']['output'];
updateApiKey?: Maybe;
@@ -3468,6 +3469,11 @@ export type MutationTransferApplicationRegistrationOwnershipArgs = {
};
+export type MutationTriggerInstallAppsOnboardingStepArgs = {
+ universalIdentifiers: Array;
+};
+
+
export type MutationUnarchiveChatThreadArgs = {
id: Scalars['UUID']['input'];
};
@@ -4014,6 +4020,7 @@ export type ObjectStandardOverrides = {
/** Onboarding status */
export enum OnboardingStatus {
+ APPS_INSTALLATION = 'APPS_INSTALLATION',
BOOK_ONBOARDING = 'BOOK_ONBOARDING',
COMPLETED = 'COMPLETED',
INVITE_TEAM = 'INVITE_TEAM',
@@ -7277,6 +7284,13 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
+export type TriggerInstallAppsOnboardingStepMutationVariables = Exact<{
+ universalIdentifiers: Array | Scalars['String']['input'];
+}>;
+
+
+export type TriggerInstallAppsOnboardingStepMutation = { __typename?: 'Mutation', triggerInstallAppsOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
+
export type GetInviteSuggestionsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -8830,6 +8844,7 @@ export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"k
export const ObjectRecordCountsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectRecordCounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectRecordCounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectNamePlural"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode;
export const SkipBookOnboardingStepDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SkipBookOnboardingStep"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"skipBookOnboardingStep"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode;
export const SkipSyncEmailOnboardingStepDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SkipSyncEmailOnboardingStep"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"skipSyncEmailOnboardingStep"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode;
+export const TriggerInstallAppsOnboardingStepDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TriggerInstallAppsOnboardingStep"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifiers"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"triggerInstallAppsOnboardingStep"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifiers"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifiers"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode;
export const GetInviteSuggestionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInviteSuggestions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getInviteSuggestions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}}]}}]}}]} as unknown as DocumentNode;
export const ResetPageLayoutTabToDefaultDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ResetPageLayoutTabToDefault"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resetPageLayoutTabToDefault"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageLayoutTabFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageLayoutWidgetFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayoutWidget"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"objectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"deletedAt"}},{"kind":"Field","name":{"kind":"Name","value":"conditionalDisplay"}},{"kind":"Field","name":{"kind":"Name","value":"conditionalAvailabilityExpression"}},{"kind":"Field","name":{"kind":"Name","value":"gridPosition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"column"}},{"kind":"Field","name":{"kind":"Name","value":"columnSpan"}},{"kind":"Field","name":{"kind":"Name","value":"row"}},{"kind":"Field","name":{"kind":"Name","value":"rowSpan"}}]}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayoutWidgetGridPosition"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"layoutMode"}},{"kind":"Field","name":{"kind":"Name","value":"row"}},{"kind":"Field","name":{"kind":"Name","value":"column"}},{"kind":"Field","name":{"kind":"Name","value":"rowSpan"}},{"kind":"Field","name":{"kind":"Name","value":"columnSpan"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayoutWidgetVerticalListPosition"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"layoutMode"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayoutWidgetCanvasPosition"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"layoutMode"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"configuration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BarChartConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateOperation"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisGroupByFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisGroupBySubFieldName"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisDateGranularity"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisOrderBy"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisManualSortOrder"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupByFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupBySubFieldName"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupByDateGranularity"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisOrderBy"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisManualSortOrder"}},{"kind":"Field","name":{"kind":"Name","value":"omitNullValues"}},{"kind":"Field","name":{"kind":"Name","value":"axisNameDisplay"}},{"kind":"Field","name":{"kind":"Name","value":"displayDataLabel"}},{"kind":"Field","name":{"kind":"Name","value":"displayLegend"}},{"kind":"Field","name":{"kind":"Name","value":"rangeMin"}},{"kind":"Field","name":{"kind":"Name","value":"rangeMax"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"filter"}},{"kind":"Field","name":{"kind":"Name","value":"groupMode"}},{"kind":"Field","name":{"kind":"Name","value":"layout"}},{"kind":"Field","name":{"kind":"Name","value":"isCumulative"}},{"kind":"Field","name":{"kind":"Name","value":"splitMultiValueFields"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"firstDayOfTheWeek"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LineChartConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateOperation"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisGroupByFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisGroupBySubFieldName"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisDateGranularity"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisOrderBy"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisManualSortOrder"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupByFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupBySubFieldName"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupByDateGranularity"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisOrderBy"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisManualSortOrder"}},{"kind":"Field","name":{"kind":"Name","value":"omitNullValues"}},{"kind":"Field","name":{"kind":"Name","value":"axisNameDisplay"}},{"kind":"Field","name":{"kind":"Name","value":"displayDataLabel"}},{"kind":"Field","name":{"kind":"Name","value":"displayLegend"}},{"kind":"Field","name":{"kind":"Name","value":"rangeMin"}},{"kind":"Field","name":{"kind":"Name","value":"rangeMax"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"filter"}},{"kind":"Field","name":{"kind":"Name","value":"isStacked"}},{"kind":"Field","name":{"kind":"Name","value":"isCumulative"}},{"kind":"Field","name":{"kind":"Name","value":"splitMultiValueFields"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"firstDayOfTheWeek"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PieChartConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"groupByFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateOperation"}},{"kind":"Field","name":{"kind":"Name","value":"groupBySubFieldName"}},{"kind":"Field","name":{"kind":"Name","value":"dateGranularity"}},{"kind":"Field","name":{"kind":"Name","value":"orderBy"}},{"kind":"Field","name":{"kind":"Name","value":"manualSortOrder"}},{"kind":"Field","name":{"kind":"Name","value":"displayDataLabel"}},{"kind":"Field","name":{"kind":"Name","value":"showCenterMetric"}},{"kind":"Field","name":{"kind":"Name","value":"displayLegend"}},{"kind":"Field","name":{"kind":"Name","value":"hideEmptyCategory"}},{"kind":"Field","name":{"kind":"Name","value":"splitMultiValueFields"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"filter"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"firstDayOfTheWeek"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AggregateChartConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateOperation"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"displayDataLabel"}},{"kind":"Field","name":{"kind":"Name","value":"numberFormat"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"filter"}},{"kind":"Field","name":{"kind":"Name","value":"prefix"}},{"kind":"Field","name":{"kind":"Name","value":"suffix"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"firstDayOfTheWeek"}},{"kind":"Field","name":{"kind":"Name","value":"ratioAggregateConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"optionValue"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"IframeConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StandaloneRichTextConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"blocknote"}},{"kind":"Field","name":{"kind":"Name","value":"markdown"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CalendarConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EmailsConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EmailThreadConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FieldConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"fieldDisplayMode"}},{"kind":"Field","name":{"kind":"Name","value":"fieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FieldRichTextConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FieldsConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"newFieldDefaultVisibility"}},{"kind":"Field","name":{"kind":"Name","value":"shouldAllowUserToSeeHiddenFields"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FilesConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotesConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TasksConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ViewConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RecordTableConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkflowConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkflowRunConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkflowVersionConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FrontComponentConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"frontComponentId"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageLayoutTabId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageLayoutTabFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayoutTab"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"layoutMode"}},{"kind":"Field","name":{"kind":"Name","value":"widgets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageLayoutWidgetFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageLayoutId"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode;
export const ResetPageLayoutToDefaultDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ResetPageLayoutToDefault"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resetPageLayoutToDefault"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageLayoutFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageLayoutWidgetFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayoutWidget"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"objectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"deletedAt"}},{"kind":"Field","name":{"kind":"Name","value":"conditionalDisplay"}},{"kind":"Field","name":{"kind":"Name","value":"conditionalAvailabilityExpression"}},{"kind":"Field","name":{"kind":"Name","value":"gridPosition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"column"}},{"kind":"Field","name":{"kind":"Name","value":"columnSpan"}},{"kind":"Field","name":{"kind":"Name","value":"row"}},{"kind":"Field","name":{"kind":"Name","value":"rowSpan"}}]}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayoutWidgetGridPosition"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"layoutMode"}},{"kind":"Field","name":{"kind":"Name","value":"row"}},{"kind":"Field","name":{"kind":"Name","value":"column"}},{"kind":"Field","name":{"kind":"Name","value":"rowSpan"}},{"kind":"Field","name":{"kind":"Name","value":"columnSpan"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayoutWidgetVerticalListPosition"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"layoutMode"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayoutWidgetCanvasPosition"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"layoutMode"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"configuration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BarChartConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateOperation"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisGroupByFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisGroupBySubFieldName"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisDateGranularity"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisOrderBy"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisManualSortOrder"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupByFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupBySubFieldName"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupByDateGranularity"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisOrderBy"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisManualSortOrder"}},{"kind":"Field","name":{"kind":"Name","value":"omitNullValues"}},{"kind":"Field","name":{"kind":"Name","value":"axisNameDisplay"}},{"kind":"Field","name":{"kind":"Name","value":"displayDataLabel"}},{"kind":"Field","name":{"kind":"Name","value":"displayLegend"}},{"kind":"Field","name":{"kind":"Name","value":"rangeMin"}},{"kind":"Field","name":{"kind":"Name","value":"rangeMax"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"filter"}},{"kind":"Field","name":{"kind":"Name","value":"groupMode"}},{"kind":"Field","name":{"kind":"Name","value":"layout"}},{"kind":"Field","name":{"kind":"Name","value":"isCumulative"}},{"kind":"Field","name":{"kind":"Name","value":"splitMultiValueFields"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"firstDayOfTheWeek"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LineChartConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateOperation"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisGroupByFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisGroupBySubFieldName"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisDateGranularity"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisOrderBy"}},{"kind":"Field","name":{"kind":"Name","value":"primaryAxisManualSortOrder"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupByFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupBySubFieldName"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisGroupByDateGranularity"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisOrderBy"}},{"kind":"Field","name":{"kind":"Name","value":"secondaryAxisManualSortOrder"}},{"kind":"Field","name":{"kind":"Name","value":"omitNullValues"}},{"kind":"Field","name":{"kind":"Name","value":"axisNameDisplay"}},{"kind":"Field","name":{"kind":"Name","value":"displayDataLabel"}},{"kind":"Field","name":{"kind":"Name","value":"displayLegend"}},{"kind":"Field","name":{"kind":"Name","value":"rangeMin"}},{"kind":"Field","name":{"kind":"Name","value":"rangeMax"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"filter"}},{"kind":"Field","name":{"kind":"Name","value":"isStacked"}},{"kind":"Field","name":{"kind":"Name","value":"isCumulative"}},{"kind":"Field","name":{"kind":"Name","value":"splitMultiValueFields"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"firstDayOfTheWeek"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PieChartConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"groupByFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateOperation"}},{"kind":"Field","name":{"kind":"Name","value":"groupBySubFieldName"}},{"kind":"Field","name":{"kind":"Name","value":"dateGranularity"}},{"kind":"Field","name":{"kind":"Name","value":"orderBy"}},{"kind":"Field","name":{"kind":"Name","value":"manualSortOrder"}},{"kind":"Field","name":{"kind":"Name","value":"displayDataLabel"}},{"kind":"Field","name":{"kind":"Name","value":"showCenterMetric"}},{"kind":"Field","name":{"kind":"Name","value":"displayLegend"}},{"kind":"Field","name":{"kind":"Name","value":"hideEmptyCategory"}},{"kind":"Field","name":{"kind":"Name","value":"splitMultiValueFields"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"filter"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"firstDayOfTheWeek"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AggregateChartConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"aggregateOperation"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"displayDataLabel"}},{"kind":"Field","name":{"kind":"Name","value":"numberFormat"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"filter"}},{"kind":"Field","name":{"kind":"Name","value":"prefix"}},{"kind":"Field","name":{"kind":"Name","value":"suffix"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"firstDayOfTheWeek"}},{"kind":"Field","name":{"kind":"Name","value":"ratioAggregateConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"optionValue"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"IframeConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StandaloneRichTextConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"blocknote"}},{"kind":"Field","name":{"kind":"Name","value":"markdown"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CalendarConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EmailsConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EmailThreadConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FieldConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"fieldDisplayMode"}},{"kind":"Field","name":{"kind":"Name","value":"fieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FieldRichTextConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FieldsConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"newFieldDefaultVisibility"}},{"kind":"Field","name":{"kind":"Name","value":"shouldAllowUserToSeeHiddenFields"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FilesConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotesConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TasksConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ViewConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RecordTableConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkflowConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkflowRunConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkflowVersionConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FrontComponentConfiguration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configurationType"}},{"kind":"Field","name":{"kind":"Name","value":"frontComponentId"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageLayoutTabId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageLayoutTabFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayoutTab"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"layoutMode"}},{"kind":"Field","name":{"kind":"Name","value":"widgets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageLayoutWidgetFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageLayoutId"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageLayoutFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageLayout"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"objectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"defaultTabToFocusOnMobileAndSidePanelId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"tabs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageLayoutTabFragment"}}]}}]}}]} as unknown as DocumentNode;
diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts
index 0c57d8b6f1..23f2802e55 100644
--- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts
+++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts
@@ -118,6 +118,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.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 },
@@ -128,6 +129,7 @@ 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.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 },
@@ -138,6 +140,7 @@ const testCases: {
{ 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 },
@@ -148,6 +151,7 @@ const testCases: {
{ 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.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 },
@@ -158,6 +162,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.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 },
@@ -170,6 +175,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.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 },
@@ -180,6 +186,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.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 },
@@ -190,6 +197,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.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 },
@@ -200,6 +208,7 @@ 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.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 },
@@ -210,6 +219,7 @@ const testCases: {
{ 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.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 },
@@ -220,6 +230,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.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 },
@@ -230,6 +241,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.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 },
@@ -240,6 +252,7 @@ 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.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 },
@@ -250,6 +263,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.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 },
@@ -260,6 +274,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.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 },
@@ -270,6 +285,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.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 },
@@ -280,6 +296,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.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 },
@@ -290,6 +307,7 @@ 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.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 },
@@ -302,6 +320,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.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 },
@@ -312,6 +331,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.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 },
@@ -326,6 +346,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.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 },
@@ -336,6 +357,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.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 },
@@ -346,6 +368,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.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 },
@@ -356,6 +379,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.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 },
@@ -366,6 +390,7 @@ 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.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 },
@@ -519,6 +544,23 @@ describe('usePageChangeEffectNavigateLocation — onboarding V2', () => {
);
});
+ 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,
diff --git a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts
index b49493717d..8d848684aa 100644
--- a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts
+++ b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts
@@ -146,6 +146,13 @@ export const usePageChangeEffectNavigateLocation = () => {
return isOnboardingV2 ? AppPath.SyncEmailsV2 : AppPath.SyncEmails;
}
+ if (
+ onboardingStatus === OnboardingStatus.APPS_INSTALLATION &&
+ !isMatchingLocation(location, AppPath.InstallAppsV2)
+ ) {
+ return AppPath.InstallAppsV2;
+ }
+
if (
onboardingStatus === OnboardingStatus.INVITE_TEAM &&
!someMatchingLocationOf([AppPath.InviteTeam, AppPath.InviteTeamV2])
diff --git a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx
index 1ed6014281..b18e0c58bd 100644
--- a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx
+++ b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx
@@ -93,6 +93,12 @@ const SyncEmailsV2 = lazy(() =>
})),
);
+const InstallAppsV2 = lazy(() =>
+ import('~/pages/onboarding/InstallAppsV2').then((module) => ({
+ default: module.InstallAppsV2,
+ })),
+);
+
const InviteTeam = lazy(() =>
import('~/pages/onboarding/InviteTeam').then((module) => ({
default: module.InviteTeam,
@@ -338,6 +344,14 @@ export const useCreateAppRouter = (
}
/>
+
+
+
+ }
+ />
void;
+};
+
+export const InstallAppsAutoSkipEffect = ({
+ onError,
+}: InstallAppsAutoSkipEffectProps) => {
+ const triggerInstallAppsOnboardingStep =
+ useTriggerInstallAppsOnboardingStep();
+
+ // oxlint-disable-next-line twenty/no-state-useref
+ const hasSkippedRef = useRef(false);
+
+ useEffect(() => {
+ if (hasSkippedRef.current) {
+ return;
+ }
+ hasSkippedRef.current = true;
+
+ const skip = async () => {
+ try {
+ await triggerInstallAppsOnboardingStep([]);
+ } catch {
+ onError();
+ }
+ };
+
+ void skip();
+ }, [triggerInstallAppsOnboardingStep, onError]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/onboarding/graphql/mutations/triggerInstallAppsOnboardingStep.ts b/packages/twenty-front/src/modules/onboarding/graphql/mutations/triggerInstallAppsOnboardingStep.ts
new file mode 100644
index 0000000000..ab7b289423
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/graphql/mutations/triggerInstallAppsOnboardingStep.ts
@@ -0,0 +1,11 @@
+import { gql } from '@apollo/client';
+
+export const TRIGGER_INSTALL_APPS_ONBOARDING_STEP = gql`
+ mutation TriggerInstallAppsOnboardingStep($universalIdentifiers: [String!]!) {
+ triggerInstallAppsOnboardingStep(
+ universalIdentifiers: $universalIdentifiers
+ ) {
+ success
+ }
+ }
+`;
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useInstallOnboardingApps.test.ts b/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useInstallOnboardingApps.test.ts
new file mode 100644
index 0000000000..668db402f6
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useInstallOnboardingApps.test.ts
@@ -0,0 +1,162 @@
+import { act, renderHook } from '@testing-library/react';
+import { Provider as JotaiProvider } from 'jotai';
+import { createElement } from 'react';
+
+import { onboardingConfigState } from '@/client-config/states/onboardingConfigState';
+import { type OnboardingConfig } from '@/client-config/types/OnboardingConfig';
+import { useInstallOnboardingApps } from '@/onboarding/hooks/useInstallOnboardingApps';
+import { onboardingFreeCreditsState } from '@/onboarding/states/onboardingFreeCreditsState';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
+import {
+ jotaiStore,
+ resetJotaiStore,
+} from '@/ui/utilities/state/jotai/jotaiStore';
+
+const mockTriggerInstallAppsOnboardingStep = jest.fn();
+
+jest.mock('@/onboarding/hooks/useTriggerInstallAppsOnboardingStep', () => ({
+ useTriggerInstallAppsOnboardingStep: () =>
+ mockTriggerInstallAppsOnboardingStep,
+}));
+
+const onboardingConfig: OnboardingConfig = {
+ importContactsCreditsReward: 2,
+ inviteTeamMaxCreditsReward: 9,
+ inviteTeamCreditsRewardPerUser: 3,
+ upgradeCreditsReward: 5,
+ installAppsCreditsRewardPerApp: 1,
+};
+
+const Wrapper = ({ children }: { children: React.ReactNode }) =>
+ createElement(JotaiProvider, { store: jotaiStore }, children);
+
+const renderInstallHook = () => {
+ const { result } = renderHook(
+ () => {
+ const setOnboardingConfig = useSetAtomState(onboardingConfigState);
+ const onboardingFreeCredits = useAtomStateValue(
+ onboardingFreeCreditsState,
+ );
+ const installOnboardingApps = useInstallOnboardingApps();
+
+ return {
+ setOnboardingConfig,
+ onboardingFreeCredits,
+ installOnboardingApps,
+ };
+ },
+ { wrapper: Wrapper },
+ );
+
+ act(() => {
+ result.current.setOnboardingConfig(onboardingConfig);
+ });
+
+ return result;
+};
+
+describe('useInstallOnboardingApps', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ resetJotaiStore();
+ mockTriggerInstallAppsOnboardingStep.mockReset();
+ });
+
+ it('should credit the selected apps once the step succeeds', async () => {
+ mockTriggerInstallAppsOnboardingStep.mockResolvedValue(undefined);
+
+ const result = renderInstallHook();
+
+ act(() => {
+ result.current.installOnboardingApps.toggleApp('app-1');
+ });
+ act(() => {
+ result.current.installOnboardingApps.toggleApp('app-2');
+ });
+
+ await act(async () => {
+ await result.current.installOnboardingApps.installSelectedAppsAndContinue();
+ });
+
+ expect(mockTriggerInstallAppsOnboardingStep).toHaveBeenCalledWith([
+ 'app-1',
+ 'app-2',
+ ]);
+ expect(result.current.onboardingFreeCredits.installApps).toBe(2);
+ });
+
+ it('should reset the completing state and not credit when the step fails', async () => {
+ mockTriggerInstallAppsOnboardingStep.mockRejectedValue(
+ new Error('network error'),
+ );
+
+ const result = renderInstallHook();
+
+ act(() => {
+ result.current.installOnboardingApps.toggleApp('app-1');
+ });
+
+ await act(async () => {
+ await result.current.installOnboardingApps.installSelectedAppsAndContinue();
+ });
+
+ expect(result.current.installOnboardingApps.isCompleting).toBe(false);
+ expect(result.current.onboardingFreeCredits.installApps).toBe(0);
+ });
+
+ it('should allow retrying after a failed attempt', async () => {
+ mockTriggerInstallAppsOnboardingStep
+ .mockRejectedValueOnce(new Error('network error'))
+ .mockResolvedValueOnce(undefined);
+
+ const result = renderInstallHook();
+
+ act(() => {
+ result.current.installOnboardingApps.toggleApp('app-1');
+ });
+
+ await act(async () => {
+ await result.current.installOnboardingApps.installSelectedAppsAndContinue();
+ });
+
+ await act(async () => {
+ await result.current.installOnboardingApps.installSelectedAppsAndContinue();
+ });
+
+ expect(mockTriggerInstallAppsOnboardingStep).toHaveBeenCalledTimes(2);
+ expect(result.current.onboardingFreeCredits.installApps).toBe(1);
+ });
+
+ it('should ignore a second submission while one is already in flight', async () => {
+ let resolveTrigger: () => void = () => {};
+
+ mockTriggerInstallAppsOnboardingStep.mockReturnValue(
+ new Promise((resolve) => {
+ resolveTrigger = resolve;
+ }),
+ );
+
+ const result = renderInstallHook();
+
+ act(() => {
+ result.current.installOnboardingApps.toggleApp('app-1');
+ });
+
+ act(() => {
+ void result.current.installOnboardingApps.installSelectedAppsAndContinue();
+ });
+
+ expect(result.current.installOnboardingApps.isCompleting).toBe(true);
+
+ act(() => {
+ void result.current.installOnboardingApps.skip();
+ });
+
+ expect(mockTriggerInstallAppsOnboardingStep).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ resolveTrigger();
+ });
+ });
+});
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 604c0575ba..a8c0d50b9c 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
@@ -93,8 +93,15 @@ describe('useSetNextOnboardingStatus', () => {
expect(nextOnboardingStatus).toEqual(OnboardingStatus.SYNC_EMAIL);
});
- it('should create profile after syncing emails', () => {
+ it('should install apps after syncing emails', () => {
const nextOnboardingStatus = renderHooks(OnboardingStatus.SYNC_EMAIL);
+ expect(nextOnboardingStatus).toEqual(OnboardingStatus.APPS_INSTALLATION);
+ });
+
+ it('should create profile after installing apps', () => {
+ const nextOnboardingStatus = renderHooks(
+ OnboardingStatus.APPS_INSTALLATION,
+ );
expect(nextOnboardingStatus).toEqual(OnboardingStatus.PROFILE_CREATION);
});
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useInstallOnboardingApps.ts b/packages/twenty-front/src/modules/onboarding/hooks/useInstallOnboardingApps.ts
new file mode 100644
index 0000000000..80352d85bb
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/hooks/useInstallOnboardingApps.ts
@@ -0,0 +1,55 @@
+import { onboardingConfigState } from '@/client-config/states/onboardingConfigState';
+import { useTriggerInstallAppsOnboardingStep } from '@/onboarding/hooks/useTriggerInstallAppsOnboardingStep';
+import { onboardingFreeCreditsState } from '@/onboarding/states/onboardingFreeCreditsState';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
+import { useState } from 'react';
+
+export const useInstallOnboardingApps = () => {
+ const onboardingConfig = useAtomStateValue(onboardingConfigState);
+ const setOnboardingFreeCredits = useSetAtomState(onboardingFreeCreditsState);
+ const triggerInstallAppsOnboardingStep =
+ useTriggerInstallAppsOnboardingStep();
+
+ const [selectedUniversalIdentifiers, setSelectedUniversalIdentifiers] =
+ useState([]);
+ const [isCompleting, setIsCompleting] = useState(false);
+
+ const toggleApp = (universalIdentifier: string) => {
+ setSelectedUniversalIdentifiers((current) =>
+ current.includes(universalIdentifier)
+ ? current.filter((identifier) => identifier !== universalIdentifier)
+ : [...current, universalIdentifier],
+ );
+ };
+
+ const triggerStep = async (universalIdentifiers: string[]) => {
+ if (isCompleting) {
+ return;
+ }
+ setIsCompleting(true);
+
+ try {
+ await triggerInstallAppsOnboardingStep(universalIdentifiers);
+
+ const creditsRewardPerApp =
+ onboardingConfig?.installAppsCreditsRewardPerApp ?? 0;
+
+ setOnboardingFreeCredits((current) => ({
+ ...current,
+ installApps: creditsRewardPerApp * universalIdentifiers.length,
+ }));
+ } catch {
+ setIsCompleting(false);
+ }
+ };
+
+ return {
+ selectedUniversalIdentifiers,
+ isCompleting,
+ toggleApp,
+ installSelectedAppsAndContinue: () =>
+ triggerStep(selectedUniversalIdentifiers),
+ skip: () => triggerStep([]),
+ };
+};
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useInviteTeam.ts b/packages/twenty-front/src/modules/onboarding/hooks/useInviteTeam.ts
index f815bcf050..eb733449b9 100644
--- a/packages/twenty-front/src/modules/onboarding/hooks/useInviteTeam.ts
+++ b/packages/twenty-front/src/modules/onboarding/hooks/useInviteTeam.ts
@@ -150,15 +150,10 @@ export const useInviteTeam = () => {
const creditsRewardPerUser =
onboardingConfig?.inviteTeamCreditsRewardPerUser ?? 0;
- const maxCreditsReward =
- onboardingConfig?.inviteTeamMaxCreditsReward ?? 0;
setOnboardingFreeCredits((current) => ({
...current,
- inviteTeam: Math.min(
- emails.length * creditsRewardPerUser,
- maxCreditsReward,
- ),
+ inviteTeam: emails.length * creditsRewardPerUser,
}));
if (emails.length > 0) {
@@ -175,7 +170,6 @@ export const useInviteTeam = () => {
[
enqueueSuccessSnackBar,
onboardingConfig?.inviteTeamCreditsRewardPerUser,
- onboardingConfig?.inviteTeamMaxCreditsReward,
sendInvitation,
setNextOnboardingStatus,
setOnboardingFreeCredits,
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useOnboardingFreeCreditsTotal.ts b/packages/twenty-front/src/modules/onboarding/hooks/useOnboardingFreeCreditsTotal.ts
index 51fb750147..5bb7f257e1 100644
--- a/packages/twenty-front/src/modules/onboarding/hooks/useOnboardingFreeCreditsTotal.ts
+++ b/packages/twenty-front/src/modules/onboarding/hooks/useOnboardingFreeCreditsTotal.ts
@@ -5,6 +5,8 @@ export const useOnboardingFreeCreditsTotal = () => {
const onboardingFreeCredits = useAtomStateValue(onboardingFreeCreditsState);
return (
- onboardingFreeCredits.importContacts + onboardingFreeCredits.inviteTeam
+ onboardingFreeCredits.importContacts +
+ onboardingFreeCredits.inviteTeam +
+ onboardingFreeCredits.installApps
);
};
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts b/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts
index 5da62ea45f..d9fb8cb818 100644
--- a/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts
+++ b/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts
@@ -38,6 +38,10 @@ const getNextOnboardingStatus = ({
}
if (currentUser?.onboardingStatus === OnboardingStatus.SYNC_EMAIL) {
+ return OnboardingStatus.APPS_INSTALLATION;
+ }
+
+ if (currentUser?.onboardingStatus === OnboardingStatus.APPS_INSTALLATION) {
return OnboardingStatus.PROFILE_CREATION;
}
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useTriggerInstallAppsOnboardingStep.ts b/packages/twenty-front/src/modules/onboarding/hooks/useTriggerInstallAppsOnboardingStep.ts
new file mode 100644
index 0000000000..26b6beb7ba
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/hooks/useTriggerInstallAppsOnboardingStep.ts
@@ -0,0 +1,21 @@
+import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
+import { useMutation } from '@apollo/client/react';
+import { useCallback } from 'react';
+import { TriggerInstallAppsOnboardingStepDocument } from '~/generated-metadata/graphql';
+
+export const useTriggerInstallAppsOnboardingStep = () => {
+ const setNextOnboardingStatus = useSetNextOnboardingStatus();
+ const [triggerInstallAppsOnboardingStep] = useMutation(
+ TriggerInstallAppsOnboardingStepDocument,
+ );
+
+ return useCallback(
+ async (universalIdentifiers: string[]) => {
+ await triggerInstallAppsOnboardingStep({
+ variables: { universalIdentifiers },
+ });
+ setNextOnboardingStatus();
+ },
+ [triggerInstallAppsOnboardingStep, setNextOnboardingStatus],
+ );
+};
diff --git a/packages/twenty-front/src/modules/onboarding/states/onboardingFreeCreditsState.ts b/packages/twenty-front/src/modules/onboarding/states/onboardingFreeCreditsState.ts
index 6292d2afef..9b5eb8a4e7 100644
--- a/packages/twenty-front/src/modules/onboarding/states/onboardingFreeCreditsState.ts
+++ b/packages/twenty-front/src/modules/onboarding/states/onboardingFreeCreditsState.ts
@@ -4,10 +4,11 @@ import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomStat
export const onboardingFreeCreditsState =
createAtomState({
key: 'onboardingFreeCreditsState',
- defaultValue: { importContacts: 0, inviteTeam: 0 },
+ defaultValue: { importContacts: 0, inviteTeam: 0, installApps: 0 },
useLocalStorage: true,
localStorageOptions: { getOnInit: true },
validateInitFn: (payload) =>
Number.isFinite(payload.importContacts) &&
- Number.isFinite(payload.inviteTeam),
+ Number.isFinite(payload.inviteTeam) &&
+ Number.isFinite(payload.installApps),
});
diff --git a/packages/twenty-front/src/modules/onboarding/types/OnboardingFreeCredits.ts b/packages/twenty-front/src/modules/onboarding/types/OnboardingFreeCredits.ts
index 05fa6a4648..ebee5c331e 100644
--- a/packages/twenty-front/src/modules/onboarding/types/OnboardingFreeCredits.ts
+++ b/packages/twenty-front/src/modules/onboarding/types/OnboardingFreeCredits.ts
@@ -1,4 +1,5 @@
export type OnboardingFreeCredits = {
importContacts: number;
inviteTeam: number;
+ installApps: number;
};
diff --git a/packages/twenty-front/src/modules/onboarding/types/OnboardingInstallableApp.ts b/packages/twenty-front/src/modules/onboarding/types/OnboardingInstallableApp.ts
new file mode 100644
index 0000000000..4b12f83087
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/types/OnboardingInstallableApp.ts
@@ -0,0 +1,7 @@
+import { type MessageDescriptor } from '@lingui/core';
+
+export type OnboardingInstallableApp = {
+ universalIdentifier: string;
+ label: MessageDescriptor;
+ description: MessageDescriptor;
+};
diff --git a/packages/twenty-front/src/pages/onboarding/InstallApps.tsx b/packages/twenty-front/src/pages/onboarding/InstallApps.tsx
new file mode 100644
index 0000000000..536ad231f0
--- /dev/null
+++ b/packages/twenty-front/src/pages/onboarding/InstallApps.tsx
@@ -0,0 +1,234 @@
+import { OnboardingCreditsRewardTag } from '@/onboarding/components/import-contacts/OnboardingCreditsRewardTag';
+import { type OnboardingInstallableApp } from '@/onboarding/types/OnboardingInstallableApp';
+import { styled } from '@linaria/react';
+import { useLingui } from '@lingui/react/macro';
+import { isDefined } from 'twenty-shared/utils';
+import { Avatar } from 'twenty-ui/data-display';
+import { IconCheck, IconPlus } from 'twenty-ui/icon';
+import { IconButton, MainButton } from 'twenty-ui/input';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
+
+const CONTENT_BLOCK_WIDTH = 340;
+
+const StyledContent = styled.div`
+ align-items: center;
+ background-color: ${themeCssVariables.background.secondary};
+ box-sizing: border-box;
+ display: flex;
+ flex: 1 1 0;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[14]};
+ min-height: 0;
+ overflow-y: auto;
+ padding: ${themeCssVariables.spacing[16]} ${themeCssVariables.spacing[8]};
+ width: 100%;
+`;
+
+const StyledHeading = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[4]};
+ width: ${CONTENT_BLOCK_WIDTH}px;
+`;
+
+const StyledTitleRow = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[2]};
+`;
+
+const StyledTitle = styled.h1`
+ color: ${themeCssVariables.font.color.primary};
+ font-size: ${themeCssVariables.font.size.xl};
+ font-weight: ${themeCssVariables.font.weight.semiBold};
+ margin: 0;
+`;
+
+const StyledBetaTag = styled.span`
+ align-items: center;
+ background-color: ${themeCssVariables.background.tertiary};
+ border: 1px solid ${themeCssVariables.border.color.light};
+ border-radius: ${themeCssVariables.border.radius.pill};
+ box-sizing: border-box;
+ color: ${themeCssVariables.font.color.tertiary};
+ display: flex;
+ font-size: ${themeCssVariables.font.size.sm};
+ font-weight: ${themeCssVariables.font.weight.medium};
+ height: ${themeCssVariables.spacing[6]};
+ padding: 0 ${themeCssVariables.spacing[2]};
+`;
+
+const StyledSubtitle = styled.p`
+ color: ${themeCssVariables.font.color.secondary};
+ font-size: ${themeCssVariables.font.size.md};
+ margin: 0;
+`;
+
+const StyledCreditsRow = styled.div`
+ display: flex;
+ padding-top: ${themeCssVariables.spacing[1]};
+`;
+
+const StyledCard = styled.div`
+ background-color: ${themeCssVariables.background.primary};
+ border: 1px solid ${themeCssVariables.border.color.medium};
+ border-radius: ${themeCssVariables.border.radius.md};
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ width: ${CONTENT_BLOCK_WIDTH}px;
+`;
+
+const StyledAppRow = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[3]};
+ padding: ${themeCssVariables.spacing[4]};
+
+ & + & {
+ border-top: 1px solid ${themeCssVariables.border.color.light};
+ }
+`;
+
+const StyledAppText = styled.div`
+ display: flex;
+ flex: 1 1 0;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[1]};
+ min-width: 0;
+`;
+
+const StyledAppLabel = styled.span`
+ color: ${themeCssVariables.font.color.primary};
+ font-size: ${themeCssVariables.font.size.md};
+ font-weight: ${themeCssVariables.font.weight.medium};
+`;
+
+const StyledAppDescription = styled.span`
+ color: ${themeCssVariables.font.color.tertiary};
+ font-size: ${themeCssVariables.font.size.sm};
+`;
+
+const StyledFooter = styled.div`
+ align-items: flex-end;
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[4]};
+ width: ${CONTENT_BLOCK_WIDTH}px;
+`;
+
+const StyledInstallButton = styled.div`
+ width: 100%;
+`;
+
+const StyledSkipButton = styled.button`
+ background-color: transparent;
+ border: 1px solid ${themeCssVariables.border.color.light};
+ border-radius: ${themeCssVariables.border.radius.md};
+ color: ${themeCssVariables.font.color.tertiary};
+ cursor: pointer;
+ font-family: ${themeCssVariables.font.family};
+ font-size: ${themeCssVariables.font.size.md};
+ font-weight: ${themeCssVariables.font.weight.semiBold};
+ height: ${themeCssVariables.spacing[8]};
+ padding: 0 ${themeCssVariables.spacing[5]};
+`;
+
+type InstallAppsProps = {
+ apps: (OnboardingInstallableApp & { logo: string | null })[];
+ selectedUniversalIdentifiers: string[];
+ creditsRewardPerApp?: number;
+ isCompleting: boolean;
+ onToggleApp: (universalIdentifier: string) => void;
+ onInstall: () => void;
+ onSkip: () => void;
+};
+
+export const InstallApps = ({
+ apps,
+ selectedUniversalIdentifiers,
+ creditsRewardPerApp,
+ isCompleting,
+ onToggleApp,
+ onInstall,
+ onSkip,
+}: InstallAppsProps) => {
+ const { t } = useLingui();
+
+ return (
+
+
+
+ {t`Install your first apps`}
+ {t`Beta`}
+
+
+ {t`Get the most out of your CRM by installing some apps`}
+
+ {isDefined(creditsRewardPerApp) && (
+
+
+
+ )}
+
+
+
+ {apps.map((app) => {
+ const labelText = t(app.label);
+ const isSelected = selectedUniversalIdentifiers.includes(
+ app.universalIdentifier,
+ );
+
+ return (
+
+
+
+ {labelText}
+
+ {t(app.description)}
+
+
+ onToggleApp(app.universalIdentifier)}
+ />
+
+ );
+ })}
+
+
+
+
+
+
+
+ {t`Skip`}
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/onboarding/InstallAppsV2.tsx b/packages/twenty-front/src/pages/onboarding/InstallAppsV2.tsx
new file mode 100644
index 0000000000..346dc3f947
--- /dev/null
+++ b/packages/twenty-front/src/pages/onboarding/InstallAppsV2.tsx
@@ -0,0 +1,56 @@
+import { isOnboardingV2State } from '@/auth/states/isOnboardingV2State';
+import { onboardingConfigState } from '@/client-config/states/onboardingConfigState';
+import { useMarketplaceApps } from '@/marketplace/hooks/useMarketplaceApps';
+import { OnboardingV2Layout } from '@/onboarding/components/OnboardingV2Layout';
+import { ONBOARDING_INSTALLABLE_APPS } from '@/onboarding/constants/OnboardingInstallableApps';
+import { InstallAppsAutoSkipEffect } from '@/onboarding/effect-components/InstallAppsAutoSkipEffect';
+import { useInstallOnboardingApps } from '@/onboarding/hooks/useInstallOnboardingApps';
+import { useOnboardingFreeCreditsTotal } from '@/onboarding/hooks/useOnboardingFreeCreditsTotal';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useCallback, useState } from 'react';
+import { InstallApps } from '~/pages/onboarding/InstallApps';
+
+export const InstallAppsV2 = () => {
+ const isOnboardingV2 = useAtomStateValue(isOnboardingV2State);
+ const { data: marketplaceApps } = useMarketplaceApps();
+ const onboardingConfig = useAtomStateValue(onboardingConfigState);
+ const freeCreditsTotal = useOnboardingFreeCreditsTotal();
+ const [hasAutoSkipFailed, setHasAutoSkipFailed] = useState(false);
+ const {
+ selectedUniversalIdentifiers,
+ isCompleting,
+ toggleApp,
+ installSelectedAppsAndContinue,
+ skip,
+ } = useInstallOnboardingApps();
+
+ const handleAutoSkipError = useCallback(() => {
+ setHasAutoSkipFailed(true);
+ }, []);
+
+ if (!isOnboardingV2 && !hasAutoSkipFailed) {
+ return ;
+ }
+
+ const apps = ONBOARDING_INSTALLABLE_APPS.map((app) => ({
+ ...app,
+ logo:
+ marketplaceApps.find(
+ (marketplaceApp) => marketplaceApp.id === app.universalIdentifier,
+ )?.logo ?? null,
+ }));
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/onboarding/WorkspaceActivationV2.tsx b/packages/twenty-front/src/pages/onboarding/WorkspaceActivationV2.tsx
index b2dc4b6b43..789cf51fc3 100644
--- a/packages/twenty-front/src/pages/onboarding/WorkspaceActivationV2.tsx
+++ b/packages/twenty-front/src/pages/onboarding/WorkspaceActivationV2.tsx
@@ -94,7 +94,11 @@ export const WorkspaceActivationV2 = () => {
}
hasTriggeredRef.current = true;
- setOnboardingFreeCredits({ importContacts: 0, inviteTeam: 0 });
+ setOnboardingFreeCredits({
+ importContacts: 0,
+ inviteTeam: 0,
+ installApps: 0,
+ });
void activate();
}, [activate, currentWorkspace, setOnboardingFreeCredits]);
diff --git a/packages/twenty-front/src/pages/onboarding/__stories__/InstallAppsV2.stories.tsx b/packages/twenty-front/src/pages/onboarding/__stories__/InstallAppsV2.stories.tsx
new file mode 100644
index 0000000000..2cd3425553
--- /dev/null
+++ b/packages/twenty-front/src/pages/onboarding/__stories__/InstallAppsV2.stories.tsx
@@ -0,0 +1,67 @@
+import { getOperationName } from '~/utils/getOperationName';
+import { type Meta, type StoryObj } from '@storybook/react-vite';
+import { HttpResponse, graphql } from 'msw';
+import { within } from 'storybook/test';
+import { AppPath } from 'twenty-shared/types';
+
+import { isOnboardingV2State } from '@/auth/states/isOnboardingV2State';
+import { FIND_MANY_MARKETPLACE_APPS } from '@/marketplace/graphql/queries/findManyMarketplaceApps';
+import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
+import { OnboardingStatus } from '~/generated-metadata/graphql';
+import { GET_CURRENT_USER } from '~/modules/users/graphql/queries/getCurrentUser';
+import { InstallAppsV2 } from '~/pages/onboarding/InstallAppsV2';
+import {
+ PageDecorator,
+ type PageDecoratorArgs,
+} from '~/testing/decorators/PageDecorator';
+import { graphqlMocks } from '~/testing/graphqlMocks';
+import { mockedOnboardingUserData } from '~/testing/mock-data/users';
+
+const meta: Meta = {
+ title: 'Pages/Onboarding/InstallAppsV2',
+ component: InstallAppsV2,
+ decorators: [
+ (Story) => {
+ jotaiStore.set(isOnboardingV2State.atom, true);
+
+ return ;
+ },
+ PageDecorator,
+ ],
+ args: { routePath: AppPath.InstallAppsV2 },
+ parameters: {
+ msw: {
+ handlers: [
+ graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
+ return HttpResponse.json({
+ data: {
+ currentUser: mockedOnboardingUserData(
+ OnboardingStatus.APPS_INSTALLATION,
+ ),
+ },
+ });
+ }),
+ graphql.query(
+ getOperationName(FIND_MANY_MARKETPLACE_APPS) ?? '',
+ () => {
+ return HttpResponse.json({
+ data: { findManyMarketplaceApps: [] },
+ });
+ },
+ ),
+ graphqlMocks.handlers,
+ ],
+ },
+ },
+};
+
+export default meta;
+
+export type Story = StoryObj;
+
+export const Default: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement.ownerDocument.body);
+ await canvas.findByText('Install your first apps');
+ },
+};
diff --git a/packages/twenty-front/src/testing/constants/UntestedAppPaths.ts b/packages/twenty-front/src/testing/constants/UntestedAppPaths.ts
index 506ac47763..8bfe768b14 100644
--- a/packages/twenty-front/src/testing/constants/UntestedAppPaths.ts
+++ b/packages/twenty-front/src/testing/constants/UntestedAppPaths.ts
@@ -7,6 +7,7 @@ export const UNTESTED_APP_PATHS = [
AppPath.WorkspaceActivationV2,
AppPath.CreateProfileV2,
AppPath.SyncEmailsV2,
+ AppPath.InstallAppsV2,
AppPath.InviteTeamV2,
AppPath.PlanRequiredV2,
// Public, unauthenticated redirect route handled in useCreateAppRouter — not
diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts
index f535dec1e8..3bbe5494f4 100644
--- a/packages/twenty-front/src/testing/mock-data/config.ts
+++ b/packages/twenty-front/src/testing/mock-data/config.ts
@@ -49,6 +49,7 @@ export const mockedClientConfig: ClientConfig = {
inviteTeamMaxCreditsReward: 9,
inviteTeamCreditsRewardPerUser: 3,
upgradeCreditsReward: 5,
+ installAppsCreditsRewardPerApp: 1,
},
canManageFeatureFlags: true,
publicFeatureFlags: [],
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts
index e804b06d9e..ec4932d725 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts
@@ -383,6 +383,15 @@ export class SignInUpService {
},
queryRunner,
);
+
+ await this.onboardingService.setOnboardingInstallAppsPending(
+ {
+ userId: user.id,
+ workspaceId: workspace.id,
+ value: true,
+ },
+ queryRunner,
+ );
}
private async saveNewUser(
diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts
index 16b761f5ce..55dcaf3236 100644
--- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts
@@ -89,6 +89,7 @@ describe('ClientConfigController', () => {
importContactsCreditsReward: 2,
inviteTeamCreditsRewardPerUser: 3,
upgradeCreditsReward: 5,
+ installAppsCreditsRewardPerApp: 1,
},
isAttachmentPreviewEnabled: true,
analyticsEnabled: false,
diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts
index 35588a763a..f15c30668e 100644
--- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts
+++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts
@@ -214,6 +214,8 @@ export class OnboardingConfig {
inviteTeamCreditsRewardPerUser: number;
upgradeCreditsReward: number;
+
+ installAppsCreditsRewardPerApp: number;
}
@ObjectType()
diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts
index b38463f728..d47c7cf63b 100644
--- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts
@@ -95,6 +95,7 @@ describe('ClientConfigService', () => {
ONBOARDING_IMPORT_CONTACTS_CREDITS_REWARD: 2_000_000,
ONBOARDING_INVITE_TEAM_CREDITS_REWARD_PER_USER: 3_000_000,
BILLING_FREE_WORKFLOW_CREDITS_FOR_TRIAL_PERIOD_WITH_CREDIT_CARD: 5_000_000,
+ ONBOARDING_INSTALL_APPS_CREDITS_REWARD_PER_APP: 1_000_000,
IS_ATTACHMENT_PREVIEW_ENABLED: true,
ANALYTICS_ENABLED: true,
MESSAGING_PROVIDER_MICROSOFT_ENABLED: false,
@@ -172,6 +173,7 @@ describe('ClientConfigService', () => {
importContactsCreditsReward: 2,
inviteTeamCreditsRewardPerUser: 3,
upgradeCreditsReward: 5,
+ installAppsCreditsRewardPerApp: 1,
},
isAttachmentPreviewEnabled: true,
analyticsEnabled: true,
diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts
index e020db84b0..f6ccfd0cc4 100644
--- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts
@@ -235,6 +235,11 @@ export class ClientConfigService {
'BILLING_FREE_WORKFLOW_CREDITS_FOR_TRIAL_PERIOD_WITH_CREDIT_CARD',
),
),
+ installAppsCreditsRewardPerApp: toDisplayCredits(
+ this.twentyConfigService.get(
+ 'ONBOARDING_INSTALL_APPS_CREDITS_REWARD_PER_APP',
+ ),
+ ),
},
isAttachmentPreviewEnabled: this.twentyConfigService.get(
'IS_ATTACHMENT_PREVIEW_ENABLED',
diff --git a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts
index 36667c9686..e9794ce912 100644
--- a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts
@@ -12,8 +12,12 @@ import { UpdateSubscriptionQuantityJob } from 'src/engine/core-modules/billing/j
import { BillingReminderModule } from 'src/engine/core-modules/billing/reminders/billing-reminder.module';
import { BillingReminderCronJob } from 'src/engine/core-modules/billing/reminders/crons/billing-reminder.cron.job';
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
+import { ApplicationInstallModule } from 'src/engine/core-modules/application/application-install/application-install.module';
+import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
import { BackfillApplicationInstallationJob } from 'src/engine/core-modules/application/jobs/backfill-application-installation.job';
import { PreInstalledAppsModule } from 'src/engine/core-modules/application/pre-installed-apps/pre-installed-apps.module';
+import { InstallOnboardingAppsJob } from 'src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job';
+import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { EmailSenderJob } from 'src/engine/core-modules/email/email-sender.job';
import { EmailModule } from 'src/engine/core-modules/email/email.module';
import { EmailingModule } from 'src/modules/emailing/emailing.module';
@@ -88,6 +92,9 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
EnterpriseModule,
EmailingModule,
PreInstalledAppsModule,
+ ApplicationInstallModule,
+ ApplicationRegistrationModule,
+ OnboardingModule,
BillingReminderModule,
],
providers: [
@@ -103,6 +110,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
UpdateWorkspaceMemberEmailJob,
GenerateSdkClientJob,
BackfillApplicationInstallationJob,
+ InstallOnboardingAppsJob,
],
})
export class JobsModule {
diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/constants/onboarding-installable-app-universal-identifiers.ts b/packages/twenty-server/src/engine/core-modules/onboarding/constants/onboarding-installable-app-universal-identifiers.ts
new file mode 100644
index 0000000000..73ce42c6ae
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/onboarding/constants/onboarding-installable-app-universal-identifiers.ts
@@ -0,0 +1,4 @@
+export const ONBOARDING_INSTALLABLE_APP_UNIVERSAL_IDENTIFIERS = [
+ '8da4b8b5-5edf-4880-b51f-ab6e679ec617',
+ '4a1178c1-3535-4a47-b592-231d3216b36f',
+];
diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/enums/onboarding-status.enum.ts b/packages/twenty-server/src/engine/core-modules/onboarding/enums/onboarding-status.enum.ts
index dd4370a52e..e407186103 100644
--- a/packages/twenty-server/src/engine/core-modules/onboarding/enums/onboarding-status.enum.ts
+++ b/packages/twenty-server/src/engine/core-modules/onboarding/enums/onboarding-status.enum.ts
@@ -3,6 +3,7 @@ export enum OnboardingStatus {
WORKSPACE_ACTIVATION = 'WORKSPACE_ACTIVATION',
PROFILE_CREATION = 'PROFILE_CREATION',
SYNC_EMAIL = 'SYNC_EMAIL',
+ APPS_INSTALLATION = 'APPS_INSTALLATION',
INVITE_TEAM = 'INVITE_TEAM',
BOOK_ONBOARDING = 'BOOK_ONBOARDING',
COMPLETED = 'COMPLETED',
diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job-constants.ts b/packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job-constants.ts
new file mode 100644
index 0000000000..56ef4fe816
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job-constants.ts
@@ -0,0 +1,6 @@
+export const INSTALL_ONBOARDING_APPS_JOB_NAME = 'InstallOnboardingAppsJob';
+
+export type InstallOnboardingAppsJobData = {
+ workspaceId: string;
+ universalIdentifiers: string[];
+};
diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job.spec.ts b/packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job.spec.ts
new file mode 100644
index 0000000000..6e54ebc343
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job.spec.ts
@@ -0,0 +1,158 @@
+import { Test, type TestingModule } from '@nestjs/testing';
+
+import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
+import { type ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
+import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
+import { InstallOnboardingAppsJob } from 'src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job';
+import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
+
+describe('InstallOnboardingAppsJob', () => {
+ let job: InstallOnboardingAppsJob;
+ let applicationRegistrationService: ApplicationRegistrationService;
+ let applicationInstallService: ApplicationInstallService;
+ let onboardingService: OnboardingService;
+
+ const workspaceId = 'workspace-id';
+ const callRecorderId = 'call-recorder-uid';
+ const peopleDataLabsId = 'people-data-labs-uid';
+
+ const buildRegistration = (id: string) =>
+ ({ id }) as ApplicationRegistrationEntity;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ InstallOnboardingAppsJob,
+ {
+ provide: ApplicationRegistrationService,
+ useValue: {
+ findOneByUniversalIdentifier: jest.fn(),
+ },
+ },
+ {
+ provide: ApplicationInstallService,
+ useValue: {
+ installApplication: jest.fn(),
+ },
+ },
+ {
+ provide: OnboardingService,
+ useValue: {
+ creditInstallAppsReward: jest.fn(),
+ },
+ },
+ ],
+ }).compile();
+
+ job = module.get(InstallOnboardingAppsJob);
+ applicationRegistrationService = module.get(
+ ApplicationRegistrationService,
+ );
+ applicationInstallService = module.get(
+ ApplicationInstallService,
+ );
+ onboardingService = module.get(OnboardingService);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should credit the reward for every requested app and install them', async () => {
+ jest
+ .spyOn(applicationRegistrationService, 'findOneByUniversalIdentifier')
+ .mockImplementation(async (universalIdentifier) =>
+ buildRegistration(`registration-${universalIdentifier}`),
+ );
+ jest
+ .spyOn(applicationInstallService, 'installApplication')
+ .mockResolvedValue(true);
+
+ await job.handle({
+ workspaceId,
+ universalIdentifiers: [callRecorderId, peopleDataLabsId],
+ });
+
+ expect(onboardingService.creditInstallAppsReward).toHaveBeenCalledWith({
+ workspaceId,
+ rewardAppsCount: 2,
+ });
+ expect(applicationInstallService.installApplication).toHaveBeenCalledTimes(
+ 2,
+ );
+ });
+
+ it('should credit before attempting the installs', async () => {
+ jest
+ .spyOn(applicationRegistrationService, 'findOneByUniversalIdentifier')
+ .mockImplementation(async (universalIdentifier) =>
+ buildRegistration(`registration-${universalIdentifier}`),
+ );
+ jest
+ .spyOn(applicationInstallService, 'installApplication')
+ .mockResolvedValue(true);
+
+ await job.handle({
+ workspaceId,
+ universalIdentifiers: [callRecorderId],
+ });
+
+ const creditOrder = (onboardingService.creditInstallAppsReward as jest.Mock)
+ .mock.invocationCallOrder[0];
+ const installOrder = (
+ applicationInstallService.installApplication as jest.Mock
+ ).mock.invocationCallOrder[0];
+
+ expect(creditOrder).toBeLessThan(installOrder);
+ });
+
+ it('should credit even when an install fails', async () => {
+ jest
+ .spyOn(applicationRegistrationService, 'findOneByUniversalIdentifier')
+ .mockImplementation(async (universalIdentifier) =>
+ buildRegistration(`registration-${universalIdentifier}`),
+ );
+ jest
+ .spyOn(applicationInstallService, 'installApplication')
+ .mockRejectedValue(new Error('install failure'));
+
+ await job.handle({
+ workspaceId,
+ universalIdentifiers: [callRecorderId, peopleDataLabsId],
+ });
+
+ expect(onboardingService.creditInstallAppsReward).toHaveBeenCalledWith({
+ workspaceId,
+ rewardAppsCount: 2,
+ });
+ expect(applicationInstallService.installApplication).toHaveBeenCalledTimes(
+ 2,
+ );
+ });
+
+ it('should skip installing apps whose registration cannot be found but still credit the full count', async () => {
+ jest
+ .spyOn(applicationRegistrationService, 'findOneByUniversalIdentifier')
+ .mockImplementation(async (universalIdentifier) =>
+ universalIdentifier === callRecorderId
+ ? null
+ : buildRegistration(`registration-${universalIdentifier}`),
+ );
+ jest
+ .spyOn(applicationInstallService, 'installApplication')
+ .mockResolvedValue(true);
+
+ await job.handle({
+ workspaceId,
+ universalIdentifiers: [callRecorderId, peopleDataLabsId],
+ });
+
+ expect(onboardingService.creditInstallAppsReward).toHaveBeenCalledWith({
+ workspaceId,
+ rewardAppsCount: 2,
+ });
+ expect(applicationInstallService.installApplication).toHaveBeenCalledTimes(
+ 1,
+ );
+ });
+});
diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job.ts b/packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job.ts
new file mode 100644
index 0000000000..d9df18ae45
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job.ts
@@ -0,0 +1,73 @@
+import { Logger } from '@nestjs/common';
+
+import { isDefined } from 'twenty-shared/utils';
+
+import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
+import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
+import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
+import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
+import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
+import {
+ INSTALL_ONBOARDING_APPS_JOB_NAME,
+ type InstallOnboardingAppsJobData,
+} from 'src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job-constants';
+import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
+
+@Processor(MessageQueue.workspaceQueue)
+export class InstallOnboardingAppsJob {
+ private readonly logger = new Logger(InstallOnboardingAppsJob.name);
+
+ constructor(
+ private readonly applicationRegistrationService: ApplicationRegistrationService,
+ private readonly applicationInstallService: ApplicationInstallService,
+ private readonly onboardingService: OnboardingService,
+ ) {}
+
+ @Process(INSTALL_ONBOARDING_APPS_JOB_NAME)
+ async handle({
+ workspaceId,
+ universalIdentifiers,
+ }: InstallOnboardingAppsJobData): Promise {
+ await this.onboardingService.creditInstallAppsReward({
+ workspaceId,
+ rewardAppsCount: universalIdentifiers.length,
+ });
+
+ for (const universalIdentifier of universalIdentifiers) {
+ await this.installApp({ universalIdentifier, workspaceId });
+ }
+ }
+
+ private async installApp({
+ universalIdentifier,
+ workspaceId,
+ }: {
+ universalIdentifier: string;
+ workspaceId: string;
+ }): Promise {
+ try {
+ const registration =
+ await this.applicationRegistrationService.findOneByUniversalIdentifier(
+ universalIdentifier,
+ );
+
+ if (!isDefined(registration)) {
+ this.logger.error(
+ `Onboarding app ${universalIdentifier} not found while installing for workspace ${workspaceId}`,
+ );
+
+ return;
+ }
+
+ await this.applicationInstallService.installApplication({
+ appRegistrationId: registration.id,
+ workspaceId,
+ });
+ } catch (error) {
+ this.logger.error(
+ `Failed to install onboarding app ${universalIdentifier} for workspace ${workspaceId}`,
+ error,
+ );
+ }
+ }
+}
diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts
index 5c9a9b08dd..895ee19b00 100644
--- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts
+++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts
@@ -1,5 +1,5 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
-import { Mutation, Query } from '@nestjs/graphql';
+import { Args, Mutation, Query } from '@nestjs/graphql';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
@@ -68,4 +68,21 @@ export class OnboardingResolver {
return { success: true };
}
+
+ @Mutation(() => OnboardingStepSuccessDTO)
+ @UseGuards(NoPermissionGuard)
+ async triggerInstallAppsOnboardingStep(
+ @AuthUser() user: AuthContextUser,
+ @AuthWorkspace() workspace: WorkspaceEntity,
+ @Args({ name: 'universalIdentifiers', type: () => [String] })
+ universalIdentifiers: string[],
+ ): Promise {
+ await this.onboardingService.triggerInstallAppsOnboardingStep({
+ userId: user.id,
+ workspaceId: workspace.id,
+ universalIdentifiers,
+ });
+
+ return { success: true };
+ }
}
diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.spec.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.spec.ts
index 9f61719a7c..7a1178d52a 100644
--- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.spec.ts
@@ -5,6 +5,11 @@ import { Repository } from 'typeorm';
import { BillingCreditService } from 'src/engine/core-modules/billing/services/billing-credit.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
+import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
+import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
+import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
+import { ONBOARDING_INSTALLABLE_APP_UNIVERSAL_IDENTIFIERS } from 'src/engine/core-modules/onboarding/constants/onboarding-installable-app-universal-identifiers';
+import { INSTALL_ONBOARDING_APPS_JOB_NAME } from 'src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job-constants';
import {
OnboardingService,
OnboardingStepKeys,
@@ -18,6 +23,7 @@ describe('OnboardingService', () => {
let userVarsService: UserVarsService;
let billingCreditService: BillingCreditService;
let twentyConfigService: TwentyConfigService;
+ let messageQueueService: MessageQueueService;
const userId = 'user-id';
const workspaceId = 'workspace-id';
@@ -56,6 +62,12 @@ describe('OnboardingService', () => {
provide: getRepositoryToken(WorkspaceEntity),
useClass: Repository,
},
+ {
+ provide: getQueueToken(MessageQueue.workspaceQueue),
+ useValue: {
+ add: jest.fn(),
+ },
+ },
],
}).compile();
@@ -64,6 +76,9 @@ describe('OnboardingService', () => {
billingCreditService =
module.get(BillingCreditService);
twentyConfigService = module.get(TwentyConfigService);
+ messageQueueService = module.get(
+ getQueueToken(MessageQueue.workspaceQueue),
+ );
});
afterEach(() => {
@@ -136,4 +151,91 @@ describe('OnboardingService', () => {
).resolves.not.toThrow();
});
});
+
+ describe('triggerInstallAppsOnboardingStep', () => {
+ const [callRecorderId, peopleDataLabsId] =
+ ONBOARDING_INSTALLABLE_APP_UNIVERSAL_IDENTIFIERS;
+
+ it('should claim the step and enqueue the install job for the installable apps without crediting', async () => {
+ jest.spyOn(userVarsService, 'delete').mockResolvedValue(1);
+
+ await service.triggerInstallAppsOnboardingStep({
+ userId,
+ workspaceId,
+ universalIdentifiers: [callRecorderId, peopleDataLabsId],
+ });
+
+ expect(userVarsService.delete).toHaveBeenCalledWith({
+ userId,
+ workspaceId,
+ key: OnboardingStepKeys.ONBOARDING_INSTALL_APPS_PENDING,
+ });
+ expect(messageQueueService.add).toHaveBeenCalledWith(
+ INSTALL_ONBOARDING_APPS_JOB_NAME,
+ {
+ workspaceId,
+ universalIdentifiers: [callRecorderId, peopleDataLabsId],
+ },
+ { id: `${INSTALL_ONBOARDING_APPS_JOB_NAME}-${workspaceId}` },
+ );
+ expect(
+ billingCreditService.creditWorkspaceBalance,
+ ).not.toHaveBeenCalled();
+ });
+
+ it('should not enqueue anything when the step was already consumed', async () => {
+ jest.spyOn(userVarsService, 'delete').mockResolvedValue(0);
+
+ await service.triggerInstallAppsOnboardingStep({
+ userId,
+ workspaceId,
+ universalIdentifiers: [callRecorderId],
+ });
+
+ expect(messageQueueService.add).not.toHaveBeenCalled();
+ });
+
+ it('should claim the step but not enqueue when no installable app was selected', async () => {
+ jest.spyOn(userVarsService, 'delete').mockResolvedValue(1);
+
+ await service.triggerInstallAppsOnboardingStep({
+ userId,
+ workspaceId,
+ universalIdentifiers: ['00000000-0000-0000-0000-000000000000'],
+ });
+
+ expect(userVarsService.delete).toHaveBeenCalled();
+ expect(messageQueueService.add).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('creditInstallAppsReward', () => {
+ it('should credit the reward per installed app', async () => {
+ jest.spyOn(twentyConfigService, 'get').mockReturnValue(1_000_000);
+
+ await service.creditInstallAppsReward({
+ workspaceId,
+ rewardAppsCount: 2,
+ });
+
+ expect(billingCreditService.creditWorkspaceBalance).toHaveBeenCalledWith({
+ workspaceId,
+ amountMicro: 2_000_000,
+ });
+ });
+
+ it('should not throw when crediting fails', async () => {
+ jest.spyOn(twentyConfigService, 'get').mockReturnValue(1_000_000);
+ jest
+ .spyOn(billingCreditService, 'creditWorkspaceBalance')
+ .mockRejectedValue(new Error('billing failure'));
+
+ await expect(
+ service.creditInstallAppsReward({
+ workspaceId,
+ rewardAppsCount: 1,
+ }),
+ ).resolves.not.toThrow();
+ });
+ });
});
diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts
index 9fa16d1bb5..ac4d90c01d 100644
--- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts
@@ -8,7 +8,15 @@ import { type QueryRunner, Repository } from 'typeorm';
import { BillingCreditService } from 'src/engine/core-modules/billing/services/billing-credit.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
+import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
+import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
+import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
+import { ONBOARDING_INSTALLABLE_APP_UNIVERSAL_IDENTIFIERS } from 'src/engine/core-modules/onboarding/constants/onboarding-installable-app-universal-identifiers';
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
+import {
+ INSTALL_ONBOARDING_APPS_JOB_NAME,
+ type InstallOnboardingAppsJobData,
+} from 'src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job-constants';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
@@ -19,6 +27,7 @@ export enum OnboardingStepKeys {
ONBOARDING_INVITE_TEAM_PENDING = 'ONBOARDING_INVITE_TEAM_PENDING',
ONBOARDING_CREATE_PROFILE_PENDING = 'ONBOARDING_CREATE_PROFILE_PENDING',
ONBOARDING_BOOK_ONBOARDING_PENDING = 'ONBOARDING_BOOK_ONBOARDING_PENDING',
+ ONBOARDING_INSTALL_APPS_PENDING = 'ONBOARDING_INSTALL_APPS_PENDING',
}
export type OnboardingKeyValueTypeMap = {
@@ -26,6 +35,7 @@ export type OnboardingKeyValueTypeMap = {
[OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING]: boolean;
[OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING]: boolean;
[OnboardingStepKeys.ONBOARDING_BOOK_ONBOARDING_PENDING]: boolean;
+ [OnboardingStepKeys.ONBOARDING_INSTALL_APPS_PENDING]: boolean;
};
@Injectable()
@@ -39,6 +49,8 @@ export class OnboardingService {
private readonly twentyConfigService: TwentyConfigService,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository,
+ @InjectMessageQueue(MessageQueue.workspaceQueue)
+ private readonly messageQueueService: MessageQueueService,
) {}
private isWorkspaceActivationPending(workspace: WorkspaceEntity) {
@@ -85,6 +97,9 @@ export class OnboardingService {
userVars.get(OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING) ===
true;
+ const isInstallAppsPending =
+ userVars.get(OnboardingStepKeys.ONBOARDING_INSTALL_APPS_PENDING) === true;
+
const isInviteTeamPending =
userVars.get(OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING) === true;
@@ -96,6 +111,10 @@ export class OnboardingService {
return OnboardingStatus.SYNC_EMAIL;
}
+ if (isInstallAppsPending) {
+ return OnboardingStatus.APPS_INSTALLATION;
+ }
+
if (isProfileCreationPending) {
return OnboardingStatus.PROFILE_CREATION;
}
@@ -237,6 +256,116 @@ export class OnboardingService {
}
}
+ async setOnboardingInstallAppsPending(
+ {
+ userId,
+ workspaceId,
+ value,
+ }: {
+ userId: string;
+ workspaceId: string;
+ value: boolean;
+ },
+ queryRunner?: QueryRunner,
+ ) {
+ if (!value) {
+ await this.userVarsService.delete(
+ {
+ userId,
+ workspaceId,
+ key: OnboardingStepKeys.ONBOARDING_INSTALL_APPS_PENDING,
+ },
+ queryRunner,
+ );
+
+ return;
+ }
+
+ await this.userVarsService.set(
+ {
+ userId,
+ workspaceId,
+ key: OnboardingStepKeys.ONBOARDING_INSTALL_APPS_PENDING,
+ value: true,
+ },
+ queryRunner,
+ );
+ }
+
+ async triggerInstallAppsOnboardingStep({
+ userId,
+ workspaceId,
+ universalIdentifiers,
+ }: {
+ userId: string;
+ workspaceId: string;
+ universalIdentifiers: string[];
+ }) {
+ const hasClaimedInstallAppsStep = await this.claimInstallAppsOnboardingStep(
+ { userId, workspaceId },
+ );
+
+ if (!hasClaimedInstallAppsStep) {
+ return;
+ }
+
+ const installableUniversalIdentifiers = universalIdentifiers.filter(
+ (universalIdentifier) =>
+ ONBOARDING_INSTALLABLE_APP_UNIVERSAL_IDENTIFIERS.includes(
+ universalIdentifier,
+ ),
+ );
+
+ if (installableUniversalIdentifiers.length === 0) {
+ return;
+ }
+
+ await this.messageQueueService.add(
+ INSTALL_ONBOARDING_APPS_JOB_NAME,
+ { workspaceId, universalIdentifiers: installableUniversalIdentifiers },
+ { id: `${INSTALL_ONBOARDING_APPS_JOB_NAME}-${workspaceId}` },
+ );
+ }
+
+ private async claimInstallAppsOnboardingStep({
+ userId,
+ workspaceId,
+ }: {
+ userId: string;
+ workspaceId: string;
+ }): Promise {
+ const affectedRows = await this.userVarsService.delete({
+ userId,
+ workspaceId,
+ key: OnboardingStepKeys.ONBOARDING_INSTALL_APPS_PENDING,
+ });
+
+ return isDefined(affectedRows) && affectedRows > 0;
+ }
+
+ async creditInstallAppsReward({
+ workspaceId,
+ rewardAppsCount,
+ }: {
+ workspaceId: string;
+ rewardAppsCount: number;
+ }) {
+ try {
+ await this.billingCreditService.creditWorkspaceBalance({
+ workspaceId,
+ amountMicro:
+ this.twentyConfigService.get(
+ 'ONBOARDING_INSTALL_APPS_CREDITS_REWARD_PER_APP',
+ ) * rewardAppsCount,
+ });
+ } catch (error) {
+ this.logger.error(
+ `Failed to credit onboarding install-apps reward for workspace ${workspaceId}`,
+ error,
+ );
+ }
+ }
+
async setOnboardingInviteTeamPending(
{
workspaceId,
diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts
index 27a38b9eb1..7e95bbfaa5 100644
--- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts
+++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts
@@ -966,6 +966,17 @@ export class ConfigVariables {
@IsOptional()
ONBOARDING_INVITE_TEAM_CREDITS_REWARD_PER_USER = 500_000;
+ @ConfigVariablesMetadata({
+ group: ConfigVariablesGroup.BILLING_CONFIG,
+ description:
+ 'Free credits granted per app installed during the install-apps onboarding step (in microCredits)',
+ type: ConfigVariableType.NUMBER,
+ })
+ @CastToPositiveNumber()
+ @IsInt()
+ @IsOptional()
+ ONBOARDING_INSTALL_APPS_CREDITS_REWARD_PER_APP = 1_000_000;
+
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SERVER_CONFIG,
description: 'Url for the frontend application',
diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/update-workspace-member-settings-onboarding.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/update-workspace-member-settings-onboarding.integration-spec.ts
index fa30663ec0..b4954e1905 100644
--- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/update-workspace-member-settings-onboarding.integration-spec.ts
+++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/update-workspace-member-settings-onboarding.integration-spec.ts
@@ -74,8 +74,27 @@ describe('updateWorkspaceMemberSettings and profile onboarding', () => {
it('should clear PROFILE_CREATION onboarding after saving first and last name via updateWorkspaceMemberSettings', async () => {
const uniqueEmail = `profile-onboarding-${randomUUID()}@example.com`;
- newUserAccessToken =
- await signUpInWorkspaceAndGetAccessToken(uniqueEmail);
+ newUserAccessToken = await signUpInWorkspaceAndGetAccessToken(uniqueEmail);
+
+ const triggerInstallAppsOnboardingStepMutation = gql`
+ mutation TriggerInstallAppsOnboardingStep(
+ $universalIdentifiers: [String!]!
+ ) {
+ triggerInstallAppsOnboardingStep(
+ universalIdentifiers: $universalIdentifiers
+ ) {
+ success
+ }
+ }
+ `;
+
+ await makeMetadataAPIRequest(
+ {
+ query: triggerInstallAppsOnboardingStepMutation,
+ variables: { universalIdentifiers: [] },
+ },
+ newUserAccessToken,
+ );
const currentUserWithOnboardingQuery = gql`
query CurrentUserWithOnboarding {
diff --git a/packages/twenty-shared/src/types/AppPath.ts b/packages/twenty-shared/src/types/AppPath.ts
index 28d06f54e5..41faebc143 100644
--- a/packages/twenty-shared/src/types/AppPath.ts
+++ b/packages/twenty-shared/src/types/AppPath.ts
@@ -15,6 +15,7 @@ export enum AppPath {
CreateProfileV2 = '/create/profile-v2',
SyncEmails = '/sync/emails',
SyncEmailsV2 = '/sync/emails-v2',
+ InstallAppsV2 = '/install-apps-v2',
InviteTeam = '/invite-team',
InviteTeamV2 = '/invite-team-v2',
PlanRequired = '/plan-required',