diff --git a/README.md b/README.md index 9e7b96fe6..ff28b5986 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,6 @@ # Demo - Go to demo.twenty.com and login with the following credentials: ``` diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index bf7297616..ba9400002 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -117,9 +117,9 @@ export type AvailableWorkspaceOutput = { export type Billing = { __typename?: 'Billing'; - billingFreeTrialDurationInDays?: Maybe; billingUrl?: Maybe; isBillingEnabled: Scalars['Boolean']['output']; + trialPeriods: Array; }; /** The different billing plans available */ @@ -178,6 +178,7 @@ export type ClientConfig = { api: ApiConfig; authProviders: AuthProviders; billing: Billing; + canManageFeatureFlags: Scalars['Boolean']['output']; captcha: Captcha; chromeExtensionId?: Maybe; debugMode: Scalars['Boolean']['output']; @@ -185,7 +186,6 @@ export type ClientConfig = { frontDomain: Scalars['String']['output']; isEmailVerificationRequired: Scalars['Boolean']['output']; isMultiWorkspaceEnabled: Scalars['Boolean']['output']; - isSSOEnabled: Scalars['Boolean']['output']; sentry: Sentry; signInPrefilled: Scalars['Boolean']['output']; support: Support; @@ -382,12 +382,6 @@ export type FeatureFlag = { workspaceId: Scalars['String']['output']; }; -export type FeatureFlagFilter = { - and?: InputMaybe>; - id?: InputMaybe; - or?: InputMaybe>; -}; - export enum FeatureFlagKey { IsAdvancedFiltersEnabled = 'IsAdvancedFiltersEnabled', IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled', @@ -402,22 +396,11 @@ export enum FeatureFlagKey { IsJsonFilterEnabled = 'IsJsonFilterEnabled', IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', - IsSsoEnabled = 'IsSSOEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', IsWorkflowEnabled = 'IsWorkflowEnabled' } -export type FeatureFlagSort = { - direction: SortDirection; - field: FeatureFlagSortFields; - nulls?: InputMaybe; -}; - -export enum FeatureFlagSortFields { - Id = 'id' -} - export type FieldConnection = { __typename?: 'FieldConnection'; /** Array of edges. */ @@ -859,6 +842,7 @@ export type MutationSignUpArgs = { captchaToken?: InputMaybe; email: Scalars['String']['input']; password: Scalars['String']['input']; + workspaceId?: InputMaybe; workspaceInviteHash?: InputMaybe; workspacePersonalInviteToken?: InputMaybe; }; @@ -1537,6 +1521,12 @@ export type TransientToken = { transientToken: AuthToken; }; +export type TrialPeriodDto = { + __typename?: 'TrialPeriodDTO'; + duration: Scalars['Float']['output']; + isCreditCardRequired: Scalars['Boolean']['output']; +}; + export type UuidFilterComparison = { eq?: InputMaybe; gt?: InputMaybe; @@ -1793,17 +1783,12 @@ export type WorkspaceBillingSubscriptionsArgs = { sorting?: Array; }; - -export type WorkspaceFeatureFlagsArgs = { - filter?: FeatureFlagFilter; - sorting?: Array; -}; - export enum WorkspaceActivationStatus { Active = 'ACTIVE', Inactive = 'INACTIVE', OngoingCreation = 'ONGOING_CREATION', - PendingCreation = 'PENDING_CREATION' + PendingCreation = 'PENDING_CREATION', + Suspended = 'SUSPENDED' } export type WorkspaceEdge = { diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 40c09c186..6e738b9d1 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1,5 +1,5 @@ -import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; +import { gql } from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -110,9 +110,9 @@ export type AvailableWorkspaceOutput = { export type Billing = { __typename?: 'Billing'; - billingFreeTrialDurationInDays?: Maybe; billingUrl?: Maybe; isBillingEnabled: Scalars['Boolean']; + trialPeriods: Array; }; /** The different billing plans available */ @@ -1311,6 +1311,12 @@ export type TransientToken = { transientToken: AuthToken; }; +export type TrialPeriodDto = { + __typename?: 'TrialPeriodDTO'; + duration: Scalars['Float']; + isCreditCardRequired: Scalars['Boolean']; +}; + export type UuidFilterComparison = { eq?: InputMaybe; gt?: InputMaybe; @@ -2094,7 +2100,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isEmailVerificationRequired: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, trialPeriods: Array<{ __typename?: 'TrialPeriodDTO', duration: number, isCreditCardRequired: boolean }> }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>; @@ -3564,7 +3570,10 @@ export const GetClientConfigDocument = gql` billing { isBillingEnabled billingUrl - billingFreeTrialDurationInDays + trialPeriods { + duration + isCreditCardRequired + } } authProviders { google diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts index a7316f98a..8ad603c7b 100644 --- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts +++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts @@ -2,9 +2,9 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { AppPath } from '@/types/AppPath'; +import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended'; -import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; -import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql'; +import { OnboardingStatus } from '~/generated/graphql'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; @@ -17,11 +17,13 @@ const setupMockOnboardingStatus = ( jest.mocked(useOnboardingStatus).mockReturnValueOnce(onboardingStatus); }; -jest.mock('@/workspace/hooks/useSubscriptionStatus'); -const setupMockSubscriptionStatus = ( - subscriptionStatus: SubscriptionStatus | undefined, +jest.mock('@/workspace/hooks/useIsWorkspaceActivationStatusSuspended'); +const setupMockIsWorkspaceActivationStatusSuspended = ( + isWorkspaceSuspended: boolean, ) => { - jest.mocked(useSubscriptionStatus).mockReturnValueOnce(subscriptionStatus); + jest + .mocked(useIsWorkspaceActivationStatusSuspended) + .mockReturnValueOnce(isWorkspaceSuspended); }; jest.mock('~/hooks/useIsMatchingLocation'); @@ -47,262 +49,206 @@ jest.mocked(useDefaultHomePagePath).mockReturnValue({ // prettier-ignore const testCases = [ - { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.Verify, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.Verify, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, + { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.Verify, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, + { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.VerifyEmail, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined }, - { loc: AppPath.VerifyEmail, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.VerifyEmail, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.VerifyEmail, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.VerifyEmail, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.VerifyEmail, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined }, - { loc: AppPath.VerifyEmail, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: undefined }, - { loc: AppPath.VerifyEmail, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: undefined }, - { loc: AppPath.VerifyEmail, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: undefined }, - { loc: AppPath.VerifyEmail, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.SignInUp, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, + { loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.SignInUp, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.SignInUp, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, + { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined }, + { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.Invite, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, + { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined }, + { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: undefined }, + { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: undefined }, + { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: undefined }, + { loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined }, - { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Invite, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined }, - { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: undefined }, - { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: undefined }, - { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: undefined }, - { loc: AppPath.Invite, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined }, + { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.ResetPassword, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, + { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined }, + { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: undefined }, + { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: undefined }, + { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: undefined }, + { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined }, - { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.ResetPassword, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined }, - { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: undefined }, - { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: undefined }, - { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: undefined }, - { loc: AppPath.ResetPassword, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.CreateWorkspace, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined }, + { loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.CreateWorkspace, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined }, - { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.CreateWorkspace, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, + { loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.CreateProfile, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: undefined }, + { loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.CreateProfile, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: undefined }, - { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.CreateProfile, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, + { loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.SyncEmails, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: undefined }, + { loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.SyncEmails, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: undefined }, - { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.SyncEmails, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, + { loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.InviteTeam, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: undefined }, + { loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.InviteTeam, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: undefined }, - { loc: AppPath.InviteTeam, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, + { loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined }, + { loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.PlanRequired, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined }, - { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.PlanRequired, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.PlanRequired, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, + { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined }, + { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.PlanRequiredSuccess, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined }, + { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: undefined }, - { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.PlanRequiredSuccess, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: undefined }, - { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, + { loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.Index, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, - { loc: AppPath.Index, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.Index, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: defaultHomePagePath }, + { loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.TasksPage, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.TasksPage, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.TasksPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.OpportunitiesPage, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.OpportunitiesPage, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.OpportunitiesPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.RecordIndexPage, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.RecordIndexPage, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.RecordIndexPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.RecordShowPage, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.RecordShowPage, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.RecordShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.SettingsCatchAll, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.SettingsCatchAll, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.SettingsCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.DevelopersCatchAll, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.DevelopersCatchAll, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.Authorize, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Authorize, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.NotFoundWildcard, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.NotFoundWildcard, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.NotFoundWildcard, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - - { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.NotFound, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.NotFound, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, + { loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, + { loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, + { loc: AppPath.NotFound, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, + { loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, + { loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, + { loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, + { loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.Completed, res: undefined }, ]; describe('usePageChangeEffectNavigateLocation', () => { testCases.forEach((testCase) => { - it(`with location ${testCase.loc} and onboardingStatus ${testCase.onboardingStatus} and subscriptionStatus ${testCase.subscriptionStatus} should return ${testCase.res}`, () => { + it(`with location ${testCase.loc} and onboardingStatus ${testCase.onboardingStatus} and isWorkspaceSuspended ${testCase.isWorkspaceSuspended} should return ${testCase.res}`, () => { setupMockIsMatchingLocation(testCase.loc); setupMockOnboardingStatus(testCase.onboardingStatus); - setupMockSubscriptionStatus(testCase.subscriptionStatus); + setupMockIsWorkspaceActivationStatusSuspended( + testCase.isWorkspaceSuspended, + ); setupMockIsLogged(testCase.isLoggedIn); expect(usePageChangeEffectNavigateLocation()).toEqual(testCase.res); }); }); describe('tests should be exhaustive', () => { - it('all location and onboarding status should be tested', () => { - const untestedSubscriptionStatus = [ - SubscriptionStatus.Incomplete, - SubscriptionStatus.IncompleteExpired, - SubscriptionStatus.Paused, - SubscriptionStatus.Trialing, - ]; + it('all location, onboarding status and suspended/not suspended workspace activation status should be tested', () => { expect(testCases.length).toEqual( (Object.keys(AppPath).length - UNTESTED_APP_PATHS.length) * (Object.keys(OnboardingStatus).length + - (Object.keys(SubscriptionStatus).length - - untestedSubscriptionStatus.length)), + ['isWorkspaceSuspended:true', 'isWorkspaceSuspended:false'].length), ); }); }); diff --git a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts index ff2821aee..a5153cb75 100644 --- a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts +++ b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts @@ -3,15 +3,15 @@ import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePat import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; -import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; -import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql'; +import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended'; +import { OnboardingStatus } from '~/generated/graphql'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; export const usePageChangeEffectNavigateLocation = () => { const isMatchingLocation = useIsMatchingLocation(); const isLoggedIn = useIsLogged(); const onboardingStatus = useOnboardingStatus(); - const subscriptionStatus = useSubscriptionStatus(); + const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended(); const { defaultHomePagePath } = useDefaultHomePagePath(); const isMatchingOpenRoute = @@ -49,22 +49,7 @@ export const usePageChangeEffectNavigateLocation = () => { return AppPath.PlanRequired; } - if ( - subscriptionStatus === SubscriptionStatus.Unpaid && - !isMatchingLocation(AppPath.SettingsCatchAll) - ) { - return `${AppPath.SettingsCatchAll.replace('/*', '')}/${ - SettingsPath.Billing - }`; - } - - if ( - subscriptionStatus === SubscriptionStatus.Canceled && - !( - isMatchingLocation(AppPath.SettingsCatchAll) || - isMatchingLocation(AppPath.PlanRequired) - ) - ) { + if (isWorkspaceSuspended && !isMatchingLocation(AppPath.SettingsCatchAll)) { return `${AppPath.SettingsCatchAll.replace('/*', '')}/${ SettingsPath.Billing }`; @@ -99,14 +84,6 @@ export const usePageChangeEffectNavigateLocation = () => { return AppPath.InviteTeam; } - if ( - onboardingStatus === OnboardingStatus.Completed && - subscriptionStatus === SubscriptionStatus.Canceled && - isMatchingLocation(AppPath.PlanRequired) - ) { - return; - } - if ( onboardingStatus === OnboardingStatus.Completed && isMatchingOnboardingRoute && diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithGoogle.test.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithGoogle.test.ts index 5a7072cea..659c2b7d2 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithGoogle.test.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithGoogle.test.ts @@ -19,7 +19,6 @@ describe('useSignInWithGoogle', () => { plan: BillingPlanKey.Pro, interval: SubscriptionInterval.Month, requirePaymentMethod: true, - skipPlanPage: false, }; const Wrapper = getJestMetadataAndApolloMocksWrapper({ @@ -31,7 +30,7 @@ describe('useSignInWithGoogle', () => { const mockUseParams = { workspaceInviteHash: 'testHash' }; const mockSearchParams = new URLSearchParams( - 'inviteToken=testToken&billingCheckoutSessionState={"plan":"Pro","interval":"Month","requirePaymentMethod":true,"skipPlanPage":false}', + 'inviteToken=testToken&billingCheckoutSessionState={"plan":"Pro","interval":"Month","requirePaymentMethod":true}', ); (useParams as jest.Mock).mockReturnValue(mockUseParams); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithMicrosoft.test.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithMicrosoft.test.ts index 98c9ea527..e4a5b1b2c 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithMicrosoft.test.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithMicrosoft.test.ts @@ -22,7 +22,6 @@ describe('useSignInWithMicrosoft', () => { plan: 'PRO', interval: 'Month', requirePaymentMethod: true, - skipPlanPage: false, }; it('should call signInWithMicrosoft with the correct parameters', () => { diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithGoogle.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithGoogle.ts index 26908402e..4d8361d03 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithGoogle.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithGoogle.ts @@ -12,7 +12,6 @@ export const useSignInWithGoogle = () => { plan: 'PRO', interval: 'Month', requirePaymentMethod: true, - skipPlanPage: false, } as BillingCheckoutSession; const { signInWithGoogle } = useAuth(); diff --git a/packages/twenty-front/src/modules/auth/states/billingCheckoutSessionState.ts b/packages/twenty-front/src/modules/auth/states/billingCheckoutSessionState.ts index acfe231a9..e80cd0e16 100644 --- a/packages/twenty-front/src/modules/auth/states/billingCheckoutSessionState.ts +++ b/packages/twenty-front/src/modules/auth/states/billingCheckoutSessionState.ts @@ -1,16 +1,11 @@ import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type'; +import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue'; import { createState } from '@ui/utilities/state/utils/createState'; import { syncEffect } from 'recoil-sync'; -import { BillingPlanKey, SubscriptionInterval } from '~/generated/graphql'; export const billingCheckoutSessionState = createState({ key: 'billingCheckoutSessionState', - defaultValue: { - plan: BillingPlanKey.Pro, - interval: SubscriptionInterval.Month, - requirePaymentMethod: true, - skipPlanPage: false, - }, + defaultValue: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE, effects: [ syncEffect({ refine: (value: unknown) => { @@ -19,8 +14,7 @@ export const billingCheckoutSessionState = createState({ value !== null && 'plan' in value && 'interval' in value && - 'requirePaymentMethod' in value && - 'skipPlanPage' in value + 'requirePaymentMethod' in value ) { return { type: 'success', diff --git a/packages/twenty-front/src/modules/auth/types/billingCheckoutSession.type.ts b/packages/twenty-front/src/modules/auth/types/billingCheckoutSession.type.ts index a634e17e2..8de24c98f 100644 --- a/packages/twenty-front/src/modules/auth/types/billingCheckoutSession.type.ts +++ b/packages/twenty-front/src/modules/auth/types/billingCheckoutSession.type.ts @@ -5,5 +5,4 @@ export type BillingCheckoutSession = { plan: BillingPlanKey; interval: SubscriptionInterval; requirePaymentMethod: boolean; - skipPlanPage: boolean; }; diff --git a/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx b/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx deleted file mode 100644 index 5bfbebee7..000000000 --- a/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import styled from '@emotion/styled'; - -import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice'; -import { capitalize } from 'twenty-shared'; - -type SubscriptionCardProps = { - type?: string; - price: number; - info: string; -}; - -const StyledSubscriptionCardContainer = styled.div` - display: flex; - flex-direction: column; -`; - -const StyledTypeContainer = styled.div` - color: ${({ theme }) => theme.font.color.secondary}; - font-size: ${({ theme }) => theme.font.size.sm}; - display: flex; -`; - -const StyledInfoContainer = styled.div` - color: ${({ theme }) => theme.font.color.tertiary}; - font-size: ${({ theme }) => theme.font.size.sm}; - display: flex; -`; - -export const SubscriptionCard = ({ - type, - price, - info, -}: SubscriptionCardProps) => { - return ( - - {capitalize(type || '')} - - {info} - - ); -}; diff --git a/packages/twenty-front/src/modules/billing/components/SubscriptionCardPrice.tsx b/packages/twenty-front/src/modules/billing/components/SubscriptionCardPrice.tsx deleted file mode 100644 index 8091c72ab..000000000 --- a/packages/twenty-front/src/modules/billing/components/SubscriptionCardPrice.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import styled from '@emotion/styled'; - -type SubscriptionCardPriceProps = { - price: number; -}; -const StyledSubscriptionCardPriceContainer = styled.div` - align-items: baseline; - display: flex; - gap: ${({ theme }) => theme.betweenSiblingsGap}; - margin: ${({ theme }) => theme.spacing(1)} 0 - ${({ theme }) => theme.spacing(2)}; -`; -const StyledPriceSpan = styled.span` - color: ${({ theme }) => theme.font.color.primary}; - font-size: ${({ theme }) => theme.font.size.xl}; - font-weight: ${({ theme }) => theme.font.weight.semiBold}; -`; -const StyledSeatSpan = styled.span` - color: ${({ theme }) => theme.font.color.light}; - font-size: ${({ theme }) => theme.font.size.md}; - font-weight: ${({ theme }) => theme.font.weight.medium}; -`; -export const SubscriptionCardPrice = ({ - price, -}: SubscriptionCardPriceProps) => { - return ( - - ${price} - / - seat - - ); -}; diff --git a/packages/twenty-front/src/modules/billing/components/SubscriptionPrice.tsx b/packages/twenty-front/src/modules/billing/components/SubscriptionPrice.tsx new file mode 100644 index 000000000..206c9b768 --- /dev/null +++ b/packages/twenty-front/src/modules/billing/components/SubscriptionPrice.tsx @@ -0,0 +1,29 @@ +import styled from '@emotion/styled'; +import { SubscriptionInterval } from '~/generated-metadata/graphql'; + +type SubscriptionPriceProps = { + type: SubscriptionInterval; + price: number; +}; + +const StyledPriceSpan = styled.span` + color: ${({ theme }) => theme.font.color.primary}; + font-size: ${({ theme }) => theme.font.size.xxl}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + margin-bottom: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledPriceUnitSpan = styled.span` + color: ${({ theme }) => theme.font.color.light}; + font-size: ${({ theme }) => theme.font.size.md}; + font-weight: ${({ theme }) => theme.font.weight.medium}; +`; + +export const SubscriptionPrice = ({ type, price }: SubscriptionPriceProps) => { + return ( + <> + {`$${price}`} + {`seat / ${type.toLocaleLowerCase()}`} + + ); +}; diff --git a/packages/twenty-front/src/modules/billing/components/TrialCard.tsx b/packages/twenty-front/src/modules/billing/components/TrialCard.tsx new file mode 100644 index 000000000..7ce30c484 --- /dev/null +++ b/packages/twenty-front/src/modules/billing/components/TrialCard.tsx @@ -0,0 +1,33 @@ +import styled from '@emotion/styled'; + +type TrialCardProps = { + duration: number; + withCreditCard: boolean; +}; + +const StyledTrialCardContainer = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledTrialDurationContainer = styled.div` + color: ${({ theme }) => theme.font.color.secondary}; + font-size: ${({ theme }) => theme.font.size.sm}; + display: flex; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const StyledCreditCardRequirementContainer = styled.div` + color: ${({ theme }) => theme.font.color.tertiary}; + font-size: ${({ theme }) => theme.font.size.sm}; + display: flex; +`; + +export const TrialCard = ({ duration, withCreditCard }: TrialCardProps) => { + return ( + + {`${duration} days trial`} + {`${withCreditCard ? 'With Credit Card' : 'Without Credit Card'}`} + + ); +}; diff --git a/packages/twenty-front/src/modules/billing/constants/BillingCheckoutSessionDefaultValue.ts b/packages/twenty-front/src/modules/billing/constants/BillingCheckoutSessionDefaultValue.ts new file mode 100644 index 000000000..5dd12a661 --- /dev/null +++ b/packages/twenty-front/src/modules/billing/constants/BillingCheckoutSessionDefaultValue.ts @@ -0,0 +1,11 @@ +import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type'; +import { + BillingPlanKey, + SubscriptionInterval, +} from '~/generated-metadata/graphql'; + +export const BILLING_CHECKOUT_SESSION_DEFAULT_VALUE: BillingCheckoutSession = { + plan: BillingPlanKey.Pro, + interval: SubscriptionInterval.Month, + requirePaymentMethod: true, +}; diff --git a/packages/twenty-front/src/modules/billing/hooks/useHandleCheckoutSession.ts b/packages/twenty-front/src/modules/billing/hooks/useHandleCheckoutSession.ts new file mode 100644 index 000000000..788cea521 --- /dev/null +++ b/packages/twenty-front/src/modules/billing/hooks/useHandleCheckoutSession.ts @@ -0,0 +1,50 @@ +import { AppPath } from '@/types/AppPath'; +import { SettingsPath } from '@/types/SettingsPath'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useState } from 'react'; +import { + BillingPlanKey, + SubscriptionInterval, +} from '~/generated-metadata/graphql'; +import { useCheckoutSessionMutation } from '~/generated/graphql'; + +export const useHandleCheckoutSession = ({ + recurringInterval, + plan, + requirePaymentMethod, +}: { + recurringInterval: SubscriptionInterval; + plan: BillingPlanKey; + requirePaymentMethod: boolean; +}) => { + const { enqueueSnackBar } = useSnackBar(); + + const [checkoutSession] = useCheckoutSessionMutation(); + + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleCheckoutSession = async () => { + setIsSubmitting(true); + const { data } = await checkoutSession({ + variables: { + recurringInterval, + successUrlPath: `${AppPath.Settings}/${SettingsPath.Billing}`, + plan, + requirePaymentMethod, + }, + }); + setIsSubmitting(false); + if (!data?.checkoutSession.url) { + enqueueSnackBar( + 'Checkout session error. Please retry or contact Twenty team', + { + variant: SnackBarVariant.Error, + }, + ); + return; + } + window.location.replace(data.checkoutSession.url); + }; + return { isSubmitting, handleCheckoutSession }; +}; diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index 577614bce..fa6a0a669 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -6,7 +6,10 @@ export const GET_CLIENT_CONFIG = gql` billing { isBillingEnabled billingUrl - billingFreeTrialDurationInDays + trialPeriods { + duration + isCreditCardRequired + } } authProviders { google diff --git a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx index 2c951b2e0..2cb79486a 100644 --- a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx @@ -17,12 +17,14 @@ export const InformationBanner = ({ buttonTitle, buttonIcon, buttonOnClick, + isButtonDisabled = false, }: { message: string; variant?: BannerVariant; buttonTitle?: string; buttonIcon?: IconComponent; buttonOnClick?: () => void; + isButtonDisabled?: boolean; }) => { return ( @@ -35,6 +37,7 @@ export const InformationBanner = ({ size="small" inverted onClick={buttonOnClick} + disabled={isButtonDisabled} /> )} diff --git a/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx b/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx index 0ebacf1a9..9ceb953e2 100644 --- a/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx @@ -1,6 +1,13 @@ +import { InformationBannerBillingSubscriptionPaused } from '@/information-banner/components/billing/InformationBannerBillingSubscriptionPaused'; +import { InformationBannerFailPaymentInfo } from '@/information-banner/components/billing/InformationBannerFailPaymentInfo'; +import { InformationBannerNoBillingSubscription } from '@/information-banner/components/billing/InformationBannerNoBillingSubscription'; import { InformationBannerReconnectAccountEmailAliases } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases'; import { InformationBannerReconnectAccountInsufficientPermissions } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions'; +import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended'; +import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import styled from '@emotion/styled'; +import { isDefined } from 'twenty-ui'; +import { SubscriptionStatus } from '~/generated-metadata/graphql'; const StyledInformationBannerWrapper = styled.div` height: 40px; @@ -12,10 +19,30 @@ const StyledInformationBannerWrapper = styled.div` `; export const InformationBannerWrapper = () => { + const subscriptionStatus = useSubscriptionStatus(); + const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended(); + + const displayBillingSubscriptionPausedBanner = + isWorkspaceSuspended && subscriptionStatus === SubscriptionStatus.Paused; + + const displayBillingSubscriptionCanceledBanner = + isWorkspaceSuspended && !isDefined(subscriptionStatus); + + const displayFailPaymentInfoBanner = + subscriptionStatus === SubscriptionStatus.PastDue || + subscriptionStatus === SubscriptionStatus.Unpaid; + return ( + {displayBillingSubscriptionPausedBanner && ( + + )} + {displayBillingSubscriptionCanceledBanner && ( + + )} + {displayFailPaymentInfoBanner && } ); }; diff --git a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx new file mode 100644 index 000000000..397f00f7e --- /dev/null +++ b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx @@ -0,0 +1,29 @@ +import { InformationBanner } from '@/information-banner/components/InformationBanner'; +import { AppPath } from '@/types/AppPath'; +import { SettingsPath } from '@/types/SettingsPath'; +import { isDefined } from 'twenty-ui'; +import { useBillingPortalSessionQuery } from '~/generated/graphql'; + +export const InformationBannerBillingSubscriptionPaused = () => { + const { data, loading } = useBillingPortalSessionQuery({ + variables: { + returnUrlPath: `${AppPath.Settings}/${SettingsPath.Billing}`, + }, + }); + + const openBillingPortal = () => { + if (isDefined(data) && isDefined(data.billingPortalSession.url)) { + window.location.replace(data.billingPortalSession.url); + } + }; + + return ( + openBillingPortal()} + isButtonDisabled={loading || !isDefined(data)} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerFailPaymentInfo.tsx b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerFailPaymentInfo.tsx new file mode 100644 index 000000000..d17b2e5d2 --- /dev/null +++ b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerFailPaymentInfo.tsx @@ -0,0 +1,29 @@ +import { InformationBanner } from '@/information-banner/components/InformationBanner'; +import { AppPath } from '@/types/AppPath'; +import { SettingsPath } from '@/types/SettingsPath'; +import { isDefined } from 'twenty-ui'; +import { useBillingPortalSessionQuery } from '~/generated/graphql'; + +export const InformationBannerFailPaymentInfo = () => { + const { data, loading } = useBillingPortalSessionQuery({ + variables: { + returnUrlPath: `${AppPath.Settings}/${SettingsPath.Billing}`, + }, + }); + + const openBillingPortal = () => { + if (isDefined(data) && isDefined(data.billingPortalSession.url)) { + window.location.replace(data.billingPortalSession.url); + } + }; + + return ( + openBillingPortal()} + isButtonDisabled={loading || !isDefined(data)} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerNoBillingSubscription.tsx b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerNoBillingSubscription.tsx new file mode 100644 index 000000000..eefaa20a0 --- /dev/null +++ b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerNoBillingSubscription.tsx @@ -0,0 +1,21 @@ +import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue'; +import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession'; +import { InformationBanner } from '@/information-banner/components/InformationBanner'; + +export const InformationBannerNoBillingSubscription = () => { + const { handleCheckoutSession, isSubmitting } = useHandleCheckoutSession({ + recurringInterval: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE.interval, + plan: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE.plan, + requirePaymentMethod: true, + }); + + return ( + handleCheckoutSession()} + isButtonDisabled={isSubmitting} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx b/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx index 2c9d4d581..1808b4c3d 100644 --- a/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx +++ b/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx @@ -10,11 +10,14 @@ import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig'; import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; import { View } from '@/views/types/View'; +import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended'; import { isDefined } from '~/utils/isDefined'; export const PrefetchRunQueriesEffect = () => { const currentUser = useRecoilValue(currentUserState); + const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended(); + const { upsertRecordsInCache: upsertViewsInCache } = usePrefetchRunQuery({ prefetchKey: PrefetchKey.AllViews, @@ -42,7 +45,7 @@ export const PrefetchRunQueriesEffect = () => { const { result } = useCombinedFindManyRecords({ operationSignatures, - skip: !currentUser, + skip: !currentUser || isWorkspaceSuspended, }); useEffect(() => { diff --git a/packages/twenty-front/src/modules/prefetch/hooks/useIsPrefetchLoading.ts b/packages/twenty-front/src/modules/prefetch/hooks/useIsPrefetchLoading.ts index 7df0fa892..ceaab4bd6 100644 --- a/packages/twenty-front/src/modules/prefetch/hooks/useIsPrefetchLoading.ts +++ b/packages/twenty-front/src/modules/prefetch/hooks/useIsPrefetchLoading.ts @@ -1,8 +1,10 @@ import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended'; import { useRecoilValue } from 'recoil'; export const useIsPrefetchLoading = () => { + const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended(); const isFavoriteFoldersPrefetched = useRecoilValue( prefetchIsLoadedFamilyState(PrefetchKey.AllFavoritesFolders), ); @@ -15,8 +17,9 @@ export const useIsPrefetchLoading = () => { ); return ( - !areViewsPrefetched || - !areFavoritesPrefetched || - !isFavoriteFoldersPrefetched + !isWorkspaceSuspended && + (!areViewsPrefetched || + !areFavoritesPrefetched || + !isFavoriteFoldersPrefetched) ); }; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx index ec0c5bcfe..5dc94d7b1 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx @@ -6,6 +6,7 @@ import { IconX, UndecoratedLink } from 'twenty-ui'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended'; type NavigationDrawerBackButtonProps = { title: string; @@ -51,6 +52,11 @@ export const NavigationDrawerBackButton = ({ navigationDrawerExpandedMemorizedState, ); + const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended(); + if (isWorkspaceSuspended) { + return ; + } + return ( { + const currentWorkspace = useRecoilValue(currentWorkspaceState); + return ( + currentWorkspace?.activationStatus === WorkspaceActivationStatus.Suspended + ); +}; diff --git a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx index 1d9098864..61827f9b5 100644 --- a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx +++ b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx @@ -3,50 +3,63 @@ import { Title } from '@/auth/components/Title'; import { useAuth } from '@/auth/hooks/useAuth'; import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState'; import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit'; -import { SubscriptionCard } from '@/billing/components/SubscriptionCard'; +import { SubscriptionPrice } from '@/billing/components/SubscriptionPrice'; +import { TrialCard } from '@/billing/components/TrialCard'; +import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession'; import { billingState } from '@/client-config/states/billingState'; -import { AppPath } from '@/types/AppPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import styled from '@emotion/styled'; -import { isNonEmptyString, isNumber } from '@sniptt/guards'; -import { useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { ActionLink, CAL_LINK, CardPicker, + isDefined, Loader, MainButton, } from 'twenty-ui'; -import { - ProductPriceEntity, - SubscriptionInterval, - useCheckoutSessionMutation, - useGetProductPricesQuery, -} from '~/generated/graphql'; -import { isDefined } from '~/utils/isDefined'; +import { SubscriptionInterval } from '~/generated-metadata/graphql'; +import { useGetProductPricesQuery } from '~/generated/graphql'; -const StyledChoosePlanContainer = styled.div` - display: flex; - flex-direction: row; - width: 100%; - margin: ${({ theme }) => theme.spacing(8)} 0 - ${({ theme }) => theme.spacing(2)}; - gap: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledBenefitsContainer = styled.div` +const StyledSubscriptionContainer = styled.div<{ + withLongerMarginBottom: boolean; +}>` background-color: ${({ theme }) => theme.background.secondary}; border: 1px solid ${({ theme }) => theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.md}; + + display: flex; + flex-direction: column; + margin: ${({ theme }) => theme.spacing(8)} 0 + ${({ theme, withLongerMarginBottom }) => + theme.spacing(withLongerMarginBottom ? 8 : 2)}; + width: 100%; +`; + +const StyledSubscriptionPriceContainer = styled.div` + align-items: center; + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + display: flex; + flex-direction: column; + margin: ${({ theme }) => theme.spacing(4)} ${({ theme }) => theme.spacing(3)} + 0 ${({ theme }) => theme.spacing(4)}; + padding-bottom: ${({ theme }) => theme.spacing(3)}; +`; + +const StyledBenefitsContainer = styled.div` box-sizing: border-box; display: flex; flex-direction: column; width: 100%; gap: 16px; padding: ${({ theme }) => theme.spacing(4)} ${({ theme }) => theme.spacing(3)}; +`; + +const StyledChooseTrialContainer = styled.div` + display: flex; + flex-direction: row; + width: 100%; margin-bottom: ${({ theme }) => theme.spacing(8)}; + gap: ${({ theme }) => theme.spacing(2)}; `; const StyledLinkGroup = styled.div` @@ -71,58 +84,48 @@ const benefits = [ 'Email integration', 'Custom objects', 'API & Webhooks', - 'Frequent updates', - 'And much more', + '1 000 workflow node executions', ]; - export const ChooseYourPlan = () => { const billing = useRecoilValue(billingState); - const [isSubmitting, setIsSubmitting] = useState(false); - - const { enqueueSnackBar } = useSnackBar(); - const { data: prices } = useGetProductPricesQuery({ variables: { product: 'base-plan' }, }); + const price = prices?.getProductPrices?.productPrices.find( + (productPrice) => + productPrice.recurringInterval === SubscriptionInterval.Month, + ); + + const hasWithoutCreditCardTrialPeriod = billing?.trialPeriods.some( + (trialPeriod) => + !trialPeriod.isCreditCardRequired && trialPeriod.duration !== 0, + ); + const withCreditCardTrialPeriod = billing?.trialPeriods.find( + (trialPeriod) => trialPeriod.isCreditCardRequired, + ); + const [billingCheckoutSession, setBillingCheckoutSession] = useRecoilState( billingCheckoutSessionState, ); - const [checkoutSession] = useCheckoutSessionMutation(); + const { handleCheckoutSession, isSubmitting } = useHandleCheckoutSession({ + recurringInterval: billingCheckoutSession.interval, + plan: billingCheckoutSession.plan, + requirePaymentMethod: billingCheckoutSession.requirePaymentMethod, + }); - const handleCheckoutSession = async () => { - setIsSubmitting(true); - const { data } = await checkoutSession({ - variables: { - recurringInterval: billingCheckoutSession.interval, - successUrlPath: AppPath.PlanRequiredSuccess, - plan: billingCheckoutSession.plan, - requirePaymentMethod: billingCheckoutSession.requirePaymentMethod, - }, - }); - setIsSubmitting(false); - if (!data?.checkoutSession.url) { - enqueueSnackBar( - 'Checkout session error. Please retry or contact Twenty team', - { - variant: SnackBarVariant.Error, - }, - ); - return; - } - window.location.replace(data.checkoutSession.url); - }; - - const handleIntervalChange = (type?: SubscriptionInterval) => { + const handleTrialPeriodChange = (withCreditCard: boolean) => { return () => { - if (isNonEmptyString(type) && billingCheckoutSession.interval !== type) { + if ( + isDefined(price) && + billingCheckoutSession.requirePaymentMethod !== withCreditCard + ) { setBillingCheckoutSession({ plan: billingCheckoutSession.plan, - interval: type, - requirePaymentMethod: billingCheckoutSession.requirePaymentMethod, - skipPlanPage: false, + interval: price.recurringInterval, + requirePaymentMethod: withCreditCard, }); } }; @@ -130,65 +133,58 @@ export const ChooseYourPlan = () => { const { signOut } = useAuth(); - const computeInfo = ( - price: ProductPriceEntity, - prices: ProductPriceEntity[], - ): string => { - if (price.recurringInterval !== SubscriptionInterval.Year) { - return 'Cancel anytime'; - } - const monthPrice = prices.filter( - (price) => price.recurringInterval === SubscriptionInterval.Month, - )?.[0]; - if ( - isDefined(monthPrice) && - isNumber(monthPrice.unitAmount) && - monthPrice.unitAmount > 0 && - isNumber(price.unitAmount) && - price.unitAmount > 0 - ) { - return `Save $${(12 * monthPrice.unitAmount - price.unitAmount) / 100}`; - } - return 'Cancel anytime'; - }; - - if (billingCheckoutSession.skipPlanPage && !isSubmitting) { - handleCheckoutSession(); - } - - if (billingCheckoutSession.skipPlanPage && isSubmitting) { - return ; - } - return ( - prices?.getProductPrices?.productPrices && ( + isDefined(price) && + isDefined(billing) && ( <> - Choose your Plan - - Enjoy a {billing?.billingFreeTrialDurationInDays}-day free trial - - - {prices.getProductPrices.productPrices.map((price, index) => ( - - - - ))} - - - {benefits.map((benefit, index) => ( - {benefit} - ))} - + + {hasWithoutCreditCardTrialPeriod + ? 'Choose your Trial' + : 'Get your subscription'} + + {hasWithoutCreditCardTrialPeriod ? ( + Cancel anytime + ) : ( + withCreditCardTrialPeriod && ( + {`Enjoy a ${withCreditCardTrialPeriod.duration}-days free trial`} + ) + )} + + + + + + {benefits.map((benefit) => ( + {benefit} + ))} + + + {hasWithoutCreditCardTrialPeriod && ( + + {billing.trialPeriods.map((trialPeriod) => ( + + + + ))} + + )} = { { __typename: 'ProductPriceEntity', created: 1699860608, - recurringInterval: 'month', + recurringInterval: 'Month', stripePriceId: 'monthly8usd', unitAmount: 900, }, { __typename: 'ProductPriceEntity', created: 1701874964, - recurringInterval: 'year', + recurringInterval: 'Year', stripePriceId: 'priceId', unitAmount: 9000, }, @@ -56,7 +56,7 @@ const meta: Meta = { }, }); }), - graphqlMocks.handlers, + ...graphqlMocks.handlers, ], }, }, @@ -70,7 +70,7 @@ export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await canvas.findByText('Choose your Plan', undefined, { + await canvas.findByText('Choose your Trial', undefined, { timeout: 3000, }); }, diff --git a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx index 495474723..b78280399 100644 --- a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx @@ -6,7 +6,6 @@ import { IconCalendarEvent, IconCircleX, IconCreditCard, - Info, Section, } from 'twenty-ui'; @@ -15,7 +14,6 @@ import { SettingsBillingCoverImage } from '@/billing/components/SettingsBillingC import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; -import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -25,7 +23,6 @@ import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { OnboardingStatus, SubscriptionInterval, - SubscriptionStatus, useBillingPortalSessionQuery, useUpdateBillingSubscriptionMutation, } from '~/generated/graphql'; @@ -87,17 +84,6 @@ export const SettingsBilling = () => { billingPortalButtonDisabled || onboardingStatus !== OnboardingStatus.Completed; - const displayPaymentFailInfo = - subscriptionStatus === SubscriptionStatus.PastDue || - subscriptionStatus === SubscriptionStatus.Unpaid; - - const displaySubscriptionCanceledInfo = - subscriptionStatus === SubscriptionStatus.Canceled; - - const displaySubscribeInfo = - onboardingStatus === OnboardingStatus.Completed && - !isDefined(subscriptionStatus); - const openBillingPortal = () => { if (isDefined(data) && isDefined(data.billingPortalSession.url)) { window.location.replace(data.billingPortalSession.url); @@ -147,30 +133,7 @@ export const SettingsBilling = () => { > - {displayPaymentFailInfo && ( - - )} - {displaySubscriptionCanceledInfo && ( - - )} - {displaySubscribeInfo ? ( - - ) : ( + {isDefined(subscriptionStatus) && ( <>
Number) + @Min(0) + duration: number; + + @Field(() => Boolean) + isCreditCardRequired: boolean; +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts index 896679364..89c661e05 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { isDefined } from 'class-validator'; import { Repository } from 'typeorm'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; @@ -50,15 +51,15 @@ export class BillingPortalWorkspaceService { workspaceId: workspace.id, }); - const stripeCustomerId = ( - await this.billingSubscriptionRepository.findOneBy({ - workspaceId: workspace.id, - }) - )?.stripeCustomerId; + const subscription = await this.billingSubscriptionRepository.findOneBy({ + workspaceId: workspace.id, + }); - const session = await this.stripeCheckoutService.createCheckoutSession( + const stripeCustomerId = subscription?.stripeCustomerId; + + const session = await this.stripeCheckoutService.createCheckoutSession({ user, - workspace.id, + workspaceId: workspace.id, priceId, quantity, successUrl, @@ -66,7 +67,8 @@ export class BillingPortalWorkspaceService { stripeCustomerId, plan, requirePaymentMethod, - ); + withTrialPeriod: !isDefined(subscription), + }); assert(session.url, 'Error: missing checkout.session.url'); diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts index 647751987..6e30b9f3c 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts @@ -1,9 +1,11 @@ import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'class-validator'; +import { Repository } from 'typeorm'; +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; -import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @@ -16,15 +18,41 @@ export class BillingService { private readonly environmentService: EnvironmentService, private readonly billingSubscriptionService: BillingSubscriptionService, private readonly isFeatureEnabledService: FeatureFlagService, + @InjectRepository(BillingSubscription, 'core') + private readonly billingSubscriptionRepository: Repository, ) {} isBillingEnabled() { return this.environmentService.get('IS_BILLING_ENABLED'); } - async hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement( + async hasWorkspaceSubscriptionOrFreeAccess(workspaceId: string) { + const isBillingEnabled = this.isBillingEnabled(); + + if (!isBillingEnabled) { + return true; + } + + const isFreeAccessEnabled = + await this.isFeatureEnabledService.isFeatureEnabled( + FeatureFlagKey.IsFreeAccessEnabled, + workspaceId, + ); + + if (isFreeAccessEnabled) { + return true; + } + + const subscription = await this.billingSubscriptionRepository.findOne({ + where: { workspaceId }, + }); + + return isDefined(subscription); + } + + async hasFreeAccessOrEntitlement( workspaceId: string, - entitlementKey?: BillingEntitlementKey, + entitlementKey: BillingEntitlementKey, ) { const isBillingEnabled = this.isBillingEnabled(); @@ -42,25 +70,9 @@ export class BillingService { return true; } - if (entitlementKey) { - return this.billingSubscriptionService.getWorkspaceEntitlementByKey( - workspaceId, - entitlementKey, - ); - } - - const currentBillingSubscription = - await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow( - { workspaceId }, - ); - - return ( - isDefined(currentBillingSubscription) && - [ - SubscriptionStatus.Active, - SubscriptionStatus.Trialing, - SubscriptionStatus.PastDue, - ].includes(currentBillingSubscription.status) + return this.billingSubscriptionService.getWorkspaceEntitlementByKey( + workspaceId, + entitlementKey, ); } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-checkout.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-checkout.service.ts index fd7715275..bab412a8d 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-checkout.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-checkout.service.ts @@ -24,17 +24,29 @@ export class StripeCheckoutService { ); } - async createCheckoutSession( - user: User, - workspaceId: string, - priceId: string, - quantity: number, - successUrl?: string, - cancelUrl?: string, - stripeCustomerId?: string, - plan: BillingPlanKey = BillingPlanKey.PRO, + async createCheckoutSession({ + user, + workspaceId, + priceId, + quantity, + successUrl, + cancelUrl, + stripeCustomerId, + plan = BillingPlanKey.PRO, requirePaymentMethod = true, - ): Promise { + withTrialPeriod, + }: { + user: User; + workspaceId: string; + priceId: string; + quantity: number; + successUrl?: string; + cancelUrl?: string; + stripeCustomerId?: string; + plan?: BillingPlanKey; + requirePaymentMethod?: boolean; + withTrialPeriod: boolean; + }): Promise { return await this.stripe.checkout.sessions.create({ line_items: [ { @@ -48,14 +60,25 @@ export class StripeCheckoutService { workspaceId, plan, }, - trial_period_days: this.environmentService.get( - 'BILLING_FREE_TRIAL_DURATION_IN_DAYS', - ), + ...(withTrialPeriod + ? { + trial_period_days: this.environmentService.get( + requirePaymentMethod + ? 'BILLING_FREE_TRIAL_WITH_CREDIT_CARD_DURATION_IN_DAYS' + : 'BILLING_FREE_TRIAL_WITHOUT_CREDIT_CARD_DURATION_IN_DAYS', + ), + trial_settings: { + end_behavior: { missing_payment_method: 'pause' }, + }, + } + : {}), }, automatic_tax: { enabled: !!requirePaymentMethod }, tax_id_collection: { enabled: !!requirePaymentMethod }, customer: stripeCustomerId, - customer_update: stripeCustomerId ? { name: 'auto' } : undefined, + customer_update: stripeCustomerId + ? { name: 'auto', address: 'auto' } + : undefined, customer_email: stripeCustomerId ? undefined : user.email, success_url: successUrl, cancel_url: cancelUrl, diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index fcdab3c0c..69527b001 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -1,5 +1,6 @@ import { Field, ObjectType } from '@nestjs/graphql'; +import { TrialPeriodDTO } from 'src/engine/core-modules/billing/dto/trial-period.dto'; import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces'; import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output'; @@ -11,8 +12,8 @@ class Billing { @Field(() => String, { nullable: true }) billingUrl?: string; - @Field(() => Number, { nullable: true }) - billingFreeTrialDurationInDays?: number; + @Field(() => [TrialPeriodDTO]) + trialPeriods: TrialPeriodDTO[]; } @ObjectType() diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index 9f6282f88..f932b3d6a 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -18,9 +18,20 @@ export class ClientConfigResolver { billing: { isBillingEnabled: this.environmentService.get('IS_BILLING_ENABLED'), billingUrl: this.environmentService.get('BILLING_PLAN_REQUIRED_LINK'), - billingFreeTrialDurationInDays: this.environmentService.get( - 'BILLING_FREE_TRIAL_DURATION_IN_DAYS', - ), + trialPeriods: [ + { + duration: this.environmentService.get( + 'BILLING_FREE_TRIAL_WITH_CREDIT_CARD_DURATION_IN_DAYS', + ), + isCreditCardRequired: true, + }, + { + duration: this.environmentService.get( + 'BILLING_FREE_TRIAL_WITHOUT_CREDIT_CARD_DURATION_IN_DAYS', + ), + isCreditCardRequired: false, + }, + ], }, authProviders: { google: this.environmentService.get('AUTH_GOOGLE_ENABLED'), diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index 318321994..d7e67b996 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -73,7 +73,13 @@ export class EnvironmentVariables { @CastToPositiveNumber() @IsOptional() @ValidateIf((env) => env.IS_BILLING_ENABLED === true) - BILLING_FREE_TRIAL_DURATION_IN_DAYS = 7; + BILLING_FREE_TRIAL_WITH_CREDIT_CARD_DURATION_IN_DAYS = 30; + + @IsNumber() + @CastToPositiveNumber() + @IsOptional() + @ValidateIf((env) => env.IS_BILLING_ENABLED === true) + BILLING_FREE_TRIAL_WITHOUT_CREDIT_CARD_DURATION_IN_DAYS = 7; @IsString() @ValidateIf((env) => env.IS_BILLING_ENABLED === true) diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts index 894369f71..053f011ae 100644 --- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts +++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts @@ -30,7 +30,7 @@ export class OnboardingService { private async isSubscriptionIncompleteOnboardingStatus(workspace: Workspace) { const hasSubscription = - await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement( + await this.billingService.hasWorkspaceSubscriptionOrFreeAccess( workspace.id, ); diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index 39e406cc8..fcfb35cb3 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -36,7 +36,7 @@ export class SSOService { private async isSSOEnabled(workspaceId: string) { const isSSOBillingEnabled = - await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement( + await this.billingService.hasFreeAccessOrEntitlement( workspaceId, this.featureLookUpKey, ); diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index 7eab38aea..231ce6a2a 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -41,7 +41,10 @@ export class UserService extends TypeOrmQueryService { } async loadWorkspaceMember(user: User, workspace: Workspace) { - if (workspace?.activationStatus !== WorkspaceActivationStatus.ACTIVE) { + if ( + workspace?.activationStatus !== WorkspaceActivationStatus.ACTIVE && + workspace?.activationStatus !== WorkspaceActivationStatus.SUSPENDED + ) { return null; }