5095 move onboardingstatus computation from frontend to backend (#5954)

- move front `onboardingStatus` computing to server side
- add logic to `useSetNextOnboardingStatus`
- update some missing redirections in
`usePageChangeEffectNavigateLocation`
- separate subscriptionStatus from onboardingStatus
This commit is contained in:
martmull
2024-06-28 17:32:02 +02:00
committed by GitHub
parent 1a66db5bff
commit b8f33f6f59
78 changed files with 1767 additions and 1763 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/Sn
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { App } from '~/App'; import { App } from '~/App';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedUsersData } from '~/testing/mock-data/users'; import { mockedUserData } from '~/testing/mock-data/users';
const meta: Meta<typeof App> = { const meta: Meta<typeof App> = {
title: 'App/App', title: 'App/App',
@@ -67,9 +67,9 @@ export const DarkMode: Story = {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: { currentUser: {
...mockedUsersData[0], ...mockedUserData,
workspaceMember: { workspaceMember: {
...mockedUsersData[0].workspaceMember, ...mockedUserData.workspaceMember,
colorScheme: 'Dark', colorScheme: 'Dark',
}, },
}, },

View File

@@ -36,6 +36,11 @@ export type Analytics = {
success: Scalars['Boolean']['output']; success: Scalars['Boolean']['output'];
}; };
export type ApiConfig = {
__typename?: 'ApiConfig';
mutationMaximumAffectedRecords: Scalars['Float']['output'];
};
export type ApiKeyToken = { export type ApiKeyToken = {
__typename?: 'ApiKeyToken'; __typename?: 'ApiKeyToken';
token: Scalars['String']['output']; token: Scalars['String']['output'];
@@ -98,8 +103,8 @@ export type Billing = {
export type BillingSubscription = { export type BillingSubscription = {
__typename?: 'BillingSubscription'; __typename?: 'BillingSubscription';
id: Scalars['UUID']['output']; id: Scalars['UUID']['output'];
interval?: Maybe<Scalars['String']['output']>; interval?: Maybe<SubscriptionInterval>;
status: Scalars['String']['output']; status: SubscriptionStatus;
}; };
export type BillingSubscriptionFilter = { export type BillingSubscriptionFilter = {
@@ -142,6 +147,7 @@ export enum CaptchaDriverType {
export type ClientConfig = { export type ClientConfig = {
__typename?: 'ClientConfig'; __typename?: 'ClientConfig';
api: ApiConfig;
authProviders: AuthProviders; authProviders: AuthProviders;
billing: Billing; billing: Billing;
captcha: Captcha; captcha: Captcha;
@@ -398,7 +404,9 @@ export type Mutation = {
deleteOneRelation: Relation; deleteOneRelation: Relation;
deleteOneRemoteServer: RemoteServer; deleteOneRemoteServer: RemoteServer;
deleteUser: User; deleteUser: User;
disablePostgresProxy: PostgresCredentials;
emailPasswordResetLink: EmailPasswordResetLink; emailPasswordResetLink: EmailPasswordResetLink;
enablePostgresProxy: PostgresCredentials;
exchangeAuthorizationCode: ExchangeAuthCode; exchangeAuthorizationCode: ExchangeAuthCode;
generateApiKeyToken: ApiKeyToken; generateApiKeyToken: ApiKeyToken;
generateJWT: AuthTokens; generateJWT: AuthTokens;
@@ -451,7 +459,7 @@ export type MutationChallengeArgs = {
export type MutationCheckoutSessionArgs = { export type MutationCheckoutSessionArgs = {
recurringInterval: Scalars['String']['input']; recurringInterval: SubscriptionInterval;
successUrlPath?: InputMaybe<Scalars['String']['input']>; successUrlPath?: InputMaybe<Scalars['String']['input']>;
}; };
@@ -636,10 +644,14 @@ export type ObjectFieldsConnection = {
pageInfo: PageInfo; pageInfo: PageInfo;
}; };
/** Onboarding step */ /** Onboarding status */
export enum OnboardingStep { export enum OnboardingStatus {
Completed = 'COMPLETED',
InviteTeam = 'INVITE_TEAM', InviteTeam = 'INVITE_TEAM',
SyncEmail = 'SYNC_EMAIL' PlanRequired = 'PLAN_REQUIRED',
ProfileCreation = 'PROFILE_CREATION',
SyncEmail = 'SYNC_EMAIL',
WorkspaceActivation = 'WORKSPACE_ACTIVATION'
} }
export type OnboardingStepSuccess = { export type OnboardingStepSuccess = {
@@ -660,10 +672,18 @@ export type PageInfo = {
startCursor?: Maybe<Scalars['ConnectionCursor']['output']>; startCursor?: Maybe<Scalars['ConnectionCursor']['output']>;
}; };
export type PostgresCredentials = {
__typename?: 'PostgresCredentials';
id: Scalars['UUID']['output'];
password: Scalars['String']['output'];
user: Scalars['String']['output'];
workspaceId: Scalars['String']['output'];
};
export type ProductPriceEntity = { export type ProductPriceEntity = {
__typename?: 'ProductPriceEntity'; __typename?: 'ProductPriceEntity';
created: Scalars['Float']['output']; created: Scalars['Float']['output'];
recurringInterval: Scalars['String']['output']; recurringInterval: SubscriptionInterval;
stripePriceId: Scalars['String']['output']; stripePriceId: Scalars['String']['output'];
unitAmount: Scalars['Float']['output']; unitAmount: Scalars['Float']['output'];
}; };
@@ -688,6 +708,7 @@ export type Query = {
findManyRemoteServersByType: Array<RemoteServer>; findManyRemoteServersByType: Array<RemoteServer>;
findOneRemoteServerById: RemoteServer; findOneRemoteServerById: RemoteServer;
findWorkspaceFromInviteHash: Workspace; findWorkspaceFromInviteHash: Workspace;
getPostgresCredentials?: Maybe<PostgresCredentials>;
getProductPrices: ProductPricesEntity; getProductPrices: ProductPricesEntity;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
@@ -912,6 +933,24 @@ export enum SortNulls {
NullsLast = 'NULLS_LAST' NullsLast = 'NULLS_LAST'
} }
export enum SubscriptionInterval {
Day = 'Day',
Month = 'Month',
Week = 'Week',
Year = 'Year'
}
export enum SubscriptionStatus {
Active = 'Active',
Canceled = 'Canceled',
Incomplete = 'Incomplete',
IncompleteExpired = 'IncompleteExpired',
PastDue = 'PastDue',
Paused = 'Paused',
Trialing = 'Trialing',
Unpaid = 'Unpaid'
}
export type Support = { export type Support = {
__typename?: 'Support'; __typename?: 'Support';
supportDriver: Scalars['String']['output']; supportDriver: Scalars['String']['output'];
@@ -1084,7 +1123,7 @@ export type User = {
firstName: Scalars['String']['output']; firstName: Scalars['String']['output'];
id: Scalars['UUID']['output']; id: Scalars['UUID']['output'];
lastName: Scalars['String']['output']; lastName: Scalars['String']['output'];
onboardingStep?: Maybe<OnboardingStep>; onboardingStatus?: Maybe<OnboardingStatus>;
passwordHash?: Maybe<Scalars['String']['output']>; passwordHash?: Maybe<Scalars['String']['output']>;
/** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */ /** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
passwordResetToken?: Maybe<Scalars['String']['output']>; passwordResetToken?: Maybe<Scalars['String']['output']>;
@@ -1163,7 +1202,6 @@ export type Workspace = {
id: Scalars['UUID']['output']; id: Scalars['UUID']['output'];
inviteHash?: Maybe<Scalars['String']['output']>; inviteHash?: Maybe<Scalars['String']['output']>;
logo?: Maybe<Scalars['String']['output']>; logo?: Maybe<Scalars['String']['output']>;
subscriptionStatus: Scalars['String']['output'];
updatedAt: Scalars['DateTime']['output']; updatedAt: Scalars['DateTime']['output'];
}; };

View File

@@ -97,8 +97,8 @@ export type Billing = {
export type BillingSubscription = { export type BillingSubscription = {
__typename?: 'BillingSubscription'; __typename?: 'BillingSubscription';
id: Scalars['UUID']; id: Scalars['UUID'];
interval?: Maybe<Scalars['String']>; interval?: Maybe<SubscriptionInterval>;
status: Scalars['String']; status: SubscriptionStatus;
}; };
export type BillingSubscriptionFilter = { export type BillingSubscriptionFilter = {
@@ -347,7 +347,7 @@ export type MutationChallengeArgs = {
export type MutationCheckoutSessionArgs = { export type MutationCheckoutSessionArgs = {
recurringInterval: Scalars['String']; recurringInterval: SubscriptionInterval;
successUrlPath?: InputMaybe<Scalars['String']>; successUrlPath?: InputMaybe<Scalars['String']>;
}; };
@@ -467,10 +467,14 @@ export type ObjectFieldsConnection = {
pageInfo: PageInfo; pageInfo: PageInfo;
}; };
/** Onboarding step */ /** Onboarding status */
export enum OnboardingStep { export enum OnboardingStatus {
Completed = 'COMPLETED',
InviteTeam = 'INVITE_TEAM', InviteTeam = 'INVITE_TEAM',
SyncEmail = 'SYNC_EMAIL' PlanRequired = 'PLAN_REQUIRED',
ProfileCreation = 'PROFILE_CREATION',
SyncEmail = 'SYNC_EMAIL',
WorkspaceActivation = 'WORKSPACE_ACTIVATION'
} }
export type OnboardingStepSuccess = { export type OnboardingStepSuccess = {
@@ -502,7 +506,7 @@ export type PostgresCredentials = {
export type ProductPriceEntity = { export type ProductPriceEntity = {
__typename?: 'ProductPriceEntity'; __typename?: 'ProductPriceEntity';
created: Scalars['Float']; created: Scalars['Float'];
recurringInterval: Scalars['String']; recurringInterval: SubscriptionInterval;
stripePriceId: Scalars['String']; stripePriceId: Scalars['String'];
unitAmount: Scalars['Float']; unitAmount: Scalars['Float'];
}; };
@@ -684,6 +688,24 @@ export enum SortNulls {
NullsLast = 'NULLS_LAST' NullsLast = 'NULLS_LAST'
} }
export enum SubscriptionInterval {
Day = 'Day',
Month = 'Month',
Week = 'Week',
Year = 'Year'
}
export enum SubscriptionStatus {
Active = 'Active',
Canceled = 'Canceled',
Incomplete = 'Incomplete',
IncompleteExpired = 'IncompleteExpired',
PastDue = 'PastDue',
Paused = 'Paused',
Trialing = 'Trialing',
Unpaid = 'Unpaid'
}
export type Support = { export type Support = {
__typename?: 'Support'; __typename?: 'Support';
supportDriver: Scalars['String']; supportDriver: Scalars['String'];
@@ -827,7 +849,7 @@ export type User = {
firstName: Scalars['String']; firstName: Scalars['String'];
id: Scalars['UUID']; id: Scalars['UUID'];
lastName: Scalars['String']; lastName: Scalars['String'];
onboardingStep?: Maybe<OnboardingStep>; onboardingStatus?: Maybe<OnboardingStatus>;
passwordHash?: Maybe<Scalars['String']>; passwordHash?: Maybe<Scalars['String']>;
/** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */ /** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
passwordResetToken?: Maybe<Scalars['String']>; passwordResetToken?: Maybe<Scalars['String']>;
@@ -896,7 +918,6 @@ export type Workspace = {
id: Scalars['UUID']; id: Scalars['UUID'];
inviteHash?: Maybe<Scalars['String']>; inviteHash?: Maybe<Scalars['String']>;
logo?: Maybe<Scalars['String']>; logo?: Maybe<Scalars['String']>;
subscriptionStatus: Scalars['String'];
updatedAt: Scalars['DateTime']; updatedAt: Scalars['DateTime'];
}; };
@@ -1156,7 +1177,7 @@ export type ImpersonateMutationVariables = Exact<{
}>; }>;
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStep?: OnboardingStep | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type RenewTokenMutationVariables = Exact<{ export type RenewTokenMutationVariables = Exact<{
appToken: Scalars['String']; appToken: Scalars['String'];
@@ -1188,7 +1209,7 @@ export type VerifyMutationVariables = Exact<{
}>; }>;
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStep?: OnboardingStep | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type CheckUserExistsQueryVariables = Exact<{ export type CheckUserExistsQueryVariables = Exact<{
email: Scalars['String']; email: Scalars['String'];
@@ -1213,7 +1234,7 @@ export type BillingPortalSessionQueryVariables = Exact<{
export type BillingPortalSessionQuery = { __typename?: 'Query', billingPortalSession: { __typename?: 'SessionEntity', url?: string | null } }; export type BillingPortalSessionQuery = { __typename?: 'Query', billingPortalSession: { __typename?: 'SessionEntity', url?: string | null } };
export type CheckoutSessionMutationVariables = Exact<{ export type CheckoutSessionMutationVariables = Exact<{
recurringInterval: Scalars['String']; recurringInterval: SubscriptionInterval;
successUrlPath?: InputMaybe<Scalars['String']>; successUrlPath?: InputMaybe<Scalars['String']>;
}>; }>;
@@ -1225,7 +1246,7 @@ export type GetProductPricesQueryVariables = Exact<{
}>; }>;
export type GetProductPricesQuery = { __typename?: 'Query', getProductPrices: { __typename?: 'ProductPricesEntity', productPrices: Array<{ __typename?: 'ProductPriceEntity', created: number, recurringInterval: string, stripePriceId: string, unitAmount: number }> } }; export type GetProductPricesQuery = { __typename?: 'Query', getProductPrices: { __typename?: 'ProductPricesEntity', productPrices: Array<{ __typename?: 'ProductPriceEntity', created: number, recurringInterval: SubscriptionInterval, stripePriceId: string, unitAmount: number }> } };
export type UpdateBillingSubscriptionMutationVariables = Exact<{ [key: string]: never; }>; export type UpdateBillingSubscriptionMutationVariables = Exact<{ [key: string]: never; }>;
@@ -1242,7 +1263,7 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } }; export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStep?: OnboardingStep | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@@ -1259,7 +1280,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStep?: OnboardingStep | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } }; export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
export type AddUserToWorkspaceMutationVariables = Exact<{ export type AddUserToWorkspaceMutationVariables = Exact<{
inviteHash: Scalars['String']; inviteHash: Scalars['String'];
@@ -1292,7 +1313,7 @@ export type UpdateWorkspaceMutationVariables = Exact<{
}>; }>;
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: any, domainName?: string | null, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, subscriptionStatus: string } }; export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: any, domainName?: string | null, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } };
export type UploadWorkspaceLogoMutationVariables = Exact<{ export type UploadWorkspaceLogoMutationVariables = Exact<{
file: Scalars['Upload']; file: Scalars['Upload'];
@@ -1403,7 +1424,7 @@ export const UserQueryFragmentFragmentDoc = gql`
email email
canImpersonate canImpersonate
supportUserHash supportUserHash
onboardingStep onboardingStatus
workspaceMember { workspaceMember {
id id
name { name {
@@ -1421,7 +1442,6 @@ export const UserQueryFragmentFragmentDoc = gql`
domainName domainName
inviteHash inviteHash
allowImpersonation allowImpersonation
subscriptionStatus
activationStatus activationStatus
featureFlags { featureFlags {
id id
@@ -2221,7 +2241,7 @@ export type BillingPortalSessionQueryHookResult = ReturnType<typeof useBillingPo
export type BillingPortalSessionLazyQueryHookResult = ReturnType<typeof useBillingPortalSessionLazyQuery>; export type BillingPortalSessionLazyQueryHookResult = ReturnType<typeof useBillingPortalSessionLazyQuery>;
export type BillingPortalSessionQueryResult = Apollo.QueryResult<BillingPortalSessionQuery, BillingPortalSessionQueryVariables>; export type BillingPortalSessionQueryResult = Apollo.QueryResult<BillingPortalSessionQuery, BillingPortalSessionQueryVariables>;
export const CheckoutSessionDocument = gql` export const CheckoutSessionDocument = gql`
mutation CheckoutSession($recurringInterval: String!, $successUrlPath: String) { mutation CheckoutSession($recurringInterval: SubscriptionInterval!, $successUrlPath: String) {
checkoutSession( checkoutSession(
recurringInterval: $recurringInterval recurringInterval: $recurringInterval
successUrlPath: $successUrlPath successUrlPath: $successUrlPath
@@ -2663,7 +2683,6 @@ export const UpdateWorkspaceDocument = gql`
displayName displayName
logo logo
allowImpersonation allowImpersonation
subscriptionStatus
} }
} }
`; `;

View File

@@ -7,7 +7,7 @@ import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMet
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath'; import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
import { mockedUsersData } from '~/testing/mock-data/users'; import { mockedUserData } from '~/testing/mock-data/users';
const objectMetadataItem = getObjectMetadataItemsMock()[0]; const objectMetadataItem = getObjectMetadataItemsMock()[0];
jest.mock('@/object-metadata/hooks/useObjectMetadataItem'); jest.mock('@/object-metadata/hooks/useObjectMetadataItem');
@@ -36,7 +36,7 @@ const renderHooks = (withCurrentUser: boolean) => {
() => { () => {
const setCurrentUser = useSetRecoilState(currentUserState); const setCurrentUser = useSetRecoilState(currentUserState);
if (withCurrentUser) { if (withCurrentUser) {
setCurrentUser(mockedUsersData[0]); setCurrentUser(mockedUserData);
} }
return useDefaultHomePagePath(); return useDefaultHomePagePath();
}, },

View File

@@ -1,15 +1,26 @@
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus'; import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath'; import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
jest.mock('@/auth/hooks/useOnboardingStatus'); jest.mock('@/onboarding/hooks/useOnboardingStatus');
const setupMockOnboardingStatus = (onboardingStatus: OnboardingStatus) => { const setupMockOnboardingStatus = (
onboardingStatus: OnboardingStatus | undefined,
) => {
jest.mocked(useOnboardingStatus).mockReturnValueOnce(onboardingStatus); jest.mocked(useOnboardingStatus).mockReturnValueOnce(onboardingStatus);
}; };
jest.mock('@/workspace/hooks/useSubscriptionStatus');
const setupMockSubscriptionStatus = (
subscriptionStatus: SubscriptionStatus | undefined,
) => {
jest.mocked(useSubscriptionStatus).mockReturnValueOnce(subscriptionStatus);
};
jest.mock('~/hooks/useIsMatchingLocation'); jest.mock('~/hooks/useIsMatchingLocation');
const mockUseIsMatchingLocation = jest.mocked(useIsMatchingLocation); const mockUseIsMatchingLocation = jest.mocked(useIsMatchingLocation);
@@ -19,281 +30,276 @@ const setupMockIsMatchingLocation = (pathname: string) => {
); );
}; };
jest.mock('@/auth/hooks/useIsLogged');
const setupMockIsLogged = (isLogged: boolean) => {
jest.mocked(useIsLogged).mockReturnValueOnce(isLogged);
};
const defaultHomePagePath = '/objects/companies'; const defaultHomePagePath = '/objects/companies';
jest.mock('~/hooks/useDefaultHomePagePath'); jest.mock('~/hooks/useDefaultHomePagePath');
jest.mocked(useDefaultHomePagePath).mockReturnValue({ jest.mocked(useDefaultHomePagePath).mockReturnValue({
defaultHomePagePath: '/objects/companies', defaultHomePagePath,
}); });
// prettier-ignore // prettier-ignore
const testCases = [ const testCases = [
{ loc: AppPath.Verify, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.Verify, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.Verify, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.Verify, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingUserCreation, res: undefined }, { loc: AppPath.Verify, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.Verify, status: OnboardingStatus.Completed, res: defaultHomePagePath }, { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.Verify, status: OnboardingStatus.CompletedWithoutSubscription, res: defaultHomePagePath },
{ loc: AppPath.SignInUp, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.SignInUp, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.SignInUp, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.SignInUp, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingUserCreation, res: undefined }, { loc: AppPath.SignInUp, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.SignInUp, status: OnboardingStatus.Completed, res: defaultHomePagePath }, { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.SignInUp, status: OnboardingStatus.CompletedWithoutSubscription, res: defaultHomePagePath },
{ loc: AppPath.Invite, status: OnboardingStatus.Incomplete, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined },
{ loc: AppPath.Invite, status: OnboardingStatus.Canceled, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.Invite, status: OnboardingStatus.Unpaid, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.Invite, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingUserCreation, res: undefined }, { loc: AppPath.Invite, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingWorkspaceActivation, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingProfileCreation, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: undefined },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingSyncEmail, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: undefined },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingInviteTeam, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: undefined },
{ loc: AppPath.Invite, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.Invite, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.Incomplete, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.Canceled, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.Unpaid, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingUserCreation, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingWorkspaceActivation, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingProfileCreation, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingSyncEmail, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingInviteTeam, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.CreateWorkspace, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingWorkspaceActivation, res: undefined }, { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.Completed, res: defaultHomePagePath }, { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.CompletedWithoutSubscription, res: defaultHomePagePath },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.CreateProfile, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingProfileCreation, res: undefined }, { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: undefined },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.Completed, res: defaultHomePagePath }, { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.CompletedWithoutSubscription, res: defaultHomePagePath },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.SyncEmails, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingSyncEmail, res: undefined }, { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: undefined },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.Completed, res: defaultHomePagePath }, { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.CompletedWithoutSubscription, res: defaultHomePagePath },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.InviteTeam, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingInviteTeam, res: undefined }, { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: undefined },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Completed, res: defaultHomePagePath }, { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.CompletedWithoutSubscription, res: defaultHomePagePath },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Incomplete, res: undefined }, { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Canceled, res: undefined }, { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Unpaid, res: undefined }, { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.PlanRequired, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Completed, res: defaultHomePagePath }, { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.PlanRequiredSuccess, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingWorkspaceActivation, res: undefined }, { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.Completed, res: defaultHomePagePath }, { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.CompletedWithoutSubscription, res: defaultHomePagePath },
{ loc: AppPath.Index, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.Index, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.Index, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.Index, status: OnboardingStatus.PastDue, res: defaultHomePagePath }, { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.Index, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.Index, status: OnboardingStatus.Completed, res: defaultHomePagePath }, { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath },
{ loc: AppPath.Index, status: OnboardingStatus.CompletedWithoutSubscription, res: defaultHomePagePath },
{ loc: AppPath.TasksPage, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.TasksPage, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.TasksPage, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.TasksPage, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.TasksPage, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.TasksPage, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.TasksPage, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.OpportunitiesPage, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.RecordIndexPage, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.RecordShowPage, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.Canceled, res: undefined }, { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.Unpaid, res: undefined }, { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.SettingsCatchAll, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.Impersonate, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.Impersonate, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.Impersonate, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.Impersonate, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.Impersonate, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.Impersonate, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.Impersonate, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.Authorize, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.Authorize, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.Authorize, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.Authorize, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.Authorize, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.Authorize, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.Authorize, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.NotFoundWildcard, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
{ loc: AppPath.NotFound, status: OnboardingStatus.Incomplete, res: AppPath.PlanRequired }, { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.NotFound, status: OnboardingStatus.Canceled, res: '/settings/billing' }, { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.NotFound, status: OnboardingStatus.Unpaid, res: '/settings/billing' }, { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.NotFound, status: OnboardingStatus.PastDue, res: undefined }, { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingUserCreation, res: AppPath.SignInUp }, { loc: AppPath.NotFound, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingWorkspaceActivation, res: AppPath.CreateWorkspace }, { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingProfileCreation, res: AppPath.CreateProfile }, { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingSyncEmail, res: AppPath.SyncEmails }, { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingInviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.NotFound, status: OnboardingStatus.Completed, res: undefined }, { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
{ loc: AppPath.NotFound, status: OnboardingStatus.CompletedWithoutSubscription, res: undefined },
]; ];
describe('usePageChangeEffectNavigateLocation', () => { describe('usePageChangeEffectNavigateLocation', () => {
testCases.forEach((testCase) => { testCases.forEach((testCase) => {
it(`with location ${testCase.loc} and onboardingStatus ${testCase.status} should return ${testCase.res}`, () => { it(`with location ${testCase.loc} and onboardingStatus ${testCase.onboardingStatus} and subscriptionStatus ${testCase.subscriptionStatus} should return ${testCase.res}`, () => {
setupMockIsMatchingLocation(testCase.loc); setupMockIsMatchingLocation(testCase.loc);
setupMockOnboardingStatus(testCase.status); setupMockOnboardingStatus(testCase.onboardingStatus);
setupMockSubscriptionStatus(testCase.subscriptionStatus);
setupMockIsLogged(testCase.isLoggedIn);
expect(usePageChangeEffectNavigateLocation()).toEqual(testCase.res); expect(usePageChangeEffectNavigateLocation()).toEqual(testCase.res);
}); });
}); });
describe('tests should be exhaustive', () => { describe('tests should be exhaustive', () => {
it('all location and onboarding status should be tested', () => { it('all location and onboarding status should be tested', () => {
const untestedSubscriptionStatus = [
SubscriptionStatus.Incomplete,
SubscriptionStatus.IncompleteExpired,
SubscriptionStatus.Paused,
SubscriptionStatus.Trialing,
];
expect(testCases.length).toEqual( expect(testCases.length).toEqual(
Object.keys(AppPath).length * Object.keys(OnboardingStatus).length, Object.keys(AppPath).length *
(Object.keys(OnboardingStatus).length +
(Object.keys(SubscriptionStatus).length -
untestedSubscriptionStatus.length)),
); );
}); });
}); });

View File

@@ -1,14 +1,18 @@
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus'; import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath'; import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const usePageChangeEffectNavigateLocation = () => { export const usePageChangeEffectNavigateLocation = () => {
const isMatchingLocation = useIsMatchingLocation(); const isMatchingLocation = useIsMatchingLocation();
const isLoggedIn = useIsLogged();
const onboardingStatus = useOnboardingStatus(); const onboardingStatus = useOnboardingStatus();
const subscriptionStatus = useSubscriptionStatus();
const { defaultHomePagePath } = useDefaultHomePagePath(); const { defaultHomePagePath } = useDefaultHomePagePath();
const isMatchingOpenRoute = const isMatchingOpenRoute =
@@ -33,25 +37,28 @@ export const usePageChangeEffectNavigateLocation = () => {
return; return;
} }
if ( if (!isLoggedIn && !isMatchingOngoingUserCreationRoute) {
onboardingStatus === OnboardingStatus.OngoingUserCreation &&
!isMatchingOngoingUserCreationRoute
) {
return AppPath.SignInUp; return AppPath.SignInUp;
} }
if ( if (
onboardingStatus === OnboardingStatus.Incomplete && onboardingStatus === OnboardingStatus.PlanRequired &&
!isMatchingLocation(AppPath.PlanRequired) !isMatchingLocation(AppPath.PlanRequired)
) { ) {
return AppPath.PlanRequired; return AppPath.PlanRequired;
} }
if ( if (
isDefined(onboardingStatus) && subscriptionStatus === SubscriptionStatus.Unpaid &&
[OnboardingStatus.Unpaid, OnboardingStatus.Canceled].includes( !isMatchingLocation(AppPath.SettingsCatchAll)
onboardingStatus, ) {
) && return `${AppPath.SettingsCatchAll.replace('/*', '')}/${
SettingsPath.Billing
}`;
}
if (
subscriptionStatus === SubscriptionStatus.Canceled &&
!( !(
isMatchingLocation(AppPath.SettingsCatchAll) || isMatchingLocation(AppPath.SettingsCatchAll) ||
isMatchingLocation(AppPath.PlanRequired) isMatchingLocation(AppPath.PlanRequired)
@@ -63,7 +70,7 @@ export const usePageChangeEffectNavigateLocation = () => {
} }
if ( if (
onboardingStatus === OnboardingStatus.OngoingWorkspaceActivation && onboardingStatus === OnboardingStatus.WorkspaceActivation &&
!isMatchingLocation(AppPath.CreateWorkspace) && !isMatchingLocation(AppPath.CreateWorkspace) &&
!isMatchingLocation(AppPath.PlanRequiredSuccess) !isMatchingLocation(AppPath.PlanRequiredSuccess)
) { ) {
@@ -71,21 +78,21 @@ export const usePageChangeEffectNavigateLocation = () => {
} }
if ( if (
onboardingStatus === OnboardingStatus.OngoingProfileCreation && onboardingStatus === OnboardingStatus.ProfileCreation &&
!isMatchingLocation(AppPath.CreateProfile) !isMatchingLocation(AppPath.CreateProfile)
) { ) {
return AppPath.CreateProfile; return AppPath.CreateProfile;
} }
if ( if (
onboardingStatus === OnboardingStatus.OngoingSyncEmail && onboardingStatus === OnboardingStatus.SyncEmail &&
!isMatchingLocation(AppPath.SyncEmails) !isMatchingLocation(AppPath.SyncEmails)
) { ) {
return AppPath.SyncEmails; return AppPath.SyncEmails;
} }
if ( if (
onboardingStatus === OnboardingStatus.OngoingInviteTeam && onboardingStatus === OnboardingStatus.InviteTeam &&
!isMatchingLocation(AppPath.InviteTeam) !isMatchingLocation(AppPath.InviteTeam)
) { ) {
return AppPath.InviteTeam; return AppPath.InviteTeam;
@@ -93,15 +100,9 @@ export const usePageChangeEffectNavigateLocation = () => {
if ( if (
onboardingStatus === OnboardingStatus.Completed && onboardingStatus === OnboardingStatus.Completed &&
isMatchingOnboardingRoute
) {
return defaultHomePagePath;
}
if (
onboardingStatus === OnboardingStatus.CompletedWithoutSubscription &&
isMatchingOnboardingRoute && isMatchingOnboardingRoute &&
!isMatchingLocation(AppPath.PlanRequired) subscriptionStatus !== SubscriptionStatus.Canceled &&
(isDefined(subscriptionStatus) || !isMatchingLocation(AppPath.PlanRequired))
) { ) {
return defaultHomePagePath; return defaultHomePagePath;
} }

View File

@@ -14,7 +14,7 @@ import {
} from '~/testing/decorators/PageDecorator'; } from '~/testing/decorators/PageDecorator';
import { graphqlMocks, metadataGraphql } from '~/testing/graphqlMocks'; import { graphqlMocks, metadataGraphql } from '~/testing/graphqlMocks';
import { mockedClientConfig } from '~/testing/mock-data/config'; import { mockedClientConfig } from '~/testing/mock-data/config';
import { mockedUsersData } from '~/testing/mock-data/users'; import { mockedUserData } from '~/testing/mock-data/users';
const userMetadataLoaderMocks = { const userMetadataLoaderMocks = {
msw: { msw: {
@@ -22,7 +22,7 @@ const userMetadataLoaderMocks = {
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: mockedUsersData[0], currentUser: mockedUserData,
}, },
}); });
}), }),

View File

@@ -1,373 +0,0 @@
import { act } from 'react-dom/test-utils';
import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { CurrentUser, currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import {
CurrentWorkspace,
currentWorkspaceState,
} from '@/auth/states/currentWorkspaceState';
import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { billingState } from '@/client-config/states/billingState';
import { OnboardingStep } from '~/generated/graphql';
const tokenPair = {
accessToken: { token: 'accessToken', expiresAt: 'expiresAt' },
refreshToken: { token: 'refreshToken', expiresAt: 'expiresAt' },
};
const billing = {
billingUrl: 'testing.com',
isBillingEnabled: true,
};
const currentUser = {
id: '1',
email: 'test@test',
supportUserHash: '1',
canImpersonate: false,
onboardingStep: null,
} as CurrentUser;
const currentWorkspace = {
activationStatus: 'active',
id: '1',
allowImpersonation: true,
currentBillingSubscription: {
status: 'trialing',
},
} as CurrentWorkspace;
const currentWorkspaceMember = {
id: '1',
locale: '',
name: {
firstName: '',
lastName: '',
},
};
const renderHooks = () => {
const { result } = renderHook(
() => {
const onboardingStatus = useOnboardingStatus();
const setBilling = useSetRecoilState(billingState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const setCurrentUser = useSetRecoilState(currentUserState);
const setTokenPair = useSetRecoilState(tokenPairState);
const setVerifyPending = useSetRecoilState(isVerifyPendingState);
return {
onboardingStatus,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
setTokenPair,
setVerifyPending,
};
},
{
wrapper: RecoilRoot,
},
);
return { result };
};
describe('useOnboardingStatus', () => {
it('should return "ongoing_user_creation" when user is not logged in', async () => {
const { result } = renderHooks();
expect(result.current.onboardingStatus).toBe('ongoing_user_creation');
});
it('should return "incomplete"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'incomplete',
});
setCurrentWorkspaceMember(currentWorkspaceMember);
});
expect(result.current.onboardingStatus).toBe('incomplete');
});
it('should return "canceled"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'canceled',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('canceled');
});
it('should return "ongoing_workspace_activation"', async () => {
const { result } = renderHooks();
const { setTokenPair, setBilling, setCurrentUser, setCurrentWorkspace } =
result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
activationStatus: 'inactive',
subscriptionStatus: 'active',
});
});
expect(result.current.onboardingStatus).toBe(
'ongoing_workspace_activation',
);
});
it('should return "ongoing_profile_creation"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember(currentWorkspaceMember);
});
expect(result.current.onboardingStatus).toBe('ongoing_profile_creation');
});
it('should return "ongoing_sync_email"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser({
...currentUser,
onboardingStep: OnboardingStep.SyncEmail,
});
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('ongoing_sync_email');
});
it('should return "ongoing_invite_team"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser({
...currentUser,
onboardingStep: OnboardingStep.InviteTeam,
});
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('ongoing_invite_team');
});
it('should return "completed"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('completed');
});
it('should return "past_due"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'past_due',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('past_due');
});
it('should return "unpaid"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'unpaid',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('unpaid');
});
it('should return "completed_without_subscription"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'trialing',
currentBillingSubscription: null,
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe(
'completed_without_subscription',
);
});
});

View File

@@ -1,28 +0,0 @@
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { billingState } from '@/client-config/states/billingState';
import { useIsLogged } from '../hooks/useIsLogged';
import {
getOnboardingStatus,
OnboardingStatus,
} from '../utils/getOnboardingStatus';
export const useOnboardingStatus = (): OnboardingStatus | undefined => {
const billing = useRecoilValue(billingState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const currentUser = useRecoilValue(currentUserState);
const isLoggedIn = useIsLogged();
return getOnboardingStatus({
isLoggedIn,
currentWorkspaceMember,
currentWorkspace,
currentUser,
isBillingEnabled: billing?.isBillingEnabled || false,
});
};

View File

@@ -4,7 +4,7 @@ import { User } from '~/generated/graphql';
export type CurrentUser = Pick< export type CurrentUser = Pick<
User, User,
'id' | 'email' | 'supportUserHash' | 'canImpersonate' | 'onboardingStep' 'id' | 'email' | 'supportUserHash' | 'canImpersonate' | 'onboardingStatus'
>; >;
export const currentUserState = createState<CurrentUser | null>({ export const currentUserState = createState<CurrentUser | null>({

View File

@@ -10,7 +10,6 @@ export type CurrentWorkspace = Pick<
| 'displayName' | 'displayName'
| 'allowImpersonation' | 'allowImpersonation'
| 'featureFlags' | 'featureFlags'
| 'subscriptionStatus'
| 'activationStatus' | 'activationStatus'
| 'currentBillingSubscription' | 'currentBillingSubscription'
| 'currentCacheVersion' | 'currentCacheVersion'

View File

@@ -1,174 +0,0 @@
import { CurrentUser } from '@/auth/states/currentUserState';
import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStep } from '~/generated/graphql';
import { getOnboardingStatus } from '../getOnboardingStatus';
describe('getOnboardingStatus', () => {
it('should return the correct status', () => {
const ongoingUserCreation = getOnboardingStatus({
isLoggedIn: false,
currentWorkspaceMember: null,
currentWorkspace: null,
currentUser: null,
isBillingEnabled: false,
});
const ongoingWorkspaceActivation = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: null,
currentWorkspace: {
id: '1',
activationStatus: 'inactive',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
const ongoingProfileCreation = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
const ongoingSyncEmail = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
onboardingStep: OnboardingStep.SyncEmail,
} as CurrentUser,
isBillingEnabled: false,
});
const ongoingInviteTeam = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
onboardingStep: OnboardingStep.InviteTeam,
} as CurrentUser,
isBillingEnabled: false,
});
const completed = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
const incomplete = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
subscriptionStatus: 'incomplete',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: true,
});
const incompleteButBillingDisabled = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
subscriptionStatus: 'incomplete',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
const canceled = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
subscriptionStatus: 'canceled',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: true,
});
expect(ongoingUserCreation).toBe('ongoing_user_creation');
expect(ongoingWorkspaceActivation).toBe('ongoing_workspace_activation');
expect(ongoingProfileCreation).toBe('ongoing_profile_creation');
expect(ongoingSyncEmail).toBe('ongoing_sync_email');
expect(ongoingInviteTeam).toBe('ongoing_invite_team');
expect(completed).toBe('completed');
expect(incomplete).toBe('incomplete');
expect(canceled).toBe('canceled');
expect(incompleteButBillingDisabled).toBe('completed');
});
});

View File

@@ -1,89 +0,0 @@
import { CurrentUser } from '@/auth/states/currentUserState';
import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStep } from '~/generated/graphql';
export enum OnboardingStatus {
Incomplete = 'incomplete',
Canceled = 'canceled',
Unpaid = 'unpaid',
PastDue = 'past_due',
OngoingUserCreation = 'ongoing_user_creation',
OngoingWorkspaceActivation = 'ongoing_workspace_activation',
OngoingProfileCreation = 'ongoing_profile_creation',
OngoingSyncEmail = 'ongoing_sync_email',
OngoingInviteTeam = 'ongoing_invite_team',
Completed = 'completed',
CompletedWithoutSubscription = 'completed_without_subscription',
}
export const getOnboardingStatus = ({
isLoggedIn,
currentWorkspaceMember,
currentWorkspace,
currentUser,
isBillingEnabled,
}: {
isLoggedIn: boolean;
currentWorkspaceMember: Omit<
WorkspaceMember,
'createdAt' | 'updatedAt' | 'userId' | 'userEmail' | '__typename'
> | null;
currentWorkspace: CurrentWorkspace | null;
currentUser: CurrentUser | null;
isBillingEnabled: boolean;
}) => {
if (!isLoggedIn) {
return OnboardingStatus.OngoingUserCreation;
}
// After SignInUp, the user should have a current workspace assigned.
// If not, it indicates that the data is still being requested.
if (!currentWorkspace || !currentUser) {
return undefined;
}
if (
isBillingEnabled &&
currentWorkspace.subscriptionStatus === 'incomplete'
) {
return OnboardingStatus.Incomplete;
}
if (currentWorkspace.activationStatus !== 'active') {
return OnboardingStatus.OngoingWorkspaceActivation;
}
if (
!currentWorkspaceMember?.name.firstName ||
!currentWorkspaceMember?.name.lastName
) {
return OnboardingStatus.OngoingProfileCreation;
}
if (currentUser.onboardingStep === OnboardingStep.SyncEmail) {
return OnboardingStatus.OngoingSyncEmail;
}
if (currentUser.onboardingStep === OnboardingStep.InviteTeam) {
return OnboardingStatus.OngoingInviteTeam;
}
if (isBillingEnabled && currentWorkspace.subscriptionStatus === 'canceled') {
return OnboardingStatus.Canceled;
}
if (isBillingEnabled && currentWorkspace.subscriptionStatus === 'past_due') {
return OnboardingStatus.PastDue;
}
if (isBillingEnabled && currentWorkspace.subscriptionStatus === 'unpaid') {
return OnboardingStatus.Unpaid;
}
if (isBillingEnabled && !currentWorkspace.currentBillingSubscription) {
return OnboardingStatus.CompletedWithoutSubscription;
}
return OnboardingStatus.Completed;
};

View File

@@ -2,7 +2,7 @@ import { gql } from '@apollo/client';
export const CHECKOUT_SESSION = gql` export const CHECKOUT_SESSION = gql`
mutation CheckoutSession( mutation CheckoutSession(
$recurringInterval: String! $recurringInterval: SubscriptionInterval!
$successUrlPath: String $successUrlPath: String
) { ) {
checkoutSession( checkoutSession(

View File

@@ -0,0 +1,61 @@
import { act } from 'react-dom/test-utils';
import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { CurrentUser, currentUserState } from '@/auth/states/currentUserState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { OnboardingStatus } from '~/generated/graphql';
const tokenPair = {
accessToken: { token: 'accessToken', expiresAt: 'expiresAt' },
refreshToken: { token: 'refreshToken', expiresAt: 'expiresAt' },
};
const currentUser = {
id: '1',
onboardingStatus: null,
} as CurrentUser;
const renderHooks = () => {
const { result } = renderHook(
() => {
const onboardingStatus = useOnboardingStatus();
const setCurrentUser = useSetRecoilState(currentUserState);
const setTokenPair = useSetRecoilState(tokenPairState);
return {
onboardingStatus,
setCurrentUser,
setTokenPair,
};
},
{
wrapper: RecoilRoot,
},
);
return { result };
};
describe('useOnboardingStatus', () => {
it(`should return "undefined" when user is not logged in`, async () => {
const { result } = renderHooks();
expect(result.current.onboardingStatus).toBe(undefined);
});
Object.values(OnboardingStatus).forEach((onboardingStatus) => {
it(`should return "${onboardingStatus}"`, async () => {
const { result } = renderHooks();
const { setTokenPair, setCurrentUser } = result.current;
act(() => {
setTokenPair(tokenPair);
setCurrentUser({
...currentUser,
onboardingStatus,
});
});
expect(result.current.onboardingStatus).toBe(onboardingStatus);
});
});
});

View File

@@ -0,0 +1,87 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilState, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
import {
mockDefaultWorkspace,
mockedUserData,
} from '~/testing/mock-data/users';
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
useFindManyRecords: jest.fn(),
}));
const setupMockWorkspaceMembers = (withManyWorkspaceMembers = false) => {
jest
.requireMock('@/object-record/hooks/useFindManyRecords')
.useFindManyRecords.mockReturnValue({
records: withManyWorkspaceMembers ? [{}, {}] : [{}],
});
};
const renderHooks = (
onboardingStatus: OnboardingStatus,
withCurrentBillingSubscription: boolean,
) => {
const { result } = renderHook(
() => {
const [currentUser, setCurrentUser] = useRecoilState(currentUserState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setNextOnboardingStatus = useSetNextOnboardingStatus();
return {
currentUser,
setCurrentUser,
setCurrentWorkspace,
setNextOnboardingStatus,
};
},
{
wrapper: RecoilRoot,
},
);
act(() => {
result.current.setCurrentUser({ ...mockedUserData, onboardingStatus });
result.current.setCurrentWorkspace({
...mockDefaultWorkspace,
currentBillingSubscription: withCurrentBillingSubscription
? { id: v4(), status: SubscriptionStatus.Active }
: undefined,
});
});
act(() => {
result.current.setNextOnboardingStatus();
});
return result.current.currentUser?.onboardingStatus;
};
describe('useSetNextOnboardingStatus', () => {
it('should set next onboarding status for ProfileCreation', () => {
setupMockWorkspaceMembers();
const nextOnboardingStatus = renderHooks(
OnboardingStatus.ProfileCreation,
false,
);
expect(nextOnboardingStatus).toEqual(OnboardingStatus.SyncEmail);
});
it('should set next onboarding status for SyncEmail', () => {
setupMockWorkspaceMembers();
const nextOnboardingStatus = renderHooks(OnboardingStatus.SyncEmail, false);
expect(nextOnboardingStatus).toEqual(OnboardingStatus.InviteTeam);
});
it('should skip invite when workspaceMembers exist', () => {
setupMockWorkspaceMembers(true);
const nextOnboardingStatus = renderHooks(OnboardingStatus.SyncEmail, true);
expect(nextOnboardingStatus).toEqual(OnboardingStatus.Completed);
});
it('should set next onboarding status for Completed', () => {
setupMockWorkspaceMembers();
const nextOnboardingStatus = renderHooks(OnboardingStatus.InviteTeam, true);
expect(nextOnboardingStatus).toEqual(OnboardingStatus.Completed);
});
});

View File

@@ -0,0 +1,9 @@
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingStatus } from '~/generated/graphql';
export const useOnboardingStatus = (): OnboardingStatus | null | undefined => {
const currentUser = useRecoilValue(currentUserState);
return currentUser?.onboardingStatus;
};

View File

@@ -0,0 +1,51 @@
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { CurrentUser, currentUserState } from '@/auth/states/currentUserState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStatus } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
const getNextOnboardingStatus = (
currentUser: CurrentUser | null,
workspaceMembers: WorkspaceMember[],
) => {
if (currentUser?.onboardingStatus === OnboardingStatus.ProfileCreation) {
return OnboardingStatus.SyncEmail;
}
if (
currentUser?.onboardingStatus === OnboardingStatus.SyncEmail &&
workspaceMembers.length === 1
) {
return OnboardingStatus.InviteTeam;
}
return OnboardingStatus.Completed;
};
export const useSetNextOnboardingStatus = () => {
const { records: workspaceMembers } = useFindManyRecords<WorkspaceMember>({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
});
const currentUser = useRecoilValue(currentUserState);
return useRecoilCallback(
({ set }) =>
() => {
const nextOnboardingStatus = getNextOnboardingStatus(
currentUser,
workspaceMembers,
);
set(currentUserState, (current) => {
if (isDefined(current)) {
return {
...current,
onboardingStatus: nextOnboardingStatus,
};
}
return current;
});
},
[workspaceMembers, currentUser],
);
};

View File

@@ -1,41 +0,0 @@
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStep } from '~/generated/graphql';
const getNextOnboardingStep = (
currentOnboardingStep: OnboardingStep,
workspaceMembers: WorkspaceMember[],
) => {
if (currentOnboardingStep === OnboardingStep.SyncEmail) {
return workspaceMembers && workspaceMembers.length > 1
? null
: OnboardingStep.InviteTeam;
}
return null;
};
export const useSetNextOnboardingStep = () => {
const setCurrentUser = useSetRecoilState(currentUserState);
const { records: workspaceMembers } = useFindManyRecords<WorkspaceMember>({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
});
return useRecoilCallback(
() => (currentOnboardingStep: OnboardingStep) => {
setCurrentUser(
(current) =>
({
...current,
onboardingStep: getNextOnboardingStep(
currentOnboardingStep,
workspaceMembers,
),
}) as any,
);
},
[setCurrentUser, workspaceMembers],
);
};

View File

@@ -10,7 +10,7 @@ import { supportChatState } from '@/client-config/states/supportChatState';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { import {
mockDefaultWorkspace, mockDefaultWorkspace,
mockedUsersData, mockedUserData,
mockedWorkspaceMemberData, mockedWorkspaceMemberData,
} from '~/testing/mock-data/users'; } from '~/testing/mock-data/users';
@@ -30,7 +30,7 @@ const meta: Meta<typeof SupportChat> = {
setCurrentWorkspace(mockDefaultWorkspace); setCurrentWorkspace(mockDefaultWorkspace);
setCurrentWorkspaceMember(mockedWorkspaceMemberData); setCurrentWorkspaceMember(mockedWorkspaceMemberData);
setCurrentUser(mockedUsersData[0]); setCurrentUser(mockedUserData);
setSupportChat({ supportDriver: 'front', supportFrontChatId: '1234' }); setSupportChat({ supportDriver: 'front', supportFrontChatId: '1234' });
return <Story />; return <Story />;

View File

@@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import { css, useTheme } from '@emotion/react'; import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconInfoCircle } from 'twenty-ui'; import { IconInfoCircle } from 'twenty-ui';
import { AppPath } from '@/types/AppPath';
import { Button } from '@/ui/input/button/components/Button'; import { Button } from '@/ui/input/button/components/Button';
export type InfoAccent = 'blue' | 'danger'; export type InfoAccent = 'blue' | 'danger';
@@ -11,6 +13,7 @@ export type InfoProps = {
text: string; text: string;
buttonTitle?: string; buttonTitle?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
to?: AppPath;
}; };
const StyledTextContainer = styled.div` const StyledTextContainer = styled.div`
@@ -30,6 +33,7 @@ const StyledInfo = styled.div<Pick<InfoProps, 'accent'>>`
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
justify-content: space-between; justify-content: space-between;
max-width: 512px; max-width: 512px;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
${({ theme, accent }) => { ${({ theme, accent }) => {
switch (accent) { switch (accent) {
@@ -46,11 +50,17 @@ const StyledInfo = styled.div<Pick<InfoProps, 'accent'>>`
} }
}} }}
`; `;
const StyledLink = styled(Link)`
text-decoration: none;
`;
export const Info = ({ export const Info = ({
accent = 'blue', accent = 'blue',
text, text,
buttonTitle, buttonTitle,
onClick, onClick,
to,
}: InfoProps) => { }: InfoProps) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
@@ -59,12 +69,23 @@ export const Info = ({
<StyledIconInfoCircle size={theme.icon.size.md} /> <StyledIconInfoCircle size={theme.icon.size.md} />
{text} {text}
</StyledTextContainer> </StyledTextContainer>
{buttonTitle && onClick && ( {buttonTitle && to && (
<StyledLink to={to}>
<Button
title={buttonTitle}
size={'small'}
variant={'secondary'}
accent={accent}
/>
</StyledLink>
)}
{buttonTitle && onClick && !to && (
<Button <Button
title={buttonTitle} title={buttonTitle}
onClick={onClick} onClick={onClick}
size={'small'} size={'small'}
variant={'secondary'} variant={'secondary'}
accent={accent}
/> />
)} )}
</StyledInfo> </StyledInfo>

View File

@@ -1,18 +1,34 @@
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil'; import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus'; import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal'; import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState'; import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
jest.mock('@/auth/hooks/useOnboardingStatus'); jest.mock('@/onboarding/hooks/useOnboardingStatus');
const setupMockOnboardingStatus = (onboardingStatus: OnboardingStatus) => { const setupMockOnboardingStatus = (
onboardingStatus: OnboardingStatus | undefined,
) => {
jest.mocked(useOnboardingStatus).mockReturnValueOnce(onboardingStatus); jest.mocked(useOnboardingStatus).mockReturnValueOnce(onboardingStatus);
}; };
jest.mock('@/workspace/hooks/useSubscriptionStatus');
const setupMockSubscriptionStatus = (
subscriptionStatus: SubscriptionStatus | undefined,
) => {
jest.mocked(useSubscriptionStatus).mockReturnValueOnce(subscriptionStatus);
};
jest.mock('@/auth/hooks/useIsLogged');
const setupMockIsLogged = (isLogged: boolean) => {
jest.mocked(useIsLogged).mockReturnValueOnce(isLogged);
};
jest.mock('~/hooks/useIsMatchingLocation'); jest.mock('~/hooks/useIsMatchingLocation');
const mockUseIsMatchingLocation = jest.mocked(useIsMatchingLocation); const mockUseIsMatchingLocation = jest.mocked(useIsMatchingLocation);
@@ -39,264 +55,245 @@ const getResult = (isDefaultLayoutAuthModalVisible = true) =>
// prettier-ignore // prettier-ignore
const testCases = [ const testCases = [
{ loc: AppPath.Verify, status: OnboardingStatus.Incomplete, res: false }, { loc: AppPath.Verify, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingUserCreation, res: false }, { loc: AppPath.Verify, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingWorkspaceActivation, res: false }, { loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingProfileCreation, res: false }, { loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingSyncEmail, res: false }, { loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingInviteTeam, res: false }, { loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.SignInUp, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.SignInUp, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SignInUp, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SignInUp, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.SignInUp, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.SignInUp, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.SignInUp, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SignInUp, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.Invite, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.Invite, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.Canceled, res: true }, { loc: AppPath.Invite, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.Unpaid, res: true }, { loc: AppPath.Invite, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.PastDue, res: true }, { loc: AppPath.Invite, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.Invite, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.Invite, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.Invite, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.Invite, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.Invite, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.Completed, res: true }, { loc: AppPath.Invite, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.CompletedWithoutSubscription, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.Canceled, res: true }, { loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.Unpaid, res: true }, { loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.PastDue, res: true }, { loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.ResetPassword, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.Completed, res: true }, { loc: AppPath.ResetPassword, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.CompletedWithoutSubscription, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.CreateWorkspace, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.CreateWorkspace, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.CreateProfile, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.CreateProfile, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.SyncEmails, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.SyncEmails, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.SyncEmails, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.SyncEmails, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.SyncEmails, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.SyncEmails, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.SyncEmails, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.SyncEmails, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.SyncEmails, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.SyncEmails, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.InviteTeam, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.InviteTeam, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.InviteTeam, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.InviteTeam, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.InviteTeam, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.InviteTeam, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.InviteTeam, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.InviteTeam, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.InviteTeam, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.InviteTeam, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Canceled, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.PlanRequired, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.PlanRequired, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.CompletedWithoutSubscription, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.PlanRequiredSuccess, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.PlanRequiredSuccess, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.Index, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.Index, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.Index, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.Index, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Index, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.Index, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Index, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.Index, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.Index, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.Index, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.Index, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.Index, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.Index, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.Index, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.Index, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Index, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.TasksPage, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.TasksPage, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.TasksPage, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.TasksPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.TasksPage, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.TasksPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.TasksPage, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.TasksPage, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.TasksPage, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.TasksPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.TasksPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.TasksPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.TasksPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.TasksPage, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.TasksPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.TasksPage, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.OpportunitiesPage, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.OpportunitiesPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.OpportunitiesPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.OpportunitiesPage, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.OpportunitiesPage, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.OpportunitiesPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.OpportunitiesPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.OpportunitiesPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.OpportunitiesPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.OpportunitiesPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.RecordIndexPage, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.RecordIndexPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.RecordIndexPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.RecordIndexPage, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.RecordIndexPage, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.RecordIndexPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.RecordIndexPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.RecordIndexPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.RecordIndexPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.RecordIndexPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.RecordShowPage, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.RecordShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.RecordShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.RecordShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.RecordShowPage, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.RecordShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.RecordShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.RecordShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.RecordShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.RecordShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.SettingsCatchAll, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.SettingsCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.SettingsCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.SettingsCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.SettingsCatchAll, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.SettingsCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.SettingsCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.SettingsCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.SettingsCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.SettingsCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.DevelopersCatchAll, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.Impersonate, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.Impersonate, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Impersonate, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Impersonate, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.Impersonate, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.Impersonate, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Impersonate, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.Authorize, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.Authorize, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Authorize, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Authorize, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.Authorize, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.Authorize, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Authorize, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.NotFoundWildcard, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.NotFoundWildcard, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.NotFoundWildcard, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.NotFoundWildcard, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.NotFoundWildcard, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.NotFoundWildcard, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.NotFoundWildcard, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.NotFoundWildcard, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.NotFoundWildcard, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.NotFoundWildcard, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.NotFound, status: OnboardingStatus.Incomplete, res: true }, { loc: AppPath.NotFound, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.NotFound, status: OnboardingStatus.Canceled, res: false }, { loc: AppPath.NotFound, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.NotFound, status: OnboardingStatus.Unpaid, res: false }, { loc: AppPath.NotFound, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.NotFound, status: OnboardingStatus.PastDue, res: false }, { loc: AppPath.NotFound, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingUserCreation, res: true }, { loc: AppPath.NotFound, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingWorkspaceActivation, res: true }, { loc: AppPath.NotFound, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingProfileCreation, res: true }, { loc: AppPath.NotFound, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingSyncEmail, res: true }, { loc: AppPath.NotFound, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingInviteTeam, res: true }, { loc: AppPath.NotFound, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.NotFound, status: OnboardingStatus.Completed, res: false }, { loc: AppPath.NotFound, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.NotFound, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
]; ];
describe('useShowAuthModal', () => { describe('useShowAuthModal', () => {
testCases.forEach((testCase) => { testCases.forEach((testCase) => {
it(`testCase for location ${testCase.loc} with onboardingStatus ${testCase.status} should return ${testCase.res}`, () => { it(`testCase for location ${testCase.loc} with onboardingStatus ${testCase.onboardingStatus} should return ${testCase.res}`, () => {
setupMockOnboardingStatus(testCase.status); setupMockOnboardingStatus(testCase.onboardingStatus);
setupMockSubscriptionStatus(testCase.subscriptionStatus);
setupMockIsMatchingLocation(testCase.loc); setupMockIsMatchingLocation(testCase.loc);
setupMockIsLogged(testCase.isLogged);
const { result } = getResult(); const { result } = getResult();
if (testCase.res) { if (testCase.res) {
expect(result.current).toBeTruthy(); expect(result.current).toBeTruthy();
@@ -309,13 +306,17 @@ describe('useShowAuthModal', () => {
describe('test with token validation loading', () => { describe('test with token validation loading', () => {
it(`with appPath ${AppPath.Invite} and isDefaultLayoutAuthModalVisible=false`, () => { it(`with appPath ${AppPath.Invite} and isDefaultLayoutAuthModalVisible=false`, () => {
setupMockOnboardingStatus(OnboardingStatus.Completed); setupMockOnboardingStatus(OnboardingStatus.Completed);
setupMockSubscriptionStatus(SubscriptionStatus.Active);
setupMockIsMatchingLocation(AppPath.Invite); setupMockIsMatchingLocation(AppPath.Invite);
setupMockIsLogged(true);
const { result } = getResult(false); const { result } = getResult(false);
expect(result.current).toBeFalsy(); expect(result.current).toBeFalsy();
}); });
it(`with appPath ${AppPath.ResetPassword} and isDefaultLayoutAuthModalVisible=false`, () => { it(`with appPath ${AppPath.ResetPassword} and isDefaultLayoutAuthModalVisible=false`, () => {
setupMockOnboardingStatus(OnboardingStatus.Completed); setupMockOnboardingStatus(OnboardingStatus.Completed);
setupMockSubscriptionStatus(SubscriptionStatus.Active);
setupMockIsMatchingLocation(AppPath.ResetPassword); setupMockIsMatchingLocation(AppPath.ResetPassword);
setupMockIsLogged(true);
const { result } = getResult(false); const { result } = getResult(false);
expect(result.current).toBeFalsy(); expect(result.current).toBeFalsy();
}); });
@@ -323,8 +324,17 @@ describe('useShowAuthModal', () => {
describe('tests should be exhaustive', () => { describe('tests should be exhaustive', () => {
it('all location and onboarding status should be tested', () => { it('all location and onboarding status should be tested', () => {
const untestedSubscriptionStatus = [
SubscriptionStatus.Active,
SubscriptionStatus.IncompleteExpired,
SubscriptionStatus.Paused,
SubscriptionStatus.Trialing,
];
expect(testCases.length).toEqual( expect(testCases.length).toEqual(
Object.keys(AppPath).length * Object.keys(OnboardingStatus).length, Object.keys(AppPath).length *
(Object.keys(OnboardingStatus).length +
(Object.keys(SubscriptionStatus).length -
untestedSubscriptionStatus.length)),
); );
}); });
}); });

View File

@@ -1,15 +1,20 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus'; import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState'; import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { isDefined } from '~/utils/isDefined';
export const useShowAuthModal = () => { export const useShowAuthModal = () => {
const isMatchingLocation = useIsMatchingLocation(); const isMatchingLocation = useIsMatchingLocation();
const isLoggedIn = useIsLogged();
const onboardingStatus = useOnboardingStatus(); const onboardingStatus = useOnboardingStatus();
const subscriptionStatus = useSubscriptionStatus();
const isDefaultLayoutAuthModalVisible = useRecoilValue( const isDefaultLayoutAuthModalVisible = useRecoilValue(
isDefaultLayoutAuthModalVisibleState, isDefaultLayoutAuthModalVisibleState,
); );
@@ -24,21 +29,28 @@ export const useShowAuthModal = () => {
return isDefaultLayoutAuthModalVisible; return isDefaultLayoutAuthModalVisible;
} }
if ( if (
OnboardingStatus.Incomplete === onboardingStatus || !isLoggedIn ||
OnboardingStatus.OngoingUserCreation === onboardingStatus || onboardingStatus === OnboardingStatus.PlanRequired ||
OnboardingStatus.OngoingProfileCreation === onboardingStatus || onboardingStatus === OnboardingStatus.ProfileCreation ||
OnboardingStatus.OngoingWorkspaceActivation === onboardingStatus || onboardingStatus === OnboardingStatus.WorkspaceActivation ||
OnboardingStatus.OngoingSyncEmail === onboardingStatus || onboardingStatus === OnboardingStatus.SyncEmail ||
OnboardingStatus.OngoingInviteTeam === onboardingStatus onboardingStatus === OnboardingStatus.InviteTeam
) { ) {
return true; return true;
} }
if (isMatchingLocation(AppPath.PlanRequired)) { if (isMatchingLocation(AppPath.PlanRequired)) {
return ( return (
OnboardingStatus.CompletedWithoutSubscription === onboardingStatus || (onboardingStatus === OnboardingStatus.Completed &&
OnboardingStatus.Canceled === onboardingStatus !isDefined(subscriptionStatus)) ||
subscriptionStatus === SubscriptionStatus.Canceled
); );
} }
return false; return false;
}, [isDefaultLayoutAuthModalVisible, isMatchingLocation, onboardingStatus]); }, [
isLoggedIn,
isDefaultLayoutAuthModalVisible,
isMatchingLocation,
onboardingStatus,
subscriptionStatus,
]);
}; };

View File

@@ -8,7 +8,7 @@ export const USER_QUERY_FRAGMENT = gql`
email email
canImpersonate canImpersonate
supportUserHash supportUserHash
onboardingStep onboardingStatus
workspaceMember { workspaceMember {
id id
name { name {
@@ -26,7 +26,6 @@ export const USER_QUERY_FRAGMENT = gql`
domainName domainName
inviteHash inviteHash
allowImpersonation allowImpersonation
subscriptionStatus
activationStatus activationStatus
featureFlags { featureFlags {
id id

View File

@@ -8,7 +8,6 @@ export const UPDATE_WORKSPACE = gql`
displayName displayName
logo logo
allowImpersonation allowImpersonation
subscriptionStatus
} }
} }
`; `;

View File

@@ -0,0 +1,57 @@
import { act } from 'react-dom/test-utils';
import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import {
CurrentWorkspace,
currentWorkspaceState,
} from '@/auth/states/currentWorkspaceState';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { SubscriptionStatus } from '~/generated/graphql';
const currentWorkspace = {
id: '1',
currentBillingSubscription: { status: SubscriptionStatus.Incomplete },
activationStatus: 'active',
allowImpersonation: true,
} as CurrentWorkspace;
const renderHooks = () => {
const { result } = renderHook(
() => {
const subscriptionStatus = useSubscriptionStatus();
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
return {
subscriptionStatus,
setCurrentWorkspace,
};
},
{
wrapper: RecoilRoot,
},
);
return { result };
};
describe('useSubscriptionStatus', () => {
Object.values(SubscriptionStatus).forEach((subscriptionStatus) => {
it(`should return "${subscriptionStatus}"`, async () => {
const { result } = renderHooks();
const { setCurrentWorkspace } = result.current;
act(() => {
setCurrentWorkspace({
...currentWorkspace,
currentBillingSubscription: {
id: v4(),
status: subscriptionStatus,
},
});
});
expect(result.current.subscriptionStatus).toBe(subscriptionStatus);
});
});
});

View File

@@ -0,0 +1,9 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SubscriptionStatus } from '~/generated/graphql';
export const useSubscriptionStatus = (): SubscriptionStatus | undefined => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
return currentWorkspace?.currentBillingSubscription?.status;
};

View File

@@ -4,14 +4,21 @@ import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw'; import { graphql, HttpResponse } from 'msw';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { ValidatePasswordResetTokenDocument } from '~/generated/graphql'; import {
OnboardingStatus,
ValidatePasswordResetTokenDocument,
} from '~/generated/graphql';
import { PasswordReset } from '~/pages/auth/PasswordReset'; import { PasswordReset } from '~/pages/auth/PasswordReset';
import { import {
PageDecorator, PageDecorator,
PageDecoratorArgs, PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator'; } from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users'; import { mockedOnboardingUserData } from '~/testing/mock-data/users';
const mockedOnboardingUsersData = mockedOnboardingUserData(
OnboardingStatus.Completed,
);
const meta: Meta<PageDecoratorArgs> = { const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/PasswordReset', title: 'Pages/Auth/PasswordReset',
@@ -30,8 +37,8 @@ const meta: Meta<PageDecoratorArgs> = {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
validatePasswordResetToken: { validatePasswordResetToken: {
id: mockedOnboardingUsersData[0].id, id: mockedOnboardingUsersData.id,
email: mockedOnboardingUsersData[0].email, email: mockedOnboardingUsersData.email,
}, },
}, },
}); });
@@ -40,7 +47,7 @@ const meta: Meta<PageDecoratorArgs> = {
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: mockedOnboardingUsersData[0], currentUser: mockedOnboardingUsersData,
}, },
}); });
}), }),

View File

@@ -19,6 +19,7 @@ import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
import { CAL_LINK } from '@/ui/navigation/link/constants/Cal'; import { CAL_LINK } from '@/ui/navigation/link/constants/Cal';
import { import {
ProductPriceEntity, ProductPriceEntity,
SubscriptionInterval,
useCheckoutSessionMutation, useCheckoutSessionMutation,
useGetProductPricesQuery, useGetProductPricesQuery,
} from '~/generated/graphql'; } from '~/generated/graphql';
@@ -75,7 +76,7 @@ const benefits = [
export const ChooseYourPlan = () => { export const ChooseYourPlan = () => {
const billing = useRecoilValue(billingState); const billing = useRecoilValue(billingState);
const [planSelected, setPlanSelected] = useState('month'); const [planSelected, setPlanSelected] = useState(SubscriptionInterval.Month);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -87,7 +88,7 @@ export const ChooseYourPlan = () => {
const [checkoutSession] = useCheckoutSessionMutation(); const [checkoutSession] = useCheckoutSessionMutation();
const handlePlanChange = (type?: string) => { const handlePlanChange = (type?: SubscriptionInterval) => {
return () => { return () => {
if (isNonEmptyString(type) && planSelected !== type) { if (isNonEmptyString(type) && planSelected !== type) {
setPlanSelected(type); setPlanSelected(type);
@@ -101,11 +102,11 @@ export const ChooseYourPlan = () => {
price: ProductPriceEntity, price: ProductPriceEntity,
prices: ProductPriceEntity[], prices: ProductPriceEntity[],
): string => { ): string => {
if (price.recurringInterval !== 'year') { if (price.recurringInterval !== SubscriptionInterval.Year) {
return 'Cancel anytime'; return 'Cancel anytime';
} }
const monthPrice = prices.filter( const monthPrice = prices.filter(
(price) => price.recurringInterval === 'month', (price) => price.recurringInterval === SubscriptionInterval.Month,
)?.[0]; )?.[0];
if ( if (
isDefined(monthPrice) && isDefined(monthPrice) &&

View File

@@ -9,11 +9,11 @@ import { z } from 'zod';
import { SubTitle } from '@/auth/components/SubTitle'; import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title'; import { Title } from '@/auth/components/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader'; import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
@@ -22,6 +22,8 @@ import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStatus } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
const StyledContentContainer = styled.div` const StyledContentContainer = styled.div`
width: 100%; width: 100%;
@@ -55,11 +57,11 @@ type Form = z.infer<typeof validationSchema>;
export const CreateProfile = () => { export const CreateProfile = () => {
const onboardingStatus = useOnboardingStatus(); const onboardingStatus = useOnboardingStatus();
const setNextOnboardingStatus = useSetNextOnboardingStatus();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState( const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
currentWorkspaceMemberState, currentWorkspaceMemberState,
); );
const { updateOneRecord } = useUpdateOneRecord<WorkspaceMember>({ const { updateOneRecord } = useUpdateOneRecord<WorkspaceMember>({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember, objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
}); });
@@ -100,17 +102,20 @@ export const CreateProfile = () => {
}, },
}); });
setCurrentWorkspaceMember( setCurrentWorkspaceMember((current) => {
(current) => if (isDefined(current)) {
({ return {
...current, ...current,
name: { name: {
firstName: data.firstName, firstName: data.firstName,
lastName: data.lastName, lastName: data.lastName,
}, },
colorScheme: 'System', colorScheme: 'System',
}) as any, };
); }
return current;
});
setNextOnboardingStatus();
} catch (error: any) { } catch (error: any) {
enqueueSnackBar(error?.message, { enqueueSnackBar(error?.message, {
variant: SnackBarVariant.Error, variant: SnackBarVariant.Error,
@@ -119,6 +124,7 @@ export const CreateProfile = () => {
}, },
[ [
currentWorkspaceMember?.id, currentWorkspaceMember?.id,
setNextOnboardingStatus,
enqueueSnackBar, enqueueSnackBar,
setCurrentWorkspaceMember, setCurrentWorkspaceMember,
updateOneRecord, updateOneRecord,
@@ -137,7 +143,7 @@ export const CreateProfile = () => {
PageHotkeyScope.CreateProfile, PageHotkeyScope.CreateProfile,
); );
if (onboardingStatus !== OnboardingStatus.OngoingProfileCreation) { if (onboardingStatus !== OnboardingStatus.ProfileCreation) {
return null; return null;
} }

View File

@@ -9,18 +9,20 @@ import { z } from 'zod';
import { SubTitle } from '@/auth/components/SubTitle'; import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title'; import { Title } from '@/auth/components/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState'; import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries'; import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { Loader } from '@/ui/feedback/loader/components/Loader'; import { Loader } from '@/ui/feedback/loader/components/Loader';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { MainButton } from '@/ui/input/button/components/MainButton'; import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { useActivateWorkspaceMutation } from '~/generated/graphql'; import {
OnboardingStatus,
useActivateWorkspaceMutation,
} from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
const StyledContentContainer = styled.div` const StyledContentContainer = styled.div`
@@ -105,7 +107,7 @@ export const CreateWorkspace = () => {
} }
}; };
if (onboardingStatus !== OnboardingStatus.OngoingWorkspaceActivation) { if (onboardingStatus !== OnboardingStatus.WorkspaceActivation) {
return null; return null;
} }

View File

@@ -17,7 +17,7 @@ import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title'; import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useSetNextOnboardingStep } from '@/onboarding/hooks/useSetNextOnboardingStep'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { SeparatorLineText } from '@/ui/display/text/components/SeparatorLineText'; import { SeparatorLineText } from '@/ui/display/text/components/SeparatorLineText';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
@@ -27,7 +27,10 @@ import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { AnimatedTranslation } from '@/ui/utilities/animation/components/AnimatedTranslation'; import { AnimatedTranslation } from '@/ui/utilities/animation/components/AnimatedTranslation';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { OnboardingStep, useSendInviteLinkMutation } from '~/generated/graphql'; import {
OnboardingStatus,
useSendInviteLinkMutation,
} from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
const StyledAnimatedContainer = styled.div` const StyledAnimatedContainer = styled.div`
@@ -63,7 +66,7 @@ export const InviteTeam = () => {
const theme = useTheme(); const theme = useTheme();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const [sendInviteLink] = useSendInviteLinkMutation(); const [sendInviteLink] = useSendInviteLinkMutation();
const setNextOnboardingStep = useSetNextOnboardingStep(); const setNextOnboardingStatus = useSetNextOnboardingStatus();
const currentUser = useRecoilValue(currentUserState); const currentUser = useRecoilValue(currentUserState);
const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { const {
@@ -133,7 +136,7 @@ export const InviteTeam = () => {
); );
const result = await sendInviteLink({ variables: { emails } }); const result = await sendInviteLink({ variables: { emails } });
setNextOnboardingStep(OnboardingStep.InviteTeam); setNextOnboardingStatus();
if (isDefined(result.errors)) { if (isDefined(result.errors)) {
throw result.errors; throw result.errors;
@@ -145,7 +148,7 @@ export const InviteTeam = () => {
}); });
} }
}, },
[enqueueSnackBar, sendInviteLink, setNextOnboardingStep], [enqueueSnackBar, sendInviteLink, setNextOnboardingStatus],
); );
useScopedHotkeys( useScopedHotkeys(
@@ -157,7 +160,7 @@ export const InviteTeam = () => {
[handleSubmit], [handleSubmit],
); );
if (currentUser?.onboardingStep !== OnboardingStep.InviteTeam) { if (currentUser?.onboardingStatus !== OnboardingStatus.InviteTeam) {
return <></>; return <></>;
} }

View File

@@ -1,14 +1,17 @@
import React from 'react'; import React from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconCheck, RGBA } from 'twenty-ui'; import { IconCheck, RGBA } from 'twenty-ui';
import { SubTitle } from '@/auth/components/SubTitle'; import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title'; import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { MainButton } from '@/ui/input/button/components/MainButton'; import { MainButton } from '@/ui/input/button/components/MainButton';
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
import { OnboardingStatus } from '~/generated/graphql';
const StyledCheckContainer = styled.div` const StyledCheckContainer = styled.div`
align-items: center; align-items: center;
@@ -29,8 +32,14 @@ const StyledButtonContainer = styled.div`
export const PaymentSuccess = () => { export const PaymentSuccess = () => {
const theme = useTheme(); const theme = useTheme();
const currentUser = useRecoilValue(currentUserState);
const color = const color =
theme.name === 'light' ? theme.grayScale.gray90 : theme.grayScale.gray10; theme.name === 'light' ? theme.grayScale.gray90 : theme.grayScale.gray10;
if (currentUser?.onboardingStatus === OnboardingStatus.Completed) {
return <></>;
}
return ( return (
<> <>
<AnimatedEaseIn> <AnimatedEaseIn>

View File

@@ -9,7 +9,7 @@ import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title'; import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard'; import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard';
import { useSetNextOnboardingStep } from '@/onboarding/hooks/useSetNextOnboardingStep'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
@@ -19,7 +19,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { import {
CalendarChannelVisibility, CalendarChannelVisibility,
MessageChannelVisibility, MessageChannelVisibility,
OnboardingStep, OnboardingStatus,
useSkipSyncEmailOnboardingStepMutation, useSkipSyncEmailOnboardingStepMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
@@ -40,12 +40,12 @@ const StyledActionLinkContainer = styled.div`
export const SyncEmails = () => { export const SyncEmails = () => {
const theme = useTheme(); const theme = useTheme();
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const setNextOnboardingStep = useSetNextOnboardingStep(); const setNextOnboardingStatus = useSetNextOnboardingStatus();
const currentUser = useRecoilValue(currentUserState); const currentUser = useRecoilValue(currentUserState);
const [visibility, setVisibility] = useState<MessageChannelVisibility>( const [visibility, setVisibility] = useState<MessageChannelVisibility>(
MessageChannelVisibility.ShareEverything, MessageChannelVisibility.ShareEverything,
); );
const [skipSyncEmailOnboardingStepMutation] = const [skipSyncEmailOnboardingStatusMutation] =
useSkipSyncEmailOnboardingStepMutation(); useSkipSyncEmailOnboardingStepMutation();
const handleButtonClick = async () => { const handleButtonClick = async () => {
@@ -62,8 +62,8 @@ export const SyncEmails = () => {
}; };
const continueWithoutSync = async () => { const continueWithoutSync = async () => {
await skipSyncEmailOnboardingStepMutation(); await skipSyncEmailOnboardingStatusMutation();
setNextOnboardingStep(OnboardingStep.SyncEmail); setNextOnboardingStatus();
}; };
useScopedHotkeys( useScopedHotkeys(
@@ -75,7 +75,7 @@ export const SyncEmails = () => {
[continueWithoutSync], [continueWithoutSync],
); );
if (currentUser?.onboardingStep !== OnboardingStep.SyncEmail) { if (currentUser?.onboardingStatus !== OnboardingStatus.SyncEmail) {
return <></>; return <></>;
} }

View File

@@ -5,13 +5,14 @@ import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { OnboardingStatus } from '~/generated/graphql';
import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan'; import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan';
import { import {
PageDecorator, PageDecorator,
PageDecoratorArgs, PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator'; } from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users'; import { mockedOnboardingUserData } from '~/testing/mock-data/users';
import { sleep } from '~/utils/sleep'; import { sleep } from '~/utils/sleep';
const meta: Meta<PageDecoratorArgs> = { const meta: Meta<PageDecoratorArgs> = {
@@ -25,7 +26,9 @@ const meta: Meta<PageDecoratorArgs> = {
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: mockedOnboardingUsersData[0], currentUser: mockedOnboardingUserData(
OnboardingStatus.PlanRequired,
),
}, },
}); });
}), }),

View File

@@ -5,13 +5,14 @@ import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { OnboardingStatus } from '~/generated/graphql';
import { CreateProfile } from '~/pages/onboarding/CreateProfile'; import { CreateProfile } from '~/pages/onboarding/CreateProfile';
import { import {
PageDecorator, PageDecorator,
PageDecoratorArgs, PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator'; } from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users'; import { mockedOnboardingUserData } from '~/testing/mock-data/users';
const meta: Meta<PageDecoratorArgs> = { const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Onboarding/CreateProfile', title: 'Pages/Onboarding/CreateProfile',
@@ -24,7 +25,9 @@ const meta: Meta<PageDecoratorArgs> = {
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: mockedOnboardingUsersData[0], currentUser: mockedOnboardingUserData(
OnboardingStatus.ProfileCreation,
),
}, },
}); });
}), }),

View File

@@ -2,30 +2,22 @@ import { getOperationName } from '@apollo/client/utilities';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test'; import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw'; import { graphql, HttpResponse } from 'msw';
import { useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { OnboardingStatus } from '~/generated/graphql';
import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace'; import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace';
import { import {
PageDecorator, PageDecorator,
PageDecoratorArgs, PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator'; } from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users'; import { mockedOnboardingUserData } from '~/testing/mock-data/users';
const meta: Meta<PageDecoratorArgs> = { const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Onboarding/CreateWorkspace', title: 'Pages/Onboarding/CreateWorkspace',
component: CreateWorkspace, component: CreateWorkspace,
decorators: [ decorators: [PageDecorator],
(Story) => {
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
setCurrentWorkspace(mockedOnboardingUsersData[1].defaultWorkspace);
return <Story />;
},
PageDecorator,
],
args: { routePath: AppPath.CreateWorkspace }, args: { routePath: AppPath.CreateWorkspace },
parameters: { parameters: {
msw: { msw: {
@@ -33,7 +25,9 @@ const meta: Meta<PageDecoratorArgs> = {
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: mockedOnboardingUsersData[1], currentUser: mockedOnboardingUserData(
OnboardingStatus.WorkspaceActivation,
),
}, },
}); });
}), }),

View File

@@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test'; import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw'; import { graphql, HttpResponse } from 'msw';
import { OnboardingStep } from '~/generated/graphql'; import { OnboardingStatus } from '~/generated/graphql';
import { AppPath } from '~/modules/types/AppPath'; import { AppPath } from '~/modules/types/AppPath';
import { GET_CURRENT_USER } from '~/modules/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '~/modules/users/graphql/queries/getCurrentUser';
import { InviteTeam } from '~/pages/onboarding/InviteTeam'; import { InviteTeam } from '~/pages/onboarding/InviteTeam';
@@ -12,7 +12,7 @@ import {
PageDecoratorArgs, PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator'; } from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users'; import { mockedOnboardingUserData } from '~/testing/mock-data/users';
const meta: Meta<PageDecoratorArgs> = { const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Onboarding/InviteTeam', title: 'Pages/Onboarding/InviteTeam',
@@ -25,10 +25,9 @@ const meta: Meta<PageDecoratorArgs> = {
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: { currentUser: mockedOnboardingUserData(
...mockedOnboardingUsersData[0], OnboardingStatus.InviteTeam,
onboardingStep: OnboardingStep.InviteTeam, ),
},
}, },
}); });
}), }),

View File

@@ -5,13 +5,14 @@ import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { OnboardingStatus } from '~/generated/graphql';
import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess'; import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess';
import { import {
PageDecorator, PageDecorator,
PageDecoratorArgs, PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator'; } from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users'; import { mockedOnboardingUserData } from '~/testing/mock-data/users';
const meta: Meta<PageDecoratorArgs> = { const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Onboarding/PaymentSuccess', title: 'Pages/Onboarding/PaymentSuccess',
@@ -24,7 +25,9 @@ const meta: Meta<PageDecoratorArgs> = {
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: mockedOnboardingUsersData[0], currentUser: mockedOnboardingUserData(
OnboardingStatus.WorkspaceActivation,
),
}, },
}); });
}), }),

View File

@@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test'; import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw'; import { graphql, HttpResponse } from 'msw';
import { OnboardingStep } from '~/generated/graphql'; import { OnboardingStatus } from '~/generated/graphql';
import { AppPath } from '~/modules/types/AppPath'; import { AppPath } from '~/modules/types/AppPath';
import { GET_CURRENT_USER } from '~/modules/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '~/modules/users/graphql/queries/getCurrentUser';
import { SyncEmails } from '~/pages/onboarding/SyncEmails'; import { SyncEmails } from '~/pages/onboarding/SyncEmails';
@@ -12,7 +12,7 @@ import {
PageDecoratorArgs, PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator'; } from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users'; import { mockedOnboardingUserData } from '~/testing/mock-data/users';
const meta: Meta<PageDecoratorArgs> = { const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Onboarding/SyncEmails', title: 'Pages/Onboarding/SyncEmails',
@@ -25,10 +25,7 @@ const meta: Meta<PageDecoratorArgs> = {
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: { currentUser: mockedOnboardingUserData(OnboardingStatus.SyncEmail),
...mockedOnboardingUsersData[0],
onboardingStep: OnboardingStep.SyncEmail,
},
}, },
}); });
}), }),

View File

@@ -10,10 +10,9 @@ import {
IconCurrencyDollar, IconCurrencyDollar,
} from 'twenty-ui'; } from 'twenty-ui';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { SettingsBillingCoverImage } from '@/billing/components/SettingsBillingCoverImage'; import { SettingsBillingCoverImage } from '@/billing/components/SettingsBillingCoverImage';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SupportChat } from '@/support/components/SupportChat'; import { SupportChat } from '@/support/components/SupportChat';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
@@ -24,8 +23,11 @@ import { Button } from '@/ui/input/button/components/Button';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { import {
OnboardingStatus,
SubscriptionInterval,
SubscriptionStatus,
useBillingPortalSessionQuery, useBillingPortalSessionQuery,
useUpdateBillingSubscriptionMutation, useUpdateBillingSubscriptionMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
@@ -40,21 +42,21 @@ const StyledInvisibleChat = styled.div`
`; `;
type SwitchInfo = { type SwitchInfo = {
newInterval: string; newInterval: SubscriptionInterval;
to: string; to: string;
from: string; from: string;
impact: string; impact: string;
}; };
const MONTHLY_SWITCH_INFO: SwitchInfo = { const MONTHLY_SWITCH_INFO: SwitchInfo = {
newInterval: 'year', newInterval: SubscriptionInterval.Year,
to: 'to yearly', to: 'to yearly',
from: 'from monthly to yearly', from: 'from monthly to yearly',
impact: 'You will be charged immediately for the full year.', impact: 'You will be charged immediately for the full year.',
}; };
const YEARLY_SWITCH_INFO: SwitchInfo = { const YEARLY_SWITCH_INFO: SwitchInfo = {
newInterval: 'month', newInterval: SubscriptionInterval.Month,
to: 'to monthly', to: 'to monthly',
from: 'from yearly to monthly', from: 'from yearly to monthly',
impact: 'Your credit balance will be used to pay the monthly bills.', impact: 'Your credit balance will be used to pay the monthly bills.',
@@ -68,10 +70,12 @@ const SWITCH_INFOS = {
export const SettingsBilling = () => { export const SettingsBilling = () => {
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const onboardingStatus = useOnboardingStatus(); const onboardingStatus = useOnboardingStatus();
const subscriptionStatus = useSubscriptionStatus();
const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentWorkspace = useRecoilValue(currentWorkspaceState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const switchingInfo = const switchingInfo =
currentWorkspace?.currentBillingSubscription?.interval === 'year' currentWorkspace?.currentBillingSubscription?.interval ===
SubscriptionInterval.Year
? SWITCH_INFOS.year ? SWITCH_INFOS.year
: SWITCH_INFOS.month; : SWITCH_INFOS.month;
const [isSwitchingIntervalModalOpen, setIsSwitchingIntervalModalOpen] = const [isSwitchingIntervalModalOpen, setIsSwitchingIntervalModalOpen] =
@@ -94,14 +98,15 @@ export const SettingsBilling = () => {
onboardingStatus !== OnboardingStatus.Completed; onboardingStatus !== OnboardingStatus.Completed;
const displayPaymentFailInfo = const displayPaymentFailInfo =
onboardingStatus === OnboardingStatus.PastDue || subscriptionStatus === SubscriptionStatus.PastDue ||
onboardingStatus === OnboardingStatus.Unpaid; subscriptionStatus === SubscriptionStatus.Unpaid;
const displaySubscriptionCanceledInfo = const displaySubscriptionCanceledInfo =
onboardingStatus === OnboardingStatus.Canceled; subscriptionStatus === SubscriptionStatus.Canceled;
const displaySubscribeInfo = const displaySubscribeInfo =
onboardingStatus === OnboardingStatus.CompletedWithoutSubscription; onboardingStatus === OnboardingStatus.Completed &&
!isDefined(subscriptionStatus);
const openBillingPortal = () => { const openBillingPortal = () => {
if (isDefined(data) && isDefined(data.billingPortalSession.url)) { if (isDefined(data) && isDefined(data.billingPortalSession.url)) {
@@ -153,22 +158,20 @@ export const SettingsBilling = () => {
/> />
)} )}
{displaySubscriptionCanceledInfo && ( {displaySubscriptionCanceledInfo && (
<UndecoratedLink to={AppPath.PlanRequired}> <Info
<Info text={'Subscription canceled. Please start a new one'}
text={'Subscription canceled. Please start a new one'} buttonTitle={'Subscribe'}
buttonTitle={'Subscribe'} accent={'danger'}
accent={'danger'} to={AppPath.PlanRequired}
/> />
</UndecoratedLink>
)} )}
{displaySubscribeInfo ? ( {displaySubscribeInfo ? (
<UndecoratedLink to={AppPath.PlanRequired}> <Info
<Info text={'Your workspace does not have an active subscription'}
text={'Your workspace does not have an active subscription'} buttonTitle={'Subscribe'}
buttonTitle={'Subscribe'} accent={'danger'}
accent={'danger'} to={AppPath.PlanRequired}
/> />
</UndecoratedLink>
) : ( ) : (
<> <>
<Section> <Section>

View File

@@ -8,7 +8,7 @@ import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/Obje
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext'; import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getRecordChipGeneratorPerObjectPerField } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField'; import { getRecordChipGeneratorPerObjectPerField } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
import { mockedUsersData } from '~/testing/mock-data/users'; import { mockedUserData } from '~/testing/mock-data/users';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
export const ObjectMetadataItemsDecorator: Decorator = (Story) => { export const ObjectMetadataItemsDecorator: Decorator = (Story) => {
@@ -20,7 +20,7 @@ export const ObjectMetadataItemsDecorator: Decorator = (Story) => {
useEffect(() => { useEffect(() => {
setCurrentWorkspaceMember(mockWorkspaceMembers[0]); setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
setCurrentUser(mockedUsersData[0]); setCurrentUser(mockedUserData);
}, [setCurrentUser, setCurrentWorkspaceMember]); }, [setCurrentUser, setCurrentWorkspaceMember]);
const chipGeneratorPerObjectPerField = useMemo(() => { const chipGeneratorPerObjectPerField = useMemo(() => {

View File

@@ -15,7 +15,7 @@ import { mockedClientConfig } from '~/testing/mock-data/config';
import { mockedObjectMetadataItemsQueryResult } from '~/testing/mock-data/metadata'; import { mockedObjectMetadataItemsQueryResult } from '~/testing/mock-data/metadata';
import { getPeopleMock } from '~/testing/mock-data/people'; import { getPeopleMock } from '~/testing/mock-data/people';
import { mockedRemoteTables } from '~/testing/mock-data/remote-tables'; import { mockedRemoteTables } from '~/testing/mock-data/remote-tables';
import { mockedUsersData } from '~/testing/mock-data/users'; import { mockedUserData } from '~/testing/mock-data/users';
import { mockedViewsData } from '~/testing/mock-data/views'; import { mockedViewsData } from '~/testing/mock-data/views';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
@@ -35,7 +35,7 @@ export const graphqlMocks = {
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: mockedUsersData[0], currentUser: mockedUserData,
}, },
}); });
}), }),

View File

@@ -38,4 +38,5 @@ export const mockedClientConfig: ClientConfig = {
siteKey: 'MOCKED_SITE_KEY', siteKey: 'MOCKED_SITE_KEY',
__typename: 'Captcha', __typename: 'Captcha',
}, },
api: { mutationMaximumAffectedRecords: 100 },
}; };

View File

@@ -1,5 +1,11 @@
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { User, Workspace } from '~/generated/graphql'; import {
OnboardingStatus,
SubscriptionInterval,
SubscriptionStatus,
User,
Workspace,
} from '~/generated/graphql';
type MockedUser = Pick< type MockedUser = Pick<
User, User,
@@ -10,7 +16,7 @@ type MockedUser = Pick<
| 'canImpersonate' | 'canImpersonate'
| '__typename' | '__typename'
| 'supportUserHash' | 'supportUserHash'
| 'onboardingStep' | 'onboardingStatus'
> & { > & {
workspaceMember: WorkspaceMember | null; workspaceMember: WorkspaceMember | null;
locale: string; locale: string;
@@ -30,7 +36,6 @@ export const mockDefaultWorkspace: Workspace = {
inviteHash: 'twenty.com-invite-hash', inviteHash: 'twenty.com-invite-hash',
logo: workspaceLogoUrl, logo: workspaceLogoUrl,
allowImpersonation: true, allowImpersonation: true,
subscriptionStatus: 'active',
activationStatus: 'active', activationStatus: 'active',
featureFlags: [ featureFlags: [
{ {
@@ -58,8 +63,8 @@ export const mockDefaultWorkspace: Workspace = {
currentBillingSubscription: { currentBillingSubscription: {
__typename: 'BillingSubscription', __typename: 'BillingSubscription',
id: '7efbc3f7-6e5e-4128-957e-8d86808cdf6a', id: '7efbc3f7-6e5e-4128-957e-8d86808cdf6a',
interval: 'month', interval: SubscriptionInterval.Month,
status: 'active', status: SubscriptionStatus.Active,
}, },
}; };
@@ -79,49 +84,26 @@ export const mockedWorkspaceMemberData: WorkspaceMember = {
userEmail: 'charles@test.com', userEmail: 'charles@test.com',
}; };
export const mockedUsersData: Array<MockedUser> = [ export const mockedUserData: MockedUser = {
{ id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', __typename: 'User',
__typename: 'User', email: 'charles@test.com',
email: 'charles@test.com', firstName: 'Charles',
firstName: 'Charles', lastName: 'Test',
lastName: 'Test', canImpersonate: false,
canImpersonate: false, supportUserHash:
supportUserHash: 'a95afad9ff6f0b364e2a3fd3e246a1a852c22b6e55a3ca33745a86c201f9c10d',
'a95afad9ff6f0b364e2a3fd3e246a1a852c22b6e55a3ca33745a86c201f9c10d', workspaceMember: mockedWorkspaceMemberData,
workspaceMember: mockedWorkspaceMemberData, defaultWorkspace: mockDefaultWorkspace,
defaultWorkspace: mockDefaultWorkspace, locale: 'en',
locale: 'en', workspaces: [{ workspace: mockDefaultWorkspace }],
workspaces: [{ workspace: mockDefaultWorkspace }], onboardingStatus: OnboardingStatus.Completed,
onboardingStep: null, };
},
{
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6c',
__typename: 'User',
email: 'felix@test.com',
firstName: 'Felix',
lastName: 'Test',
canImpersonate: false,
supportUserHash:
'54ac3986035961724cdb9a7a30c70e6463a4b68f0ecd2014c727171a82144b74',
workspaceMember: {
...mockedWorkspaceMemberData,
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6c',
name: {
firstName: 'Felix',
lastName: 'Test',
},
userId: '81aeb270-d689-4515-bd5d-35dbe956da3b',
},
defaultWorkspace: mockDefaultWorkspace,
locale: 'en',
workspaces: [{ workspace: mockDefaultWorkspace }],
onboardingStep: null,
},
];
export const mockedOnboardingUsersData: Array<MockedUser> = [ export const mockedOnboardingUserData = (
{ onboardingStatus?: OnboardingStatus,
) => {
return {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d',
__typename: 'User', __typename: 'User',
email: 'workspace-onboarding@test.com', email: 'workspace-onboarding@test.com',
@@ -130,35 +112,10 @@ export const mockedOnboardingUsersData: Array<MockedUser> = [
canImpersonate: false, canImpersonate: false,
supportUserHash: supportUserHash:
'4fb61d34ed3a4aeda2476d4b308b5162db9e1809b2b8277e6fdc6efc4a609254', '4fb61d34ed3a4aeda2476d4b308b5162db9e1809b2b8277e6fdc6efc4a609254',
workspaceMember: { workspaceMember: null,
...mockedWorkspaceMemberData,
id: 'd454f075-c72f-4ebe-bac7-d28e75e74a23',
name: {
firstName: '',
lastName: '',
},
userId: '7f793378-b939-43b7-8642-292c9510754c',
},
defaultWorkspace: mockDefaultWorkspace, defaultWorkspace: mockDefaultWorkspace,
locale: 'en', locale: 'en',
workspaces: [{ workspace: mockDefaultWorkspace }], workspaces: [{ workspace: mockDefaultWorkspace }],
onboardingStep: null, onboardingStatus: onboardingStatus || null,
}, };
{ };
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d',
__typename: 'User',
email: 'profile-onboarding@test.com',
firstName: '',
lastName: '',
canImpersonate: false,
workspaceMember: null,
defaultWorkspace: {
...mockDefaultWorkspace,
activationStatus: 'inactive',
},
locale: 'en',
workspaces: [{ workspace: mockDefaultWorkspace }],
onboardingStep: null,
},
];

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,61 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UseEnumForSubscriptionStatusInterval1719327438923
implements MigrationInterface
{
name = 'UseEnumForSubscriptionStatusInterval1719327438923';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "core"."billingSubscription_status_enum" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid')`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE "core"."billingSubscription_status_enum" USING "status"::"core"."billingSubscription_status_enum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" SET NOT NULL`,
);
await queryRunner.query(
`CREATE TYPE "core"."billingSubscription_interval_enum" AS ENUM('day', 'month', 'week', 'year')`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE "core"."billingSubscription_interval_enum" USING "interval"::"core"."billingSubscription_interval_enum"`,
);
await queryRunner.query(
`CREATE TYPE "core"."workspace_subscriptionstatus_enum" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid')`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" DROP DEFAULT`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" TYPE "core"."workspace_subscriptionstatus_enum" USING "subscriptionStatus"::"core"."workspace_subscriptionstatus_enum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" SET DEFAULT 'incomplete'::"core"."workspace_subscriptionstatus_enum"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" TYPE text`,
);
await queryRunner.query(
`DROP TYPE "core"."workspace_subscriptionstatus_enum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE text`,
);
await queryRunner.query(
`DROP TYPE "core"."billingSubscription_interval_enum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE text`,
);
await queryRunner.query(
`DROP TYPE "core"."billingSubscription_status_enum"`,
);
}
}

View File

@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RemoveSubscriptionStatusFromCoreWorkspace1719494707738
implements MigrationInterface
{
name = 'RemoveSubscriptionStatusFromCoreWorkspace1719494707738';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP COLUMN "subscriptionStatus"`,
);
await queryRunner.query(
`DROP TYPE "core"."workspace_subscriptionstatus_enum"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "core"."workspace_subscriptionstatus_enum" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid')`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD "subscriptionStatus" "core"."workspace_subscriptionstatus_enum" NOT NULL DEFAULT 'incomplete'`,
);
}
}

View File

@@ -30,6 +30,7 @@ import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/stan
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { AuthResolver } from './auth.resolver'; import { AuthResolver } from './auth.resolver';
@@ -65,6 +66,7 @@ const jwtModule = JwtModule.registerAsync({
]), ]),
HttpModule, HttpModule,
UserWorkspaceModule, UserWorkspaceModule,
WorkspaceModule,
OnboardingModule, OnboardingModule,
TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]), TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]),
WorkspaceDataSourceModule, WorkspaceDataSourceModule,

View File

@@ -8,6 +8,7 @@ import { EnvironmentService } from 'src/engine/integrations/environment/environm
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service'; import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
describe('SignInUpService', () => { describe('SignInUpService', () => {
let service: SignInUpService; let service: SignInUpService;
@@ -40,6 +41,10 @@ describe('SignInUpService', () => {
provide: HttpService, provide: HttpService,
useValue: {}, useValue: {},
}, },
{
provide: WorkspaceService,
useValue: {},
},
], ],
}).compile(); }).compile();

View File

@@ -24,6 +24,7 @@ import { FileUploadService } from 'src/engine/core-modules/file/file-upload/serv
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { getImageBufferFromUrl } from 'src/utils/image'; import { getImageBufferFromUrl } from 'src/utils/image';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
export type SignInUpServiceInput = { export type SignInUpServiceInput = {
email: string; email: string;
@@ -44,6 +45,7 @@ export class SignInUpService {
@InjectRepository(User, 'core') @InjectRepository(User, 'core')
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
private readonly userWorkspaceService: UserWorkspaceService, private readonly userWorkspaceService: UserWorkspaceService,
private readonly workspaceService: WorkspaceService,
private readonly httpService: HttpService, private readonly httpService: HttpService,
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
) {} ) {}
@@ -142,10 +144,12 @@ export class SignInUpService {
ForbiddenException, ForbiddenException,
); );
const isWorkspaceActivated =
await this.workspaceService.isWorkspaceActivated(workspace.id);
assert( assert(
!this.environmentService.get('IS_BILLING_ENABLED') || isWorkspaceActivated,
workspace.subscriptionStatus !== 'incomplete', 'Workspace is not ready to welcome new members',
'Workspace subscription status is incomplete',
ForbiddenException, ForbiddenException,
); );
@@ -199,7 +203,6 @@ export class SignInUpService {
displayName: '', displayName: '',
domainName: '', domainName: '',
inviteHash: v4(), inviteHash: v4(),
subscriptionStatus: 'incomplete',
}); });
const workspace = await this.workspaceRepository.save(workspaceToCreate); const workspace = await this.workspaceRepository.save(workspaceToCreate);

View File

@@ -10,13 +10,19 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver'; import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver';
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener'; import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@Module({ @Module({
imports: [ imports: [
StripeModule, StripeModule,
UserWorkspaceModule, UserWorkspaceModule,
TypeOrmModule.forFeature( TypeOrmModule.forFeature(
[BillingSubscription, BillingSubscriptionItem, Workspace], [
BillingSubscription,
BillingSubscriptionItem,
Workspace,
FeatureFlagEntity,
],
'core', 'core',
), ),
], ],

View File

@@ -2,17 +2,25 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { Not, Repository } from 'typeorm'; import { In, Not, Repository } from 'typeorm';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import {
BillingSubscription,
SubscriptionInterval,
SubscriptionStatus,
} from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity'; import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import {
FeatureFlagEntity,
FeatureFlagKeys,
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
export enum AvailableProduct { export enum AvailableProduct {
BasePlan = 'base-plan', BasePlan = 'base-plan',
@@ -34,12 +42,45 @@ export class BillingService {
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
@InjectRepository(BillingSubscription, 'core') @InjectRepository(BillingSubscription, 'core')
private readonly billingSubscriptionRepository: Repository<BillingSubscription>, private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
@InjectRepository(FeatureFlagEntity, 'core')
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
@InjectRepository(BillingSubscriptionItem, 'core') @InjectRepository(BillingSubscriptionItem, 'core')
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItem>, private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItem>,
@InjectRepository(Workspace, 'core') @InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>, private readonly workspaceRepository: Repository<Workspace>,
) {} ) {}
async getActiveSubscriptionWorkspaceIds() {
return (
await this.workspaceRepository.find({
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
currentBillingSubscription: {
status: In([
SubscriptionStatus.Active,
SubscriptionStatus.Trialing,
SubscriptionStatus.PastDue,
]),
},
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
}
async isBillingEnabledForWorkspace(workspaceId: string) {
const isFreeAccessEnabled = await this.featureFlagRepository.findOneBy({
workspaceId,
key: FeatureFlagKeys.IsFreeAccessEnabled,
value: true,
});
return (
!isFreeAccessEnabled && this.environmentService.get('IS_BILLING_ENABLED')
);
}
getProductStripeId(product: AvailableProduct) { getProductStripeId(product: AvailableProduct) {
if (product === AvailableProduct.BasePlan) { if (product === AvailableProduct.BasePlan) {
return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID'); return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID');
@@ -84,13 +125,13 @@ export class BillingService {
}) { }) {
const notCanceledSubscriptions = const notCanceledSubscriptions =
await this.billingSubscriptionRepository.find({ await this.billingSubscriptionRepository.find({
where: { ...criteria, status: Not('canceled') }, where: { ...criteria, status: Not(SubscriptionStatus.Canceled) },
relations: ['billingSubscriptionItems'], relations: ['billingSubscriptionItems'],
}); });
assert( assert(
notCanceledSubscriptions.length <= 1, notCanceledSubscriptions.length <= 1,
`More than on not canceled subscription for workspace ${criteria.workspaceId}`, `More than one not canceled subscription for workspace ${criteria.workspaceId}`,
); );
return notCanceledSubscriptions?.[0]; return notCanceledSubscriptions?.[0];
@@ -171,7 +212,9 @@ export class BillingService {
workspaceId: user.defaultWorkspaceId, workspaceId: user.defaultWorkspaceId,
}); });
const newInterval = const newInterval =
billingSubscription?.interval === 'year' ? 'month' : 'year'; billingSubscription?.interval === SubscriptionInterval.Year
? SubscriptionInterval.Month
: SubscriptionInterval.Year;
const billingSubscriptionItem = await this.getBillingSubscriptionItem( const billingSubscriptionItem = await this.getBillingSubscriptionItem(
user.defaultWorkspaceId, user.defaultWorkspaceId,
); );
@@ -265,10 +308,6 @@ export class BillingService {
return; return;
} }
await this.workspaceRepository.update(workspaceId, {
subscriptionStatus: data.object.status,
});
await this.billingSubscriptionRepository.upsert( await this.billingSubscriptionRepository.upsert(
{ {
workspaceId: workspaceId, workspaceId: workspaceId,
@@ -302,5 +341,10 @@ export class BillingService {
skipUpdateIfNoValuesChanged: true, skipUpdateIfNoValuesChanged: true,
}, },
); );
await this.featureFlagRepository.delete({
workspaceId,
key: FeatureFlagKeys.IsFreeAccessEnabled,
});
} }
} }

View File

@@ -3,9 +3,11 @@ import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
@ArgsType() @ArgsType()
export class CheckoutSessionInput { export class CheckoutSessionInput {
@Field(() => String) @Field(() => SubscriptionInterval)
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
recurringInterval: Stripe.Price.Recurring.Interval; recurringInterval: Stripe.Price.Recurring.Interval;

View File

@@ -2,9 +2,11 @@ import { Field, ObjectType } from '@nestjs/graphql';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
@ObjectType() @ObjectType()
export class ProductPriceEntity { export class ProductPriceEntity {
@Field(() => String) @Field(() => SubscriptionInterval)
recurringInterval: Stripe.Price.Recurring.Interval; recurringInterval: Stripe.Price.Recurring.Interval;
@Field(() => Number) @Field(() => Number)

View File

@@ -1,4 +1,4 @@
import { Field, ObjectType } from '@nestjs/graphql'; import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { import {
Column, Column,
@@ -18,6 +18,27 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
export enum SubscriptionStatus {
Active = 'active',
Canceled = 'canceled',
Incomplete = 'incomplete',
IncompleteExpired = 'incomplete_expired',
PastDue = 'past_due',
Paused = 'paused',
Trialing = 'trialing',
Unpaid = 'unpaid',
}
export enum SubscriptionInterval {
Day = 'day',
Month = 'month',
Week = 'week',
Year = 'year',
}
registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' });
registerEnumType(SubscriptionInterval, { name: 'SubscriptionInterval' });
@Entity({ name: 'billingSubscription', schema: 'core' }) @Entity({ name: 'billingSubscription', schema: 'core' })
@ObjectType('BillingSubscription') @ObjectType('BillingSubscription')
export class BillingSubscription { export class BillingSubscription {
@@ -49,12 +70,20 @@ export class BillingSubscription {
@Column({ unique: true, nullable: false }) @Column({ unique: true, nullable: false })
stripeSubscriptionId: string; stripeSubscriptionId: string;
@Field(() => String) @Field(() => SubscriptionStatus)
@Column({ type: 'text', nullable: false }) @Column({
type: 'enum',
enum: Object.values(SubscriptionStatus),
nullable: false,
})
status: Stripe.Subscription.Status; status: Stripe.Subscription.Status;
@Field(() => String, { nullable: true }) @Field(() => SubscriptionInterval, { nullable: true })
@Column({ type: 'text', nullable: true }) @Column({
type: 'enum',
enum: Object.values(SubscriptionInterval),
nullable: true,
})
interval: Stripe.Price.Recurring.Interval; interval: Stripe.Price.Recurring.Interval;
@OneToMany( @OneToMany(

View File

@@ -9,15 +9,15 @@ import {
UpdateSubscriptionJob, UpdateSubscriptionJob,
UpdateSubscriptionJobData, UpdateSubscriptionJobData,
} from 'src/engine/core-modules/billing/jobs/update-subscription.job'; } from 'src/engine/core-modules/billing/jobs/update-subscription.job';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Injectable() @Injectable()
export class BillingWorkspaceMemberListener { export class BillingWorkspaceMemberListener {
constructor( constructor(
@InjectMessageQueue(MessageQueue.billingQueue) @InjectMessageQueue(MessageQueue.billingQueue)
private readonly messageQueueService: MessageQueueService, private readonly messageQueueService: MessageQueueService,
private readonly environmentService: EnvironmentService, private readonly billingService: BillingService,
) {} ) {}
@OnEvent('workspaceMember.created') @OnEvent('workspaceMember.created')
@@ -25,7 +25,12 @@ export class BillingWorkspaceMemberListener {
async handleCreateOrDeleteEvent( async handleCreateOrDeleteEvent(
payload: ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity>, payload: ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity>,
) { ) {
if (!this.environmentService.get('IS_BILLING_ENABLED')) { const isBillingEnabledForWorkspace =
await this.billingService.isBillingEnabledForWorkspace(
payload.workspaceId,
);
if (!isBillingEnabledForWorkspace) {
return; return;
} }

View File

@@ -23,6 +23,7 @@ export enum FeatureFlagKeys {
IsStripeIntegrationEnabled = 'IS_STRIPE_INTEGRATION_ENABLED', IsStripeIntegrationEnabled = 'IS_STRIPE_INTEGRATION_ENABLED',
IsContactCreationForSentAndReceivedEmailsEnabled = 'IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED', IsContactCreationForSentAndReceivedEmailsEnabled = 'IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED',
IsGoogleCalendarSyncV2Enabled = 'IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED', IsGoogleCalendarSyncV2Enabled = 'IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED',
IsFreeAccessEnabled = 'IS_FREE_ACCESS_ENABLED',
} }
@Entity({ name: 'featureFlag', schema: 'core' }) @Entity({ name: 'featureFlag', schema: 'core' })

View File

@@ -0,0 +1,8 @@
export enum OnboardingStatus {
PLAN_REQUIRED = 'PLAN_REQUIRED',
WORKSPACE_ACTIVATION = 'WORKSPACE_ACTIVATION',
PROFILE_CREATION = 'PROFILE_CREATION',
SYNC_EMAIL = 'SYNC_EMAIL',
INVITE_TEAM = 'INVITE_TEAM',
COMPLETED = 'COMPLETED',
}

View File

@@ -1,4 +0,0 @@
export enum OnboardingStep {
SYNC_EMAIL = 'SYNC_EMAIL',
INVITE_TEAM = 'INVITE_TEAM',
}

View File

@@ -5,9 +5,19 @@ import { OnboardingResolver } from 'src/engine/core-modules/onboarding/onboardin
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module'; import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
@Module({ @Module({
imports: [DataSourceModule, UserWorkspaceModule, KeyValuePairModule], imports: [
DataSourceModule,
WorkspaceManagerModule,
UserWorkspaceModule,
KeyValuePairModule,
EnvironmentModule,
BillingModule,
],
exports: [OnboardingService], exports: [OnboardingService],
providers: [OnboardingService, OnboardingResolver], providers: [OnboardingService, OnboardingResolver],
}) })

View File

@@ -1,13 +1,20 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service'; import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum'; import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { isDefined } from 'src/utils/is-defined';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
enum OnboardingStepValues { enum OnboardingStepValues {
SKIPPED = 'SKIPPED', SKIPPED = 'SKIPPED',
@@ -26,29 +33,71 @@ type OnboardingKeyValueType = {
@Injectable() @Injectable()
export class OnboardingService { export class OnboardingService {
constructor( constructor(
private readonly billingService: BillingService,
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly userWorkspaceService: UserWorkspaceService, private readonly userWorkspaceService: UserWorkspaceService,
private readonly keyValuePairService: KeyValuePairService<OnboardingKeyValueType>, private readonly keyValuePairService: KeyValuePairService<OnboardingKeyValueType>,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository, private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectWorkspaceRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberRepository: WorkspaceRepository<WorkspaceMemberWorkspaceEntity>,
) {} ) {}
private async isSyncEmailOnboardingStep(user: User, workspace: Workspace) { private async isSubscriptionIncompleteOnboardingStatus(user: User) {
const isBillingEnabledForWorkspace =
await this.billingService.isBillingEnabledForWorkspace(
user.defaultWorkspaceId,
);
if (!isBillingEnabledForWorkspace) {
return false;
}
const currentBillingSubscription =
await this.billingService.getCurrentBillingSubscription({
workspaceId: user.defaultWorkspaceId,
});
return (
!isDefined(currentBillingSubscription) ||
currentBillingSubscription?.status === SubscriptionStatus.Incomplete
);
}
private async isWorkspaceActivationOnboardingStatus(user: User) {
return !(await this.workspaceManagerService.doesDataSourceExist(
user.defaultWorkspaceId,
));
}
private async isProfileCreationOnboardingStatus(user: User) {
const workspaceMember = await this.workspaceMemberRepository.findOneBy({
userId: user.id,
});
return (
workspaceMember &&
(!workspaceMember.name.firstName || !workspaceMember.name.lastName)
);
}
private async isSyncEmailOnboardingStatus(user: User) {
const syncEmailValue = await this.keyValuePairService.get({ const syncEmailValue = await this.keyValuePairService.get({
userId: user.id, userId: user.id,
workspaceId: workspace.id, workspaceId: user.defaultWorkspaceId,
key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP, key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP,
}); });
const isSyncEmailSkipped = syncEmailValue === OnboardingStepValues.SKIPPED; const isSyncEmailSkipped = syncEmailValue === OnboardingStepValues.SKIPPED;
const connectedAccounts = const connectedAccounts =
await this.connectedAccountRepository.getAllByUserId( await this.connectedAccountRepository.getAllByUserId(
user.id, user.id,
workspace.id, user.defaultWorkspaceId,
); );
return !isSyncEmailSkipped && !connectedAccounts?.length; return !isSyncEmailSkipped && !connectedAccounts?.length;
} }
private async isInviteTeamOnboardingStep(workspace: Workspace) { private async isInviteTeamOnboardingStatus(workspace: Workspace) {
const inviteTeamValue = await this.keyValuePairService.get({ const inviteTeamValue = await this.keyValuePairService.get({
workspaceId: workspace.id, workspaceId: workspace.id,
key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP, key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP,
@@ -64,19 +113,28 @@ export class OnboardingService {
); );
} }
async getOnboardingStep( async getOnboardingStatus(user: User) {
user: User, if (await this.isSubscriptionIncompleteOnboardingStatus(user)) {
workspace: Workspace, return OnboardingStatus.PLAN_REQUIRED;
): Promise<OnboardingStep | null> {
if (await this.isSyncEmailOnboardingStep(user, workspace)) {
return OnboardingStep.SYNC_EMAIL;
} }
if (await this.isInviteTeamOnboardingStep(workspace)) { if (await this.isWorkspaceActivationOnboardingStatus(user)) {
return OnboardingStep.INVITE_TEAM; return OnboardingStatus.WORKSPACE_ACTIVATION;
} }
return null; if (await this.isProfileCreationOnboardingStatus(user)) {
return OnboardingStatus.PROFILE_CREATION;
}
if (await this.isSyncEmailOnboardingStatus(user)) {
return OnboardingStatus.SYNC_EMAIL;
}
if (await this.isInviteTeamOnboardingStatus(user.defaultWorkspace)) {
return OnboardingStatus.INVITE_TEAM;
}
return OnboardingStatus.COMPLETED;
} }
async skipInviteTeamOnboardingStep(workspaceId: string) { async skipInviteTeamOnboardingStep(workspaceId: string) {

View File

@@ -107,9 +107,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
return undefined; return undefined;
} }
const workspaceMemberCount = await this.workspaceMemberRepository.count(); return await this.workspaceMemberRepository.count();
return workspaceMemberCount;
} }
async checkUserWorkspaceExists( async checkUserWorkspaceExists(

View File

@@ -18,11 +18,11 @@ import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-mem
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum'; import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
registerEnumType(OnboardingStep, { registerEnumType(OnboardingStatus, {
name: 'OnboardingStep', name: 'OnboardingStatus',
description: 'Onboarding step', description: 'Onboarding status',
}); });
@Entity({ name: 'user', schema: 'core' }) @Entity({ name: 'user', schema: 'core' })
@@ -119,6 +119,6 @@ export class User {
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user) @OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user)
workspaces: Relation<UserWorkspace[]>; workspaces: Relation<UserWorkspace[]>;
@Field(() => OnboardingStep, { nullable: true }) @Field(() => OnboardingStatus, { nullable: true })
onboardingStep: OnboardingStep; onboardingStatus: OnboardingStatus;
} }

View File

@@ -27,7 +27,7 @@ import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum'; import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';
@@ -118,17 +118,13 @@ export class UserResolver {
return this.userService.deleteUser(userId); return this.userService.deleteUser(userId);
} }
@ResolveField(() => OnboardingStep) @ResolveField(() => OnboardingStatus)
async onboardingStep(@Parent() user: User): Promise<OnboardingStep | null> { async onboardingStatus(@Parent() user: User): Promise<OnboardingStatus> {
if (!user) {
return null;
}
const contextInstance = await this.loadServiceWithWorkspaceContext.load( const contextInstance = await this.loadServiceWithWorkspaceContext.load(
this.onboardingService, this.onboardingService,
user.defaultWorkspaceId, user.defaultWorkspaceId,
); );
return contextInstance.getOnboardingStep(user, user.defaultWorkspace); return contextInstance.getOnboardingStatus(user);
} }
} }

View File

@@ -10,7 +10,6 @@ import {
Relation, Relation,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import Stripe from 'stripe';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@@ -85,10 +84,6 @@ export class Workspace {
@OneToMany(() => FeatureFlagEntity, (featureFlag) => featureFlag.workspace) @OneToMany(() => FeatureFlagEntity, (featureFlag) => featureFlag.workspace)
featureFlags: Relation<FeatureFlagEntity[]>; featureFlags: Relation<FeatureFlagEntity[]>;
@Field(() => String)
@Column({ type: 'text', default: 'incomplete' })
subscriptionStatus: Stripe.Subscription.Status;
@Field({ nullable: true }) @Field({ nullable: true })
currentBillingSubscription: BillingSubscription; currentBillingSubscription: BillingSubscription;

View File

@@ -8,6 +8,7 @@ import { WorkspaceService } from 'src/engine/core-modules/workspace/services/wor
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header'; import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
type DeleteIncompleteWorkspacesCommandOptions = { type DeleteIncompleteWorkspacesCommandOptions = {
dryRun?: boolean; dryRun?: boolean;
@@ -52,7 +53,7 @@ export class DeleteIncompleteWorkspacesCommand extends CommandRunner {
options: DeleteIncompleteWorkspacesCommandOptions, options: DeleteIncompleteWorkspacesCommandOptions,
): Promise<void> { ): Promise<void> {
const where: FindOptionsWhere<Workspace> = { const where: FindOptionsWhere<Workspace> = {
subscriptionStatus: 'incomplete', currentBillingSubscription: { status: SubscriptionStatus.Incomplete },
}; };
if (options.workspaceIds) { if (options.workspaceIds) {

View File

@@ -60,6 +60,7 @@ export class AddStandardIdCommand extends CommandRunner {
IS_STRIPE_INTEGRATION_ENABLED: false, IS_STRIPE_INTEGRATION_ENABLED: false,
IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED: true, IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED: true,
IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true, IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true,
IS_FREE_ACCESS_ENABLED: false,
}, },
); );
const standardFieldMetadataCollection = this.standardFieldFactory.create( const standardFieldMetadataCollection = this.standardFieldFactory.create(
@@ -76,6 +77,7 @@ export class AddStandardIdCommand extends CommandRunner {
IS_STRIPE_INTEGRATION_ENABLED: false, IS_STRIPE_INTEGRATION_ENABLED: false,
IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED: true, IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED: true,
IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true, IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true,
IS_FREE_ACCESS_ENABLED: false,
}, },
); );

View File

@@ -2,16 +2,17 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { GoogleCalendarSyncCronJob } from 'src/modules/calendar/crons/jobs/google-calendar-sync.cron.job'; import { GoogleCalendarSyncCronJob } from 'src/modules/calendar/crons/jobs/google-calendar-sync.cron.job';
import { WorkspaceGoogleCalendarSyncModule } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.module'; import { WorkspaceGoogleCalendarSyncModule } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Workspace, FeatureFlagEntity], 'core'), TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
TypeOrmModule.forFeature([DataSourceEntity], 'metadata'), TypeOrmModule.forFeature([DataSourceEntity], 'metadata'),
WorkspaceGoogleCalendarSyncModule, WorkspaceGoogleCalendarSyncModule,
BillingModule,
], ],
providers: [GoogleCalendarSyncCronJob], providers: [GoogleCalendarSyncCronJob],
}) })

View File

@@ -3,13 +3,12 @@ import { Scope } from '@nestjs/common';
import { Repository, In } from 'typeorm'; import { Repository, In } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { WorkspaceGoogleCalendarSyncService } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.service'; import { WorkspaceGoogleCalendarSyncService } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Processor({ @Processor({
queueName: MessageQueue.cronQueue, queueName: MessageQueue.cronQueue,
@@ -17,26 +16,16 @@ import { Process } from 'src/engine/integrations/message-queue/decorators/proces
}) })
export class GoogleCalendarSyncCronJob { export class GoogleCalendarSyncCronJob {
constructor( constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(DataSourceEntity, 'metadata') @InjectRepository(DataSourceEntity, 'metadata')
private readonly dataSourceRepository: Repository<DataSourceEntity>, private readonly dataSourceRepository: Repository<DataSourceEntity>,
private readonly workspaceGoogleCalendarSyncService: WorkspaceGoogleCalendarSyncService, private readonly workspaceGoogleCalendarSyncService: WorkspaceGoogleCalendarSyncService,
private readonly environmentService: EnvironmentService, private readonly billingService: BillingService,
) {} ) {}
@Process(GoogleCalendarSyncCronJob.name) @Process(GoogleCalendarSyncCronJob.name)
async handle(): Promise<void> { async handle(): Promise<void> {
const workspaceIds = ( const workspaceIds =
await this.workspaceRepository.find({ await this.billingService.getActiveSubscriptionWorkspaceIds();
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
subscriptionStatus: In(['active', 'trialing', 'past_due']),
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
const dataSources = await this.dataSourceRepository.find({ const dataSources = await this.dataSourceRepository.find({
where: { where: {

View File

@@ -3,12 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm'; import { Repository, In } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository'; import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
import { import {
MessageChannelSyncStage, MessageChannelSyncStage,
@@ -21,35 +19,26 @@ import {
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Processor(MessageQueue.cronQueue) @Processor(MessageQueue.cronQueue)
export class MessagingMessageListFetchCronJob { export class MessagingMessageListFetchCronJob {
private readonly logger = new Logger(MessagingMessageListFetchCronJob.name); private readonly logger = new Logger(MessagingMessageListFetchCronJob.name);
constructor( constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(DataSourceEntity, 'metadata') @InjectRepository(DataSourceEntity, 'metadata')
private readonly dataSourceRepository: Repository<DataSourceEntity>, private readonly dataSourceRepository: Repository<DataSourceEntity>,
@InjectMessageQueue(MessageQueue.messagingQueue) @InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService, private readonly messageQueueService: MessageQueueService,
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity) @InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelRepository: MessageChannelRepository, private readonly messageChannelRepository: MessageChannelRepository,
private readonly environmentService: EnvironmentService, private readonly billingService: BillingService,
) {} ) {}
@Process(MessagingMessageListFetchCronJob.name) @Process(MessagingMessageListFetchCronJob.name)
async handle(): Promise<void> { async handle(): Promise<void> {
const workspaceIds = ( const workspaceIds =
await this.workspaceRepository.find({ await this.billingService.getActiveSubscriptionWorkspaceIds();
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
subscriptionStatus: In(['active', 'trialing', 'past_due']),
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
const dataSources = await this.dataSourceRepository.find({ const dataSources = await this.dataSourceRepository.find({
where: { where: {

View File

@@ -3,9 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm'; import { Repository, In } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
import { import {
@@ -21,35 +19,26 @@ import {
MessageChannelSyncStage, MessageChannelSyncStage,
MessageChannelWorkspaceEntity, MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Processor(MessageQueue.cronQueue) @Processor(MessageQueue.cronQueue)
export class MessagingMessagesImportCronJob { export class MessagingMessagesImportCronJob {
private readonly logger = new Logger(MessagingMessagesImportCronJob.name); private readonly logger = new Logger(MessagingMessagesImportCronJob.name);
constructor( constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(DataSourceEntity, 'metadata') @InjectRepository(DataSourceEntity, 'metadata')
private readonly dataSourceRepository: Repository<DataSourceEntity>, private readonly dataSourceRepository: Repository<DataSourceEntity>,
private readonly environmentService: EnvironmentService,
@InjectMessageQueue(MessageQueue.messagingQueue) @InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService, private readonly messageQueueService: MessageQueueService,
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity) @InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelRepository: MessageChannelRepository, private readonly messageChannelRepository: MessageChannelRepository,
private readonly billingService: BillingService,
) {} ) {}
@Process(MessagingMessagesImportCronJob.name) @Process(MessagingMessagesImportCronJob.name)
async handle(): Promise<void> { async handle(): Promise<void> {
const workspaceIds = ( const workspaceIds =
await this.workspaceRepository.find({ await this.billingService.getActiveSubscriptionWorkspaceIds();
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
subscriptionStatus: In(['active', 'trialing', 'past_due']),
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
const dataSources = await this.dataSourceRepository.find({ const dataSources = await this.dataSourceRepository.find({
where: { where: {

View File

@@ -2,9 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm'; import { Repository, In } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
@@ -14,31 +12,22 @@ import {
MessagingOngoingStaleJobData, MessagingOngoingStaleJobData,
MessagingOngoingStaleJob, MessagingOngoingStaleJob,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job'; } from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Processor(MessageQueue.cronQueue) @Processor(MessageQueue.cronQueue)
export class MessagingOngoingStaleCronJob { export class MessagingOngoingStaleCronJob {
constructor( constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(DataSourceEntity, 'metadata') @InjectRepository(DataSourceEntity, 'metadata')
private readonly dataSourceRepository: Repository<DataSourceEntity>, private readonly dataSourceRepository: Repository<DataSourceEntity>,
private readonly environmentService: EnvironmentService,
@InjectMessageQueue(MessageQueue.messagingQueue) @InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService, private readonly messageQueueService: MessageQueueService,
private readonly billingService: BillingService,
) {} ) {}
@Process(MessagingOngoingStaleCronJob.name) @Process(MessagingOngoingStaleCronJob.name)
async handle(): Promise<void> { async handle(): Promise<void> {
const workspaceIds = ( const workspaceIds =
await this.workspaceRepository.find({ await this.billingService.getActiveSubscriptionWorkspaceIds();
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
subscriptionStatus: In(['active', 'trialing', 'past_due']),
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
const dataSources = await this.dataSourceRepository.find({ const dataSources = await this.dataSourceRepository.find({
where: { where: {

View File

@@ -20,6 +20,7 @@ import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-impo
import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job'; import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job';
import { MessagingOngoingStaleJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job'; import { MessagingOngoingStaleJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job';
import { MessagingMessageImportManagerMessageChannelListener } from 'src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener'; import { MessagingMessageImportManagerMessageChannelListener } from 'src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
@Module({ @Module({
imports: [ imports: [
@@ -28,6 +29,7 @@ import { MessagingMessageImportManagerMessageChannelListener } from 'src/modules
TypeOrmModule.forFeature([Workspace], 'core'), TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([DataSourceEntity], 'metadata'), TypeOrmModule.forFeature([DataSourceEntity], 'metadata'),
TwentyORMModule.forFeature([MessageChannelWorkspaceEntity]), TwentyORMModule.forFeature([MessageChannelWorkspaceEntity]),
BillingModule,
], ],
providers: [ providers: [
MessagingMessageListFetchCronCommand, MessagingMessageListFetchCronCommand,

View File

@@ -8,12 +8,12 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository'; import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { MessagingTelemetryService } from 'src/modules/messaging/common/services/messaging-telemetry.service'; import { MessagingTelemetryService } from 'src/modules/messaging/common/services/messaging-telemetry.service';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Processor(MessageQueue.cronQueue) @Processor(MessageQueue.cronQueue)
export class MessagingMessageChannelSyncStatusMonitoringCronJob { export class MessagingMessageChannelSyncStatusMonitoringCronJob {
@@ -28,7 +28,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
private readonly dataSourceRepository: Repository<DataSourceEntity>, private readonly dataSourceRepository: Repository<DataSourceEntity>,
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity) @InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelRepository: MessageChannelRepository, private readonly messageChannelRepository: MessageChannelRepository,
private readonly environmentService: EnvironmentService, private readonly billingService: BillingService,
private readonly messagingTelemetryService: MessagingTelemetryService, private readonly messagingTelemetryService: MessagingTelemetryService,
) {} ) {}
@@ -41,16 +41,8 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
message: 'Starting message channel sync status monitoring', message: 'Starting message channel sync status monitoring',
}); });
const workspaceIds = ( const workspaceIds =
await this.workspaceRepository.find({ await this.billingService.getActiveSubscriptionWorkspaceIds();
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
subscriptionStatus: In(['active', 'trialing', 'past_due']),
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
const dataSources = await this.dataSourceRepository.find({ const dataSources = await this.dataSourceRepository.find({
where: { where: {

View File

@@ -6,10 +6,12 @@ import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-s
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { MessagingMessageChannelSyncStatusMonitoringCronCommand } from 'src/modules/messaging/monitoring/crons/commands/messaging-message-channel-sync-status-monitoring.cron.command'; import { MessagingMessageChannelSyncStatusMonitoringCronCommand } from 'src/modules/messaging/monitoring/crons/commands/messaging-message-channel-sync-status-monitoring.cron.command';
import { MessagingMessageChannelSyncStatusMonitoringCronJob } from 'src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron'; import { MessagingMessageChannelSyncStatusMonitoringCronJob } from 'src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
@Module({ @Module({
imports: [ imports: [
MessagingCommonModule, MessagingCommonModule,
BillingModule,
TypeOrmModule.forFeature([Workspace], 'core'), TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([DataSourceEntity], 'metadata'), TypeOrmModule.forFeature([DataSourceEntity], 'metadata'),
], ],

View File

@@ -11,22 +11,6 @@ export class WorkspaceMemberRepository {
private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {} ) {}
public async getByIds(
userIds: string[],
workspaceId: string,
): Promise<WorkspaceMemberWorkspaceEntity[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const result = await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."workspaceMember" WHERE "userId" = ANY($1)`,
[userIds],
workspaceId,
);
return result;
}
public async find(workspaceMemberId: string, workspaceId: string) { public async find(workspaceMemberId: string, workspaceId: string) {
const dataSourceSchema = const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId); this.workspaceDataSourceService.getSchemaName(workspaceId);