diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index e9af365be..7745e0713 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -248,6 +248,7 @@ export type BillingSubscription = { billingSubscriptionItems?: Maybe>; id: Scalars['UUID']['output']; interval?: Maybe; + metadata: Scalars['JSON']['output']; status: SubscriptionStatus; }; @@ -256,6 +257,7 @@ export type BillingSubscriptionItem = { billingProduct?: Maybe; hasReachedCurrentPeriodCap: Scalars['Boolean']['output']; id: Scalars['UUID']['output']; + quantity?: Maybe; }; export type BillingTrialPeriodDto = { @@ -653,13 +655,13 @@ export type FeatureFlagDto = { export enum FeatureFlagKey { IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', + IS_AI_ENABLED = 'IS_AI_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_PERMISSIONS_V2_ENABLED = 'IS_PERMISSIONS_V2_ENABLED', IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED', IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED', IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED', - IS_WORKFLOW_ENABLED = 'IS_WORKFLOW_ENABLED', - IS_AI_ENABLED = 'IS_AI_ENABLED' + IS_WORKFLOW_ENABLED = 'IS_WORKFLOW_ENABLED' } export type Field = { @@ -1006,6 +1008,7 @@ export type Mutation = { signUpInNewWorkspace: SignUpOutput; skipSyncEmailOnboardingStep: OnboardingStepSuccess; submitFormStep: Scalars['Boolean']['output']; + switchToEnterprisePlan: BillingUpdateOutput; switchToYearlyInterval: BillingUpdateOutput; syncRemoteTable: RemoteTable; syncRemoteTableSchemaChanges: RemoteTable; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 16102da20..ab5df2a70 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] }; @@ -240,6 +240,7 @@ export type BillingSubscription = { billingSubscriptionItems?: Maybe>; id: Scalars['UUID']; interval?: Maybe; + metadata: Scalars['JSON']; status: SubscriptionStatus; }; @@ -248,6 +249,7 @@ export type BillingSubscriptionItem = { billingProduct?: Maybe; hasReachedCurrentPeriodCap: Scalars['Boolean']; id: Scalars['UUID']; + quantity?: Maybe; }; export type BillingTrialPeriodDto = { @@ -584,13 +586,13 @@ export type FeatureFlagDto = { export enum FeatureFlagKey { IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', + IS_AI_ENABLED = 'IS_AI_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_PERMISSIONS_V2_ENABLED = 'IS_PERMISSIONS_V2_ENABLED', IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED', IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED', IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED', - IS_WORKFLOW_ENABLED = 'IS_WORKFLOW_ENABLED', - IS_AI_ENABLED = 'IS_AI_ENABLED' + IS_WORKFLOW_ENABLED = 'IS_WORKFLOW_ENABLED' } export type Field = { @@ -926,6 +928,7 @@ export type Mutation = { signUpInNewWorkspace: SignUpOutput; skipSyncEmailOnboardingStep: OnboardingStepSuccess; submitFormStep: Scalars['Boolean']; + switchToEnterprisePlan: BillingUpdateOutput; switchToYearlyInterval: BillingUpdateOutput; trackAnalytics: Analytics; updateDatabaseConfigVariable: Scalars['Boolean']; @@ -2705,18 +2708,6 @@ export type ValidatePasswordResetTokenQueryVariables = Exact<{ export type ValidatePasswordResetTokenQuery = { __typename?: 'Query', validatePasswordResetToken: { __typename?: 'ValidatePasswordResetToken', id: string, email: string } }; -export type BillingBaseProductPricesQueryVariables = Exact<{ [key: string]: never; }>; - - -export type BillingBaseProductPricesQuery = { __typename?: 'Query', plans: Array<{ __typename?: 'BillingPlanOutput', planKey: BillingPlanKey, baseProduct: { __typename?: 'BillingProduct', name: string, prices?: Array<{ __typename?: 'BillingPriceLicensedDTO', unitAmount: number, stripePriceId: string, recurringInterval: SubscriptionInterval } | { __typename?: 'BillingPriceMeteredDTO' }> | null } }> }; - -export type BillingPortalSessionQueryVariables = Exact<{ - returnUrlPath?: InputMaybe; -}>; - - -export type BillingPortalSessionQuery = { __typename?: 'Query', billingPortalSession: { __typename?: 'BillingSessionOutput', url?: string | null } }; - export type CheckoutSessionMutationVariables = Exact<{ recurringInterval: SubscriptionInterval; successUrlPath?: InputMaybe; @@ -2732,16 +2723,33 @@ export type EndSubscriptionTrialPeriodMutationVariables = Exact<{ [key: string]: export type EndSubscriptionTrialPeriodMutation = { __typename?: 'Mutation', endSubscriptionTrialPeriod: { __typename?: 'BillingEndTrialPeriodOutput', status?: SubscriptionStatus | null, hasPaymentMethod: boolean } }; -export type GetMeteredProductsUsageQueryVariables = Exact<{ [key: string]: never; }>; +export type SwitchSubscriptionToEnterprisePlanMutationVariables = Exact<{ [key: string]: never; }>; -export type GetMeteredProductsUsageQuery = { __typename?: 'Query', getMeteredProductsUsage: Array<{ __typename?: 'BillingMeteredProductUsageOutput', productKey: BillingProductKey, usageQuantity: number, freeTierQuantity: number, freeTrialQuantity: number, unitPriceCents: number, totalCostCents: number }> }; +export type SwitchSubscriptionToEnterprisePlanMutation = { __typename?: 'Mutation', switchToEnterprisePlan: { __typename?: 'BillingUpdateOutput', success: boolean } }; export type SwitchSubscriptionToYearlyIntervalMutationVariables = Exact<{ [key: string]: never; }>; export type SwitchSubscriptionToYearlyIntervalMutation = { __typename?: 'Mutation', switchToYearlyInterval: { __typename?: 'BillingUpdateOutput', success: boolean } }; +export type BillingBaseProductPricesQueryVariables = Exact<{ [key: string]: never; }>; + + +export type BillingBaseProductPricesQuery = { __typename?: 'Query', plans: Array<{ __typename?: 'BillingPlanOutput', planKey: BillingPlanKey, baseProduct: { __typename?: 'BillingProduct', name: string, prices?: Array<{ __typename?: 'BillingPriceLicensedDTO', unitAmount: number, stripePriceId: string, recurringInterval: SubscriptionInterval } | { __typename?: 'BillingPriceMeteredDTO' }> | null } }> }; + +export type BillingPortalSessionQueryVariables = Exact<{ + returnUrlPath?: InputMaybe; +}>; + + +export type BillingPortalSessionQuery = { __typename?: 'Query', billingPortalSession: { __typename?: 'BillingSessionOutput', url?: string | null } }; + +export type GetMeteredProductsUsageQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetMeteredProductsUsageQuery = { __typename?: 'Query', getMeteredProductsUsage: Array<{ __typename?: 'BillingMeteredProductUsageOutput', productKey: BillingProductKey, usageQuantity: number, freeTierQuantity: number, freeTrialQuantity: number, unitPriceCents: number, totalCostCents: number }> }; + export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; @@ -2960,7 +2968,7 @@ export type OnDbEventSubscriptionVariables = Exact<{ export type OnDbEventSubscription = { __typename?: 'Subscription', onDbEvent: { __typename?: 'OnDbEventDTO', eventDate: string, action: DatabaseEventAction, objectNameSingular: string, updatedFields?: Array | null, record: any } }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, 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, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, 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, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -2977,7 +2985,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, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, 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, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, 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, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -3327,9 +3335,11 @@ export const UserQueryFragmentFragmentDoc = gql` id status interval + metadata billingSubscriptionItems { id hasReachedCurrentPeriodCap + quantity billingProduct { name description @@ -3344,6 +3354,7 @@ export const UserQueryFragmentFragmentDoc = gql` billingSubscriptions { id status + metadata } workspaceMembersCount defaultRole { @@ -4338,6 +4349,144 @@ export function useValidatePasswordResetTokenLazyQuery(baseOptions?: Apollo.Lazy export type ValidatePasswordResetTokenQueryHookResult = ReturnType; export type ValidatePasswordResetTokenLazyQueryHookResult = ReturnType; export type ValidatePasswordResetTokenQueryResult = Apollo.QueryResult; +export const CheckoutSessionDocument = gql` + mutation CheckoutSession($recurringInterval: SubscriptionInterval!, $successUrlPath: String, $plan: BillingPlanKey!, $requirePaymentMethod: Boolean!) { + checkoutSession( + recurringInterval: $recurringInterval + successUrlPath: $successUrlPath + plan: $plan + requirePaymentMethod: $requirePaymentMethod + ) { + url + } +} + `; +export type CheckoutSessionMutationFn = Apollo.MutationFunction; + +/** + * __useCheckoutSessionMutation__ + * + * To run a mutation, you first call `useCheckoutSessionMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCheckoutSessionMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [checkoutSessionMutation, { data, loading, error }] = useCheckoutSessionMutation({ + * variables: { + * recurringInterval: // value for 'recurringInterval' + * successUrlPath: // value for 'successUrlPath' + * plan: // value for 'plan' + * requirePaymentMethod: // value for 'requirePaymentMethod' + * }, + * }); + */ +export function useCheckoutSessionMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CheckoutSessionDocument, options); + } +export type CheckoutSessionMutationHookResult = ReturnType; +export type CheckoutSessionMutationResult = Apollo.MutationResult; +export type CheckoutSessionMutationOptions = Apollo.BaseMutationOptions; +export const EndSubscriptionTrialPeriodDocument = gql` + mutation EndSubscriptionTrialPeriod { + endSubscriptionTrialPeriod { + status + hasPaymentMethod + } +} + `; +export type EndSubscriptionTrialPeriodMutationFn = Apollo.MutationFunction; + +/** + * __useEndSubscriptionTrialPeriodMutation__ + * + * To run a mutation, you first call `useEndSubscriptionTrialPeriodMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useEndSubscriptionTrialPeriodMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [endSubscriptionTrialPeriodMutation, { data, loading, error }] = useEndSubscriptionTrialPeriodMutation({ + * variables: { + * }, + * }); + */ +export function useEndSubscriptionTrialPeriodMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(EndSubscriptionTrialPeriodDocument, options); + } +export type EndSubscriptionTrialPeriodMutationHookResult = ReturnType; +export type EndSubscriptionTrialPeriodMutationResult = Apollo.MutationResult; +export type EndSubscriptionTrialPeriodMutationOptions = Apollo.BaseMutationOptions; +export const SwitchSubscriptionToEnterprisePlanDocument = gql` + mutation SwitchSubscriptionToEnterprisePlan { + switchToEnterprisePlan { + success + } +} + `; +export type SwitchSubscriptionToEnterprisePlanMutationFn = Apollo.MutationFunction; + +/** + * __useSwitchSubscriptionToEnterprisePlanMutation__ + * + * To run a mutation, you first call `useSwitchSubscriptionToEnterprisePlanMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useSwitchSubscriptionToEnterprisePlanMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [switchSubscriptionToEnterprisePlanMutation, { data, loading, error }] = useSwitchSubscriptionToEnterprisePlanMutation({ + * variables: { + * }, + * }); + */ +export function useSwitchSubscriptionToEnterprisePlanMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(SwitchSubscriptionToEnterprisePlanDocument, options); + } +export type SwitchSubscriptionToEnterprisePlanMutationHookResult = ReturnType; +export type SwitchSubscriptionToEnterprisePlanMutationResult = Apollo.MutationResult; +export type SwitchSubscriptionToEnterprisePlanMutationOptions = Apollo.BaseMutationOptions; +export const SwitchSubscriptionToYearlyIntervalDocument = gql` + mutation SwitchSubscriptionToYearlyInterval { + switchToYearlyInterval { + success + } +} + `; +export type SwitchSubscriptionToYearlyIntervalMutationFn = Apollo.MutationFunction; + +/** + * __useSwitchSubscriptionToYearlyIntervalMutation__ + * + * To run a mutation, you first call `useSwitchSubscriptionToYearlyIntervalMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useSwitchSubscriptionToYearlyIntervalMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [switchSubscriptionToYearlyIntervalMutation, { data, loading, error }] = useSwitchSubscriptionToYearlyIntervalMutation({ + * variables: { + * }, + * }); + */ +export function useSwitchSubscriptionToYearlyIntervalMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(SwitchSubscriptionToYearlyIntervalDocument, options); + } +export type SwitchSubscriptionToYearlyIntervalMutationHookResult = ReturnType; +export type SwitchSubscriptionToYearlyIntervalMutationResult = Apollo.MutationResult; +export type SwitchSubscriptionToYearlyIntervalMutationOptions = Apollo.BaseMutationOptions; export const BillingBaseProductPricesDocument = gql` query billingBaseProductPrices { plans { @@ -4417,80 +4566,6 @@ export function useBillingPortalSessionLazyQuery(baseOptions?: Apollo.LazyQueryH export type BillingPortalSessionQueryHookResult = ReturnType; export type BillingPortalSessionLazyQueryHookResult = ReturnType; export type BillingPortalSessionQueryResult = Apollo.QueryResult; -export const CheckoutSessionDocument = gql` - mutation CheckoutSession($recurringInterval: SubscriptionInterval!, $successUrlPath: String, $plan: BillingPlanKey!, $requirePaymentMethod: Boolean!) { - checkoutSession( - recurringInterval: $recurringInterval - successUrlPath: $successUrlPath - plan: $plan - requirePaymentMethod: $requirePaymentMethod - ) { - url - } -} - `; -export type CheckoutSessionMutationFn = Apollo.MutationFunction; - -/** - * __useCheckoutSessionMutation__ - * - * To run a mutation, you first call `useCheckoutSessionMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useCheckoutSessionMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [checkoutSessionMutation, { data, loading, error }] = useCheckoutSessionMutation({ - * variables: { - * recurringInterval: // value for 'recurringInterval' - * successUrlPath: // value for 'successUrlPath' - * plan: // value for 'plan' - * requirePaymentMethod: // value for 'requirePaymentMethod' - * }, - * }); - */ -export function useCheckoutSessionMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(CheckoutSessionDocument, options); - } -export type CheckoutSessionMutationHookResult = ReturnType; -export type CheckoutSessionMutationResult = Apollo.MutationResult; -export type CheckoutSessionMutationOptions = Apollo.BaseMutationOptions; -export const EndSubscriptionTrialPeriodDocument = gql` - mutation EndSubscriptionTrialPeriod { - endSubscriptionTrialPeriod { - status - hasPaymentMethod - } -} - `; -export type EndSubscriptionTrialPeriodMutationFn = Apollo.MutationFunction; - -/** - * __useEndSubscriptionTrialPeriodMutation__ - * - * To run a mutation, you first call `useEndSubscriptionTrialPeriodMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useEndSubscriptionTrialPeriodMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [endSubscriptionTrialPeriodMutation, { data, loading, error }] = useEndSubscriptionTrialPeriodMutation({ - * variables: { - * }, - * }); - */ -export function useEndSubscriptionTrialPeriodMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(EndSubscriptionTrialPeriodDocument, options); - } -export type EndSubscriptionTrialPeriodMutationHookResult = ReturnType; -export type EndSubscriptionTrialPeriodMutationResult = Apollo.MutationResult; -export type EndSubscriptionTrialPeriodMutationOptions = Apollo.BaseMutationOptions; export const GetMeteredProductsUsageDocument = gql` query GetMeteredProductsUsage { getMeteredProductsUsage { @@ -4530,38 +4605,6 @@ export function useGetMeteredProductsUsageLazyQuery(baseOptions?: Apollo.LazyQue export type GetMeteredProductsUsageQueryHookResult = ReturnType; export type GetMeteredProductsUsageLazyQueryHookResult = ReturnType; export type GetMeteredProductsUsageQueryResult = Apollo.QueryResult; -export const SwitchSubscriptionToYearlyIntervalDocument = gql` - mutation SwitchSubscriptionToYearlyInterval { - switchToYearlyInterval { - success - } -} - `; -export type SwitchSubscriptionToYearlyIntervalMutationFn = Apollo.MutationFunction; - -/** - * __useSwitchSubscriptionToYearlyIntervalMutation__ - * - * To run a mutation, you first call `useSwitchSubscriptionToYearlyIntervalMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useSwitchSubscriptionToYearlyIntervalMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [switchSubscriptionToYearlyIntervalMutation, { data, loading, error }] = useSwitchSubscriptionToYearlyIntervalMutation({ - * variables: { - * }, - * }); - */ -export function useSwitchSubscriptionToYearlyIntervalMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(SwitchSubscriptionToYearlyIntervalDocument, options); - } -export type SwitchSubscriptionToYearlyIntervalMutationHookResult = ReturnType; -export type SwitchSubscriptionToYearlyIntervalMutationResult = Apollo.MutationResult; -export type SwitchSubscriptionToYearlyIntervalMutationOptions = Apollo.BaseMutationOptions; export const GetClientConfigDocument = gql` query GetClientConfig { clientConfig { diff --git a/packages/twenty-front/src/modules/billing/components/SettingsBillingMonthlyCreditsSection.tsx b/packages/twenty-front/src/modules/billing/components/SettingsBillingMonthlyCreditsSection.tsx index e02d8b71b..182dbfd80 100644 --- a/packages/twenty-front/src/modules/billing/components/SettingsBillingMonthlyCreditsSection.tsx +++ b/packages/twenty-front/src/modules/billing/components/SettingsBillingMonthlyCreditsSection.tsx @@ -10,18 +10,7 @@ import { BACKGROUND_LIGHT, COLOR } from 'twenty-ui/theme'; import { SubscriptionStatus } from '~/generated/graphql'; import { formatAmount } from '~/utils/format/formatAmount'; import { formatNumber } from '~/utils/format/number'; - -const StyledMonthlyCreditsContainer = styled.div` - border-radius: ${({ theme }) => theme.border.radius.md}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - background-color: ${({ theme }) => theme.background.secondary}; - padding: ${({ theme }) => theme.spacing(3)}; - width: 100%; - - display: flex; - flex-direction: column; - gap: ${({ theme }) => theme.spacing(3)}; -`; +import { SubscriptionInfoContainer } from '@/billing/components/SubscriptionInfoContainer'; const StyledLineSeparator = styled.div` width: 100%; @@ -57,7 +46,7 @@ export const SettingsBillingMonthlyCreditsSection = () => { title={t`Monthly Credits`} description={t`Track your monthly workflow credit consumption.`} /> - + { value={`$${formatNumber(totalCostCents / 100, 2)}`} /> )} - + ); }; diff --git a/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx b/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx new file mode 100644 index 000000000..c5f17ff42 --- /dev/null +++ b/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx @@ -0,0 +1,238 @@ +import { SubscriptionInfoContainer } from '@/billing/components/SubscriptionInfoContainer'; +import { SubscriptionInfoRowContainer } from '@/billing/components/SubscriptionInfoRowContainer'; + +import { + H2Title, + IconCalendarEvent, + IconTag, + IconUsers, + IconArrowUp, +} from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; +import { Section } from 'twenty-ui/layout'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { + BillingPlanKey, + BillingPlanOutput, + BillingProductKey, + SubscriptionInterval, + SubscriptionStatus, + useBillingBaseProductPricesQuery, + useSwitchSubscriptionToEnterprisePlanMutation, + useSwitchSubscriptionToYearlyIntervalMutation, +} from '~/generated/graphql'; +import { useRecoilState } from 'recoil'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { Tag } from 'twenty-ui/components'; +import { useModal } from '@/ui/layout/modal/hooks/useModal'; +import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { formatMonthlyPrices } from '@/billing/utils/formatMonthlyPrices'; +import { isDefined } from 'twenty-shared/utils'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; + +const SWITCH_BILLING_INTERVAL_MODAL_ID = 'switch-billing-interval-modal'; + +const SWITCH_BILLING_PLAN_MODAL_ID = 'switch-billing-plan-modal'; + +const StyledSwitchButtonContainer = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + margin-top: ${({ theme }) => theme.spacing(4)}; +`; + +export const SettingsBillingSubscriptionInfo = () => { + const { t } = useLingui(); + + const { openModal } = useModal(); + + const { enqueueSnackBar } = useSnackBar(); + + const subscriptionStatus = useSubscriptionStatus(); + + const { data: pricesData } = useBillingBaseProductPricesQuery(); + + const [switchToYearlyInterval] = + useSwitchSubscriptionToYearlyIntervalMutation(); + + const [switchToEnterprisePlan] = + useSwitchSubscriptionToEnterprisePlanMutation(); + + const [currentWorkspace, setCurrentWorkspace] = useRecoilState( + currentWorkspaceState, + ); + + const isMonthlyPlan = + currentWorkspace?.currentBillingSubscription?.interval === + SubscriptionInterval.Month; + + const isYearlyPlan = + currentWorkspace?.currentBillingSubscription?.interval === + SubscriptionInterval.Year; + + const isProPlan = + currentWorkspace?.currentBillingSubscription?.metadata['plan'] === + BillingPlanKey.PRO; + + const isEnterprisePlan = + currentWorkspace?.currentBillingSubscription?.metadata['plan'] === + BillingPlanKey.ENTERPRISE; + + const canSwitchSubscription = + subscriptionStatus !== SubscriptionStatus.PastDue; + + const planTag = isProPlan ? ( + + ) : isEnterprisePlan ? ( + + ) : undefined; + + const intervalLabel = isMonthlyPlan + ? t`Monthly` + : isYearlyPlan + ? t`Yearly` + : undefined; + + const seats = + currentWorkspace?.currentBillingSubscription?.billingSubscriptionItems?.find( + (item) => + item.billingProduct?.metadata.productKey === + BillingProductKey.BASE_PRODUCT, + )?.quantity as number | undefined; + + const baseProductPrices = pricesData?.plans as BillingPlanOutput[]; + + const formattedPrices = formatMonthlyPrices(baseProductPrices); + + const yearlyPrice = + formattedPrices?.[ + currentWorkspace?.currentBillingSubscription?.metadata[ + 'plan' + ] as BillingPlanKey + ]?.[SubscriptionInterval.Year]; + + const enterprisePrice = + formattedPrices?.[BillingPlanKey.ENTERPRISE]?.[ + currentWorkspace?.currentBillingSubscription?.interval as + | SubscriptionInterval.Month + | SubscriptionInterval.Year + ]; + + const switchInterval = async () => { + try { + await switchToYearlyInterval(); + if (isDefined(currentWorkspace?.currentBillingSubscription)) { + const newCurrentWorkspace = { + ...currentWorkspace, + currentBillingSubscription: { + ...currentWorkspace?.currentBillingSubscription, + interval: SubscriptionInterval.Year, + }, + }; + setCurrentWorkspace(newCurrentWorkspace); + } + enqueueSnackBar(t`Subscription has been switched to Yearly.`, { + variant: SnackBarVariant.Success, + }); + } catch (error: any) { + enqueueSnackBar(t`Error while switching subscription to Yearly.`, { + variant: SnackBarVariant.Error, + }); + } + }; + + const switchPlan = async () => { + try { + await switchToEnterprisePlan(); + if (isDefined(currentWorkspace?.currentBillingSubscription)) { + const newCurrentWorkspace = { + ...currentWorkspace, + currentBillingSubscription: { + ...currentWorkspace?.currentBillingSubscription, + metadata: { + ...currentWorkspace?.currentBillingSubscription.metadata, + plan: BillingPlanKey.ENTERPRISE, + }, + }, + }; + setCurrentWorkspace(newCurrentWorkspace); + } + enqueueSnackBar(t`Subscription has been switched to Organization Plan.`, { + variant: SnackBarVariant.Success, + }); + } catch (error: any) { + enqueueSnackBar( + t`Error while switching subscription to Organization Plan.`, + { + variant: SnackBarVariant.Error, + }, + ); + } + }; + + return ( +
+ + + + + + + + {isMonthlyPlan && ( +
+ ); +}; diff --git a/packages/twenty-front/src/modules/billing/components/SubscriptionInfoContainer.tsx b/packages/twenty-front/src/modules/billing/components/SubscriptionInfoContainer.tsx new file mode 100644 index 000000000..3cf74d263 --- /dev/null +++ b/packages/twenty-front/src/modules/billing/components/SubscriptionInfoContainer.tsx @@ -0,0 +1,14 @@ +import styled from '@emotion/styled'; + +const StyledSubscriptionInfoContainer = styled.div` + 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; + gap: ${({ theme }) => theme.spacing(3)}; + padding: ${({ theme }) => theme.spacing(3)}; + width: 100%; +`; + +export { StyledSubscriptionInfoContainer as SubscriptionInfoContainer }; diff --git a/packages/twenty-front/src/modules/billing/components/SubscriptionInfoRowContainer.tsx b/packages/twenty-front/src/modules/billing/components/SubscriptionInfoRowContainer.tsx new file mode 100644 index 000000000..e1eaf11ae --- /dev/null +++ b/packages/twenty-front/src/modules/billing/components/SubscriptionInfoRowContainer.tsx @@ -0,0 +1,48 @@ +import { IconComponent } from 'twenty-ui/display'; +import React from 'react'; +import styled from '@emotion/styled'; +import { useTheme } from '@emotion/react'; + +type SubscriptionInfoRowContainerProps = { + Icon: IconComponent; + label: string; + value: React.ReactNode; +}; + +const StyledContainer = styled.div` + align-items: center; + gap: ${({ theme }) => theme.spacing(1)}; + color: ${({ theme }) => theme.font.color.primary}; + display: flex; +`; + +const StyledIconLabelContainer = styled.div` + align-items: center; + gap: ${({ theme }) => theme.spacing(1)}; + color: ${({ theme }) => theme.font.color.tertiary}; + display: flex; + width: 120px; +`; + +const StyledLabelContainer = styled.div` + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +`; + +export const SubscriptionInfoRowContainer = ({ + Icon, + label, + value, +}: SubscriptionInfoRowContainerProps) => { + const theme = useTheme(); + return ( + + + + {label} + + {value} + + ); +}; diff --git a/packages/twenty-front/src/modules/billing/graphql/checkoutSession.ts b/packages/twenty-front/src/modules/billing/graphql/mutations/checkoutSession.ts similarity index 100% rename from packages/twenty-front/src/modules/billing/graphql/checkoutSession.ts rename to packages/twenty-front/src/modules/billing/graphql/mutations/checkoutSession.ts diff --git a/packages/twenty-front/src/modules/billing/graphql/endSubscriptionTrialPeriod.ts b/packages/twenty-front/src/modules/billing/graphql/mutations/endSubscriptionTrialPeriod.ts similarity index 100% rename from packages/twenty-front/src/modules/billing/graphql/endSubscriptionTrialPeriod.ts rename to packages/twenty-front/src/modules/billing/graphql/mutations/endSubscriptionTrialPeriod.ts diff --git a/packages/twenty-front/src/modules/billing/graphql/mutations/switchSubscriptionToEnterprisePlan.ts b/packages/twenty-front/src/modules/billing/graphql/mutations/switchSubscriptionToEnterprisePlan.ts new file mode 100644 index 000000000..56ea2c6c2 --- /dev/null +++ b/packages/twenty-front/src/modules/billing/graphql/mutations/switchSubscriptionToEnterprisePlan.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const SWITCH_SUBSCRIPTION_TO_ENTERPRISE_PLAN = gql` + mutation SwitchSubscriptionToEnterprisePlan { + switchToEnterprisePlan { + success + } + } +`; diff --git a/packages/twenty-front/src/modules/billing/graphql/switchSubscriptionToYearlyInterval.ts b/packages/twenty-front/src/modules/billing/graphql/mutations/switchSubscriptionToYearlyInterval.ts similarity index 100% rename from packages/twenty-front/src/modules/billing/graphql/switchSubscriptionToYearlyInterval.ts rename to packages/twenty-front/src/modules/billing/graphql/mutations/switchSubscriptionToYearlyInterval.ts diff --git a/packages/twenty-front/src/modules/billing/graphql/billingBaseProductPrices.ts b/packages/twenty-front/src/modules/billing/graphql/queries/billingBaseProductPrices.ts similarity index 100% rename from packages/twenty-front/src/modules/billing/graphql/billingBaseProductPrices.ts rename to packages/twenty-front/src/modules/billing/graphql/queries/billingBaseProductPrices.ts diff --git a/packages/twenty-front/src/modules/billing/graphql/billingPortalSession.ts b/packages/twenty-front/src/modules/billing/graphql/queries/billingPortalSession.ts similarity index 100% rename from packages/twenty-front/src/modules/billing/graphql/billingPortalSession.ts rename to packages/twenty-front/src/modules/billing/graphql/queries/billingPortalSession.ts diff --git a/packages/twenty-front/src/modules/billing/graphql/getMeteredProductsUsage.ts b/packages/twenty-front/src/modules/billing/graphql/queries/getMeteredProductsUsage.ts similarity index 100% rename from packages/twenty-front/src/modules/billing/graphql/getMeteredProductsUsage.ts rename to packages/twenty-front/src/modules/billing/graphql/queries/getMeteredProductsUsage.ts diff --git a/packages/twenty-front/src/modules/billing/utils/formatMonthlyPrices.ts b/packages/twenty-front/src/modules/billing/utils/formatMonthlyPrices.ts new file mode 100644 index 000000000..2262c4996 --- /dev/null +++ b/packages/twenty-front/src/modules/billing/utils/formatMonthlyPrices.ts @@ -0,0 +1,46 @@ +import { + BillingPlanKey, + SubscriptionInterval, + BillingPlanOutput, + BillingPriceLicensedDto, +} from '~/generated/graphql'; +import { isDefined } from 'twenty-shared/utils'; + +export const formatMonthlyPrices = (plans: BillingPlanOutput[] | undefined) => { + if (!isDefined(plans)) { + return; + } + + const enterprisePlan = plans.find( + (plan) => plan.planKey === BillingPlanKey.ENTERPRISE, + ); + + const enterpriseYearPrice = enterprisePlan?.baseProduct.prices?.find( + (price) => price.recurringInterval === SubscriptionInterval.Year, + ) as BillingPriceLicensedDto; + + const enterpriseMonthPrice = enterprisePlan?.baseProduct.prices?.find( + (price) => price.recurringInterval === SubscriptionInterval.Month, + ) as BillingPriceLicensedDto; + + const proPlan = plans.find((plan) => plan.planKey === BillingPlanKey.PRO); + + const proYearPrice = proPlan?.baseProduct.prices?.find( + (price) => price.recurringInterval === SubscriptionInterval.Year, + ) as BillingPriceLicensedDto; + + const proMonthPrice = proPlan?.baseProduct.prices?.find( + (price) => price.recurringInterval === SubscriptionInterval.Month, + ) as BillingPriceLicensedDto; + + return { + [BillingPlanKey.ENTERPRISE]: { + [SubscriptionInterval.Year]: enterpriseYearPrice?.unitAmount / 100 / 12, + [SubscriptionInterval.Month]: enterpriseMonthPrice?.unitAmount / 100, + }, + [BillingPlanKey.PRO]: { + [SubscriptionInterval.Year]: proYearPrice?.unitAmount / 100 / 12, + [SubscriptionInterval.Month]: proMonthPrice?.unitAmount / 100, + }, + }; +}; 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 4a4133ab2..1d362b3f8 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 @@ -38,11 +38,13 @@ const Wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ id: '1', interval: SubscriptionInterval.Month, status: SubscriptionStatus.Active, + metadata: {}, }, billingSubscriptions: [ { id: '1', status: SubscriptionStatus.Active, + metadata: {}, }, ], }); diff --git a/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts b/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts index f515b5f59..9634e4e67 100644 --- a/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts +++ b/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts @@ -37,7 +37,7 @@ const renderHooks = ( result.current.setCurrentWorkspace({ ...mockCurrentWorkspace, currentBillingSubscription: withCurrentBillingSubscription - ? { id: v4(), status: SubscriptionStatus.Active } + ? { id: v4(), status: SubscriptionStatus.Active, metadata: {} } : undefined, workspaceMembersCount: withOneWorkspaceMember ? 1 : 2, }); 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 161611880..a299bff99 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -60,9 +60,11 @@ export const USER_QUERY_FRAGMENT = gql` id status interval + metadata billingSubscriptionItems { id hasReachedCurrentPeriodCap + quantity billingProduct { name description @@ -77,6 +79,7 @@ export const USER_QUERY_FRAGMENT = gql` billingSubscriptions { id status + metadata } workspaceMembersCount defaultRole { diff --git a/packages/twenty-front/src/modules/workspace/hooks/__tests__/useSubscriptionStatus.test.ts b/packages/twenty-front/src/modules/workspace/hooks/__tests__/useSubscriptionStatus.test.ts index 821f2fa7b..edc5af116 100644 --- a/packages/twenty-front/src/modules/workspace/hooks/__tests__/useSubscriptionStatus.test.ts +++ b/packages/twenty-front/src/modules/workspace/hooks/__tests__/useSubscriptionStatus.test.ts @@ -50,6 +50,7 @@ describe('useSubscriptionStatus', () => { currentBillingSubscription: { id: v4(), status: subscriptionStatus, + metadata: {}, }, }); }); diff --git a/packages/twenty-front/src/pages/onboarding/__stories__/ChooseYourPlan.stories.tsx b/packages/twenty-front/src/pages/onboarding/__stories__/ChooseYourPlan.stories.tsx index b03abb181..3ce5df19c 100644 --- a/packages/twenty-front/src/pages/onboarding/__stories__/ChooseYourPlan.stories.tsx +++ b/packages/twenty-front/src/pages/onboarding/__stories__/ChooseYourPlan.stories.tsx @@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { within } from '@storybook/testing-library'; import { HttpResponse, graphql } from 'msw'; -import { BILLING_BASE_PRODUCT_PRICES } from '@/billing/graphql/billingBaseProductPrices'; +import { BILLING_BASE_PRODUCT_PRICES } from '@/billing/graphql/queries/billingBaseProductPrices'; import { AppPath } from '@/types/AppPath'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { diff --git a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx index 5bbffc9ec..6cd1678bb 100644 --- a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx @@ -1,56 +1,41 @@ import { Trans, useLingui } from '@lingui/react/macro'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SettingsBillingMonthlyCreditsSection } from '@/billing/components/SettingsBillingMonthlyCreditsSection'; import { useRedirect } from '@/domain-manager/hooks/useRedirect'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; 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 { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; -import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { isDefined } from 'twenty-shared/utils'; -import { - H2Title, - IconCalendarEvent, - IconCircleX, - IconCreditCard, -} from 'twenty-ui/display'; +import { H2Title, IconCircleX, IconCreditCard } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { - SubscriptionInterval, SubscriptionStatus, useBillingPortalSessionQuery, - useSwitchSubscriptionToYearlyIntervalMutation, } from '~/generated/graphql'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; - -const SWITCH_BILLING_INTERVAL_MODAL_ID = 'switch-billing-interval-modal'; +import { SettingsBillingSubscriptionInfo } from '@/billing/components/SettingsBillingSubscriptionInfo'; export const SettingsBilling = () => { const { t } = useLingui(); const { redirect } = useRedirect(); - const { enqueueSnackBar } = useSnackBar(); - 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 [switchToYearlyInterval] = - useSwitchSubscriptionToYearlyIntervalMutation(); const { data, loading } = useBillingPortalSessionQuery({ variables: { returnUrlPath: '/settings/billing', @@ -67,31 +52,6 @@ export const SettingsBilling = () => { } }; - const { openModal } = useModal(); - - const switchInterval = async () => { - try { - await switchToYearlyInterval(); - if (isDefined(currentWorkspace?.currentBillingSubscription)) { - const newCurrentWorkspace = { - ...currentWorkspace, - currentBillingSubscription: { - ...currentWorkspace?.currentBillingSubscription, - interval: SubscriptionInterval.Year, - }, - }; - setCurrentWorkspace(newCurrentWorkspace); - } - enqueueSnackBar(t`Subscription has been switched to yearly.`, { - variant: SnackBarVariant.Success, - }); - } catch (error: any) { - enqueueSnackBar(t`Error while switching subscription to yearly.`, { - variant: SnackBarVariant.Error, - }); - } - }; - return ( { ]} > + {hasNotCanceledCurrentSubscription && ( + + )} {hasNotCanceledCurrentSubscription && ( )}
- {currentWorkspace?.currentBillingSubscription?.interval === - SubscriptionInterval.Month && ( + {hasNotCanceledCurrentSubscription && (
)} -
- -
-
); }; diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 65f2163b8..85a618965 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -79,12 +79,14 @@ export const mockCurrentWorkspace: Workspace = { id: '7efbc3f7-6e5e-4128-957e-8d86808cdf6a', interval: SubscriptionInterval.Month, status: SubscriptionStatus.Active, + metadata: {}, }, billingSubscriptions: [ { __typename: 'BillingSubscription', id: '7efbc3f7-6e5e-4128-957e-8d86808cdf6a', status: SubscriptionStatus.Active, + metadata: {}, }, ], workspaceMembersCount: 1, diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts index da7328f71..e2d669133 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts @@ -25,4 +25,5 @@ export enum BillingExceptionCode { BILLING_STRIPE_ERROR = 'BILLING_STRIPE_ERROR', BILLING_SUBSCRIPTION_NOT_IN_TRIAL_PERIOD = 'BILLING_SUBSCRIPTION_NOT_IN_TRIAL_PERIOD', BILLING_SUBSCRIPTION_INTERVAL_NOT_SWITCHABLE = 'BILLING_SUBSCRIPTION_INTERVAL_NOT_SWITCHABLE', + BILLING_SUBSCRIPTION_PLAN_NOT_SWITCHABLE = 'BILLING_SUBSCRIPTION_PLAN_NOT_SWITCHABLE', } diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts index 12fd65190..4f352b75c 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts @@ -124,6 +124,17 @@ export class BillingResolver { return { success: true }; } + @Mutation(() => BillingUpdateOutput) + @UseGuards( + WorkspaceAuthGuard, + SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), + ) + async switchToEnterprisePlan(@AuthWorkspace() workspace: Workspace) { + await this.billingSubscriptionService.switchToEnterprisePlan(workspace); + + return { success: true }; + } + @Query(() => [BillingPlanOutput]) @UseGuards(WorkspaceAuthGuard) async plans(): Promise { diff --git a/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-subscription-item.output.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-subscription-item.output.ts index 4bc90bf8f..1231543cc 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-subscription-item.output.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-subscription-item.output.ts @@ -15,6 +15,9 @@ export class BillingSubscriptionItemDTO { @Field(() => Boolean) hasReachedCurrentPeriodCap: boolean; + @Field(() => Number, { nullable: true }) + quantity: number | null; + @Field(() => BillingProductDTO, { nullable: true }) billingProduct: BillingProductDTO; } diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts index d5e0ce418..fc74bcf69 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts @@ -16,6 +16,7 @@ import { Relation, UpdateDateColumn, } from 'typeorm'; +import graphqlTypeJson from 'graphql-type-json'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { BillingSubscriptionItemDTO } from 'src/engine/core-modules/billing/dtos/outputs/billing-subscription-item.output'; @@ -115,6 +116,7 @@ export class BillingSubscription { }) currentPeriodStart: Date; + @Field(() => graphqlTypeJson) @Column({ nullable: false, type: 'jsonb', default: {} }) metadata: Stripe.Metadata; diff --git a/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts index 8b8055c08..9d912d997 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts @@ -55,6 +55,8 @@ export class BillingRestApiExceptionFilter implements ExceptionFilter { case BillingExceptionCode.BILLING_METER_EVENT_FAILED: case BillingExceptionCode.BILLING_SUBSCRIPTION_NOT_IN_TRIAL_PERIOD: case BillingExceptionCode.BILLING_SUBSCRIPTION_INTERVAL_NOT_SWITCHABLE: + case BillingExceptionCode.BILLING_SUBSCRIPTION_PLAN_NOT_SWITCHABLE: + case BillingExceptionCode.BILLING_MISSING_REQUEST_BODY: return this.httpExceptionHandlerService.handleError( exception, response, diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-plan.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-plan.service.ts index 71f80b446..795de7ad1 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-plan.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-plan.service.ts @@ -66,7 +66,7 @@ export class BillingPlanService { where: { active: true, }, - relations: ['billingPrices'], + relations: ['billingPrices.billingProduct'], }); return planKeys.map((planKey) => { diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-product.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-product.service.ts index c3f49935e..ce2320e41 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-product.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-product.service.ts @@ -11,25 +11,39 @@ import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum'; import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service'; + @Injectable() export class BillingProductService { protected readonly logger = new Logger(BillingProductService.name); constructor(private readonly billingPlanService: BillingPlanService) {} - getProductPricesByInterval({ + async getProductPrices({ + interval, + planKey, + }: { + interval: SubscriptionInterval; + planKey: BillingPlanKey; + }): Promise { + const billingProducts = await this.getProductsByPlan(planKey); + + return this.getProductPricesByInterval({ + interval, + billingProductsByPlan: billingProducts, + }); + } + + private getProductPricesByInterval({ interval, billingProductsByPlan, }: { interval: SubscriptionInterval; billingProductsByPlan: BillingProduct[]; }): BillingPrice[] { - const billingPrices = billingProductsByPlan.flatMap((product) => + return billingProductsByPlan.flatMap((product) => product.billingPrices.filter( (price) => price.interval === interval && price.active, ), ); - - return billingPrices; } async getProductsByPlan(planKey: BillingPlanKey): Promise { diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts index c5ba4378e..8b39fc4f9 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts @@ -31,6 +31,8 @@ import { getPlanKeyFromSubscription } from 'src/engine/core-modules/billing/util import { getSubscriptionStatus } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum'; + @Injectable() export class BillingSubscriptionService { protected readonly logger = new Logger(BillingSubscriptionService.name); @@ -166,15 +168,14 @@ export class BillingSubscriptionService { ); } - const newInterval = SubscriptionInterval.Year; + const interval = SubscriptionInterval.Year; const planKey = getPlanKeyFromSubscription(billingSubscription); - const billingProductsByPlan = - await this.billingProductService.getProductsByPlan(planKey); + const pricesPerPlanArray = - this.billingProductService.getProductPricesByInterval({ - interval: newInterval, - billingProductsByPlan, + await this.billingProductService.getProductPrices({ + interval, + planKey, }); const subscriptionItemsToUpdate = this.getSubscriptionItemsToUpdate( @@ -188,14 +189,54 @@ export class BillingSubscriptionService { ); } + async switchToEnterprisePlan(workspace: Workspace) { + const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow( + { workspaceId: workspace.id }, + ); + + if (billingSubscription.metadata?.plan === BillingPlanKey.ENTERPRISE) { + throw new BillingException( + 'Cannot switch from Organization to Pro plan', + BillingExceptionCode.BILLING_SUBSCRIPTION_PLAN_NOT_SWITCHABLE, + ); + } + + const planKey = BillingPlanKey.ENTERPRISE; + + const interval = billingSubscription.interval as SubscriptionInterval; + + const pricesPerPlanArray = + await this.billingProductService.getProductPrices({ + interval, + planKey, + }); + + const subscriptionItemsToUpdate = this.getSubscriptionItemsToUpdate( + billingSubscription, + pricesPerPlanArray, + ); + + await this.stripeSubscriptionService.updateSubscriptionItems( + billingSubscription.stripeSubscriptionId, + subscriptionItemsToUpdate, + ); + + await this.stripeSubscriptionService.updateSubscription( + billingSubscription.stripeSubscriptionId, + { metadata: { ...billingSubscription?.metadata, plan: planKey } }, + ); + } + private getSubscriptionItemsToUpdate( billingSubscription: BillingSubscription, billingPricesPerPlanAndIntervalArray: BillingPrice[], ): BillingSubscriptionItem[] { - const subscriptionItemsToUpdate = - billingSubscription.billingSubscriptionItems.map((subscriptionItem) => { + return billingSubscription.billingSubscriptionItems.map( + (subscriptionItem) => { const matchingPrice = billingPricesPerPlanAndIntervalArray.find( - (price) => price.stripeProductId === subscriptionItem.stripeProductId, + (price) => + price.billingProduct.metadata.priceUsageBased === + subscriptionItem.billingProduct.metadata.priceUsageBased, ); if (!matchingPrice) { @@ -208,10 +249,10 @@ export class BillingSubscriptionService { return { ...subscriptionItem, stripePriceId: matchingPrice.stripePriceId, + stripeProductId: matchingPrice.stripeProductId, }; - }); - - return subscriptionItemsToUpdate; + }, + ); } async endTrialPeriod(workspace: Workspace) {