From d8815d7ebfaac9c24df36606b5e8a4475863bce1 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:01:18 +0100 Subject: [PATCH] fix: prevent billingPortal creation if no active subscription (#9701) Billing portal is created in settings/billing page even if subscription is canceled, causing server internal error. -> Skip back end request Bonus : display settings/billing page with disabled button even if subscription is canceled --------- Co-authored-by: etiennejouan Co-authored-by: Charles Bochet --- .../src/generated-metadata/graphql.ts | 95 +--------------- .../twenty-front/src/generated/graphql.tsx | 105 ++--------------- .../auth/states/currentWorkspaceState.ts | 1 + .../hooks/__mocks__/useFieldMetadataItem.ts | 6 + ...ColumnDefinitionsFromFieldMetadata.test.ts | 17 ++- .../graphql/fragments/userQueryFragment.ts | 4 + .../src/pages/settings/SettingsBilling.tsx | 106 +++++++++--------- .../src/testing/mock-data/users.ts | 7 ++ .../billing-portal.workspace-service.ts | 14 +-- .../workspace/workspace.entity.ts | 14 +-- .../workspace/workspace.module.ts | 7 +- .../workspace/workspace.resolver.ts | 17 +++ 12 files changed, 123 insertions(+), 270 deletions(-) diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 7d7c90830..f9c872e89 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -135,22 +135,6 @@ export type BillingSubscription = { status: SubscriptionStatus; }; -export type BillingSubscriptionFilter = { - and?: InputMaybe>; - id?: InputMaybe; - or?: InputMaybe>; -}; - -export type BillingSubscriptionSort = { - direction: SortDirection; - field: BillingSubscriptionSortFields; - nulls?: InputMaybe; -}; - -export enum BillingSubscriptionSortFields { - id = 'id' -} - export type BooleanFieldComparison = { is?: InputMaybe; isNot?: InputMaybe; @@ -1418,18 +1402,6 @@ export type SignUpOutput = { workspace: WorkspaceSubdomainAndId; }; -/** Sort Directions */ -export enum SortDirection { - ASC = 'ASC', - DESC = 'DESC' -} - -/** Sort Nulls Options */ -export enum SortNulls { - NULLS_FIRST = 'NULLS_FIRST', - NULLS_LAST = 'NULLS_LAST' -} - export enum SubscriptionInterval { Day = 'Day', Month = 'Month', @@ -1743,9 +1715,7 @@ export type Workspace = { __typename?: 'Workspace'; activationStatus: WorkspaceActivationStatus; allowImpersonation: Scalars['Boolean']['output']; - billingCustomers?: Maybe>; - billingEntitlements?: Maybe>; - billingSubscriptions?: Maybe>; + billingSubscriptions: Array; createdAt: Scalars['DateTime']['output']; currentBillingSubscription?: Maybe; databaseSchema: Scalars['String']['output']; @@ -1768,24 +1738,6 @@ export type Workspace = { workspaceMembersCount?: Maybe; }; - -export type WorkspaceBillingCustomersArgs = { - filter?: BillingCustomerFilter; - sorting?: Array; -}; - - -export type WorkspaceBillingEntitlementsArgs = { - filter?: BillingEntitlementFilter; - sorting?: Array; -}; - - -export type WorkspaceBillingSubscriptionsArgs = { - filter?: BillingSubscriptionFilter; - sorting?: Array; -}; - export enum WorkspaceActivationStatus { ACTIVE = 'ACTIVE', INACTIVE = 'INACTIVE', @@ -1864,51 +1816,6 @@ export type WorkspaceSubdomainAndId = { subdomain: Scalars['String']['output']; }; -export type BillingCustomer = { - __typename?: 'billingCustomer'; - id: Scalars['UUID']['output']; -}; - -export type BillingCustomerFilter = { - and?: InputMaybe>; - id?: InputMaybe; - or?: InputMaybe>; -}; - -export type BillingCustomerSort = { - direction: SortDirection; - field: BillingCustomerSortFields; - nulls?: InputMaybe; -}; - -export enum BillingCustomerSortFields { - id = 'id' -} - -export type BillingEntitlement = { - __typename?: 'billingEntitlement'; - id: Scalars['UUID']['output']; - key: Scalars['String']['output']; - value: Scalars['Boolean']['output']; - workspaceId: Scalars['String']['output']; -}; - -export type BillingEntitlementFilter = { - and?: InputMaybe>; - id?: InputMaybe; - or?: InputMaybe>; -}; - -export type BillingEntitlementSort = { - direction: SortDirection; - field: BillingEntitlementSortFields; - nulls?: InputMaybe; -}; - -export enum BillingEntitlementSortFields { - id = 'id' -} - export type Field = { __typename?: 'field'; createdAt: Scalars['DateTime']['output']; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 3f79ef0a3..2560b6b2a 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1,5 +1,5 @@ -import * as Apollo from '@apollo/client'; import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -128,22 +128,6 @@ export type BillingSubscription = { status: SubscriptionStatus; }; -export type BillingSubscriptionFilter = { - and?: InputMaybe>; - id?: InputMaybe; - or?: InputMaybe>; -}; - -export type BillingSubscriptionSort = { - direction: SortDirection; - field: BillingSubscriptionSortFields; - nulls?: InputMaybe; -}; - -export enum BillingSubscriptionSortFields { - id = 'id' -} - export type BooleanFieldComparison = { is?: InputMaybe; isNot?: InputMaybe; @@ -1228,18 +1212,6 @@ export type SignUpOutput = { workspace: WorkspaceSubdomainAndId; }; -/** Sort Directions */ -export enum SortDirection { - ASC = 'ASC', - DESC = 'DESC' -} - -/** Sort Nulls Options */ -export enum SortNulls { - NULLS_FIRST = 'NULLS_FIRST', - NULLS_LAST = 'NULLS_LAST' -} - export enum SubscriptionInterval { Day = 'Day', Month = 'Month', @@ -1540,9 +1512,7 @@ export type Workspace = { __typename?: 'Workspace'; activationStatus: WorkspaceActivationStatus; allowImpersonation: Scalars['Boolean']; - billingCustomers?: Maybe>; - billingEntitlements?: Maybe>; - billingSubscriptions?: Maybe>; + billingSubscriptions: Array; createdAt: Scalars['DateTime']; currentBillingSubscription?: Maybe; databaseSchema: Scalars['String']; @@ -1565,24 +1535,6 @@ export type Workspace = { workspaceMembersCount?: Maybe; }; - -export type WorkspaceBillingCustomersArgs = { - filter?: BillingCustomerFilter; - sorting?: Array; -}; - - -export type WorkspaceBillingEntitlementsArgs = { - filter?: BillingEntitlementFilter; - sorting?: Array; -}; - - -export type WorkspaceBillingSubscriptionsArgs = { - filter?: BillingSubscriptionFilter; - sorting?: Array; -}; - export enum WorkspaceActivationStatus { ACTIVE = 'ACTIVE', INACTIVE = 'INACTIVE', @@ -1661,51 +1613,6 @@ export type WorkspaceSubdomainAndId = { subdomain: Scalars['String']; }; -export type BillingCustomer = { - __typename?: 'billingCustomer'; - id: Scalars['UUID']; -}; - -export type BillingCustomerFilter = { - and?: InputMaybe>; - id?: InputMaybe; - or?: InputMaybe>; -}; - -export type BillingCustomerSort = { - direction: SortDirection; - field: BillingCustomerSortFields; - nulls?: InputMaybe; -}; - -export enum BillingCustomerSortFields { - id = 'id' -} - -export type BillingEntitlement = { - __typename?: 'billingEntitlement'; - id: Scalars['UUID']; - key: Scalars['String']; - value: Scalars['Boolean']; - workspaceId: Scalars['String']; -}; - -export type BillingEntitlementFilter = { - and?: InputMaybe>; - id?: InputMaybe; - or?: InputMaybe>; -}; - -export type BillingEntitlementSort = { - direction: SortDirection; - field: BillingEntitlementSortFields; - nulls?: InputMaybe; -}; - -export enum BillingEntitlementSortFields { - id = 'id' -} - export type Field = { __typename?: 'field'; createdAt: Scalars['DateTime']; @@ -2191,7 +2098,7 @@ export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key: export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -2208,7 +2115,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -2504,6 +2411,10 @@ export const UserQueryFragmentFragmentDoc = gql` status interval } + billingSubscriptions { + id + status + } workspaceMembersCount } workspaces { diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts index 872c6af93..5f2530bc6 100644 --- a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts @@ -11,6 +11,7 @@ export type CurrentWorkspace = Pick< | 'allowImpersonation' | 'featureFlags' | 'activationStatus' + | 'billingSubscriptions' | 'currentBillingSubscription' | 'workspaceMembersCount' | 'isPublicInviteLinkEnabled' diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts index 67f83367f..d630617d5 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts @@ -160,6 +160,10 @@ export const queries = { status interval } + billingSubscriptions { + id + status + } workspaceMembersCount } workspaces { @@ -300,6 +304,8 @@ export const responseData = { currentBillingSubscription: null, workspaceMembersCount: 1, }, + currentBillingSubscription: null, + billingSubscriptions: [], workspaces: [], userVars: null, }, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts index d342df2d9..f7c3aeafb 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts @@ -4,7 +4,11 @@ import { Nullable } from 'twenty-ui'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { WorkspaceActivationStatus } from '~/generated/graphql'; +import { + SubscriptionInterval, + SubscriptionStatus, + WorkspaceActivationStatus, +} from '~/generated/graphql'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; @@ -23,6 +27,17 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({ isGoogleAuthEnabled: true, isMicrosoftAuthEnabled: false, isPasswordAuthEnabled: true, + currentBillingSubscription: { + id: '1', + interval: SubscriptionInterval.Month, + status: SubscriptionStatus.Active, + }, + billingSubscriptions: [ + { + id: '1', + status: SubscriptionStatus.Active, + }, + ], }); }, }); diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index 6ec4788d2..008d9dc35 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -50,6 +50,10 @@ export const USER_QUERY_FRAGMENT = gql` status interval } + billingSubscriptions { + id + status + } workspaceMembersCount } workspaces { diff --git a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx index 86ed6623c..0a9f9eed7 100644 --- a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx @@ -12,7 +12,6 @@ import { import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SettingsBillingCoverImage } from '@/billing/components/SettingsBillingCoverImage'; -import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPath } from '@/types/SettingsPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; @@ -21,8 +20,8 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { - OnboardingStatus, SubscriptionInterval, + SubscriptionStatus, useBillingPortalSessionQuery, useUpdateBillingSubscriptionMutation, } from '~/generated/graphql'; @@ -59,9 +58,16 @@ export const SettingsBilling = () => { }; const { enqueueSnackBar } = useSnackBar(); - const onboardingStatus = useOnboardingStatus(); - const subscriptionStatus = useSubscriptionStatus(); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const subscriptions = currentWorkspace?.billingSubscriptions; + const hasSubscriptions = (subscriptions?.length ?? 0) > 0; + + const subscriptionStatus = useSubscriptionStatus(); + const hasNotCanceledCurrentSubscription = + isDefined(subscriptionStatus) && + subscriptionStatus !== SubscriptionStatus.Canceled; + const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const switchingInfo = currentWorkspace?.currentBillingSubscription?.interval === @@ -75,18 +81,12 @@ export const SettingsBilling = () => { variables: { returnUrlPath: '/settings/billing', }, + skip: !hasSubscriptions, }); const billingPortalButtonDisabled = loading || !isDefined(data) || !isDefined(data.billingPortalSession.url); - const switchIntervalButtonDisabled = - onboardingStatus !== OnboardingStatus.COMPLETED; - - const cancelPlanButtonDisabled = - billingPortalButtonDisabled || - onboardingStatus !== OnboardingStatus.COMPLETED; - const openBillingPortal = () => { if (isDefined(data) && isDefined(data.billingPortalSession.url)) { window.location.replace(data.billingPortalSession.url); @@ -137,50 +137,46 @@ export const SettingsBilling = () => { > - {isDefined(subscriptionStatus) && ( - <> -
- -
-
- -
-
- -
- - )} +
+ +
+
+ +
+
+ +
BillingSubscription, { - nullable: true, -}) -@UnPagedRelation('billingEntitlements', () => BillingEntitlement, { - nullable: true, -}) -@UnPagedRelation('billingCustomers', () => BillingCustomer, { - nullable: true, -}) export class Workspace { @IDField(() => UUIDScalarType) @PrimaryGeneratedColumn('uuid') diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts index 05d58624a..1e8cb58b6 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts @@ -1,10 +1,14 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; @@ -17,8 +21,6 @@ import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.r import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; -import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; import { Workspace } from './workspace.entity'; @@ -28,6 +30,7 @@ import { WorkspaceService } from './services/workspace.service'; @Module({ imports: [ TypeORMModule, + TypeOrmModule.forFeature([BillingSubscription], 'core'), NestjsQueryGraphQLModule.forFeature({ imports: [ DomainManagerModule, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index 0c191b954..592d5d58d 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -7,8 +7,10 @@ import { ResolveField, Resolver, } from '@nestjs/graphql'; +import { InjectRepository } from '@nestjs/typeorm'; import { FileUpload, GraphQLUpload } from 'graphql-upload'; +import { Repository } from 'typeorm'; import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; @@ -63,6 +65,8 @@ export class WorkspaceResolver { private readonly fileService: FileService, private readonly billingSubscriptionService: BillingSubscriptionService, private readonly featureFlagService: FeatureFlagService, + @InjectRepository(BillingSubscription, 'core') + private readonly billingSubscriptionRepository: Repository, ) {} @Query(() => Workspace) @@ -159,6 +163,19 @@ export class WorkspaceResolver { return this.workspaceService.deleteWorkspace(id); } + @ResolveField(() => [BillingSubscription]) + async billingSubscriptions( + @Parent() workspace: Workspace, + ): Promise { + try { + return this.billingSubscriptionRepository.find({ + where: { workspaceId: workspace.id }, + }); + } catch (error) { + workspaceGraphqlApiExceptionHandler(error); + } + } + @ResolveField(() => BillingSubscription, { nullable: true }) async currentBillingSubscription( @Parent() workspace: Workspace,