Update billing page ctas (#12459)
## Before  ## After <img width="1056" alt="image" src="https://github.com/user-attachments/assets/4a51b7c7-898b-485f-95e8-97911292f2b1" /> <img width="1299" alt="image" src="https://github.com/user-attachments/assets/44e5e545-a660-455a-91be-3b139ccb9f30" /> <img width="1180" alt="image" src="https://github.com/user-attachments/assets/0ca765a7-1d9a-473a-b7d2-c6f9b1a72417" /> <img width="963" alt="image" src="https://github.com/user-attachments/assets/b620fd8a-61c9-4dd3-a3b1-e4ba940371e4" /> <img width="863" alt="image" src="https://github.com/user-attachments/assets/a0d2dcb5-19e5-4f83-80d4-ad5a715f1e5f" /> --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
@ -248,6 +248,7 @@ export type BillingSubscription = {
|
||||
billingSubscriptionItems?: Maybe<Array<BillingSubscriptionItem>>;
|
||||
id: Scalars['UUID']['output'];
|
||||
interval?: Maybe<SubscriptionInterval>;
|
||||
metadata: Scalars['JSON']['output'];
|
||||
status: SubscriptionStatus;
|
||||
};
|
||||
|
||||
@ -256,6 +257,7 @@ export type BillingSubscriptionItem = {
|
||||
billingProduct?: Maybe<BillingProduct>;
|
||||
hasReachedCurrentPeriodCap: Scalars['Boolean']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
quantity?: Maybe<Scalars['Float']['output']>;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@ -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> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
@ -240,6 +240,7 @@ export type BillingSubscription = {
|
||||
billingSubscriptionItems?: Maybe<Array<BillingSubscriptionItem>>;
|
||||
id: Scalars['UUID'];
|
||||
interval?: Maybe<SubscriptionInterval>;
|
||||
metadata: Scalars['JSON'];
|
||||
status: SubscriptionStatus;
|
||||
};
|
||||
|
||||
@ -248,6 +249,7 @@ export type BillingSubscriptionItem = {
|
||||
billingProduct?: Maybe<BillingProduct>;
|
||||
hasReachedCurrentPeriodCap: Scalars['Boolean'];
|
||||
id: Scalars['UUID'];
|
||||
quantity?: Maybe<Scalars['Float']>;
|
||||
};
|
||||
|
||||
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<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type BillingPortalSessionQuery = { __typename?: 'Query', billingPortalSession: { __typename?: 'BillingSessionOutput', url?: string | null } };
|
||||
|
||||
export type CheckoutSessionMutationVariables = Exact<{
|
||||
recurringInterval: SubscriptionInterval;
|
||||
successUrlPath?: InputMaybe<Scalars['String']>;
|
||||
@ -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<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
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<string> | 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<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<typeof useValidatePasswordResetTokenQuery>;
|
||||
export type ValidatePasswordResetTokenLazyQueryHookResult = ReturnType<typeof useValidatePasswordResetTokenLazyQuery>;
|
||||
export type ValidatePasswordResetTokenQueryResult = Apollo.QueryResult<ValidatePasswordResetTokenQuery, ValidatePasswordResetTokenQueryVariables>;
|
||||
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<CheckoutSessionMutation, CheckoutSessionMutationVariables>;
|
||||
|
||||
/**
|
||||
* __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<CheckoutSessionMutation, CheckoutSessionMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CheckoutSessionMutation, CheckoutSessionMutationVariables>(CheckoutSessionDocument, options);
|
||||
}
|
||||
export type CheckoutSessionMutationHookResult = ReturnType<typeof useCheckoutSessionMutation>;
|
||||
export type CheckoutSessionMutationResult = Apollo.MutationResult<CheckoutSessionMutation>;
|
||||
export type CheckoutSessionMutationOptions = Apollo.BaseMutationOptions<CheckoutSessionMutation, CheckoutSessionMutationVariables>;
|
||||
export const EndSubscriptionTrialPeriodDocument = gql`
|
||||
mutation EndSubscriptionTrialPeriod {
|
||||
endSubscriptionTrialPeriod {
|
||||
status
|
||||
hasPaymentMethod
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type EndSubscriptionTrialPeriodMutationFn = Apollo.MutationFunction<EndSubscriptionTrialPeriodMutation, EndSubscriptionTrialPeriodMutationVariables>;
|
||||
|
||||
/**
|
||||
* __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<EndSubscriptionTrialPeriodMutation, EndSubscriptionTrialPeriodMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<EndSubscriptionTrialPeriodMutation, EndSubscriptionTrialPeriodMutationVariables>(EndSubscriptionTrialPeriodDocument, options);
|
||||
}
|
||||
export type EndSubscriptionTrialPeriodMutationHookResult = ReturnType<typeof useEndSubscriptionTrialPeriodMutation>;
|
||||
export type EndSubscriptionTrialPeriodMutationResult = Apollo.MutationResult<EndSubscriptionTrialPeriodMutation>;
|
||||
export type EndSubscriptionTrialPeriodMutationOptions = Apollo.BaseMutationOptions<EndSubscriptionTrialPeriodMutation, EndSubscriptionTrialPeriodMutationVariables>;
|
||||
export const SwitchSubscriptionToEnterprisePlanDocument = gql`
|
||||
mutation SwitchSubscriptionToEnterprisePlan {
|
||||
switchToEnterprisePlan {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SwitchSubscriptionToEnterprisePlanMutationFn = Apollo.MutationFunction<SwitchSubscriptionToEnterprisePlanMutation, SwitchSubscriptionToEnterprisePlanMutationVariables>;
|
||||
|
||||
/**
|
||||
* __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<SwitchSubscriptionToEnterprisePlanMutation, SwitchSubscriptionToEnterprisePlanMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SwitchSubscriptionToEnterprisePlanMutation, SwitchSubscriptionToEnterprisePlanMutationVariables>(SwitchSubscriptionToEnterprisePlanDocument, options);
|
||||
}
|
||||
export type SwitchSubscriptionToEnterprisePlanMutationHookResult = ReturnType<typeof useSwitchSubscriptionToEnterprisePlanMutation>;
|
||||
export type SwitchSubscriptionToEnterprisePlanMutationResult = Apollo.MutationResult<SwitchSubscriptionToEnterprisePlanMutation>;
|
||||
export type SwitchSubscriptionToEnterprisePlanMutationOptions = Apollo.BaseMutationOptions<SwitchSubscriptionToEnterprisePlanMutation, SwitchSubscriptionToEnterprisePlanMutationVariables>;
|
||||
export const SwitchSubscriptionToYearlyIntervalDocument = gql`
|
||||
mutation SwitchSubscriptionToYearlyInterval {
|
||||
switchToYearlyInterval {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SwitchSubscriptionToYearlyIntervalMutationFn = Apollo.MutationFunction<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>;
|
||||
|
||||
/**
|
||||
* __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<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>(SwitchSubscriptionToYearlyIntervalDocument, options);
|
||||
}
|
||||
export type SwitchSubscriptionToYearlyIntervalMutationHookResult = ReturnType<typeof useSwitchSubscriptionToYearlyIntervalMutation>;
|
||||
export type SwitchSubscriptionToYearlyIntervalMutationResult = Apollo.MutationResult<SwitchSubscriptionToYearlyIntervalMutation>;
|
||||
export type SwitchSubscriptionToYearlyIntervalMutationOptions = Apollo.BaseMutationOptions<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>;
|
||||
export const BillingBaseProductPricesDocument = gql`
|
||||
query billingBaseProductPrices {
|
||||
plans {
|
||||
@ -4417,80 +4566,6 @@ export function useBillingPortalSessionLazyQuery(baseOptions?: Apollo.LazyQueryH
|
||||
export type BillingPortalSessionQueryHookResult = ReturnType<typeof useBillingPortalSessionQuery>;
|
||||
export type BillingPortalSessionLazyQueryHookResult = ReturnType<typeof useBillingPortalSessionLazyQuery>;
|
||||
export type BillingPortalSessionQueryResult = Apollo.QueryResult<BillingPortalSessionQuery, BillingPortalSessionQueryVariables>;
|
||||
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<CheckoutSessionMutation, CheckoutSessionMutationVariables>;
|
||||
|
||||
/**
|
||||
* __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<CheckoutSessionMutation, CheckoutSessionMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CheckoutSessionMutation, CheckoutSessionMutationVariables>(CheckoutSessionDocument, options);
|
||||
}
|
||||
export type CheckoutSessionMutationHookResult = ReturnType<typeof useCheckoutSessionMutation>;
|
||||
export type CheckoutSessionMutationResult = Apollo.MutationResult<CheckoutSessionMutation>;
|
||||
export type CheckoutSessionMutationOptions = Apollo.BaseMutationOptions<CheckoutSessionMutation, CheckoutSessionMutationVariables>;
|
||||
export const EndSubscriptionTrialPeriodDocument = gql`
|
||||
mutation EndSubscriptionTrialPeriod {
|
||||
endSubscriptionTrialPeriod {
|
||||
status
|
||||
hasPaymentMethod
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type EndSubscriptionTrialPeriodMutationFn = Apollo.MutationFunction<EndSubscriptionTrialPeriodMutation, EndSubscriptionTrialPeriodMutationVariables>;
|
||||
|
||||
/**
|
||||
* __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<EndSubscriptionTrialPeriodMutation, EndSubscriptionTrialPeriodMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<EndSubscriptionTrialPeriodMutation, EndSubscriptionTrialPeriodMutationVariables>(EndSubscriptionTrialPeriodDocument, options);
|
||||
}
|
||||
export type EndSubscriptionTrialPeriodMutationHookResult = ReturnType<typeof useEndSubscriptionTrialPeriodMutation>;
|
||||
export type EndSubscriptionTrialPeriodMutationResult = Apollo.MutationResult<EndSubscriptionTrialPeriodMutation>;
|
||||
export type EndSubscriptionTrialPeriodMutationOptions = Apollo.BaseMutationOptions<EndSubscriptionTrialPeriodMutation, EndSubscriptionTrialPeriodMutationVariables>;
|
||||
export const GetMeteredProductsUsageDocument = gql`
|
||||
query GetMeteredProductsUsage {
|
||||
getMeteredProductsUsage {
|
||||
@ -4530,38 +4605,6 @@ export function useGetMeteredProductsUsageLazyQuery(baseOptions?: Apollo.LazyQue
|
||||
export type GetMeteredProductsUsageQueryHookResult = ReturnType<typeof useGetMeteredProductsUsageQuery>;
|
||||
export type GetMeteredProductsUsageLazyQueryHookResult = ReturnType<typeof useGetMeteredProductsUsageLazyQuery>;
|
||||
export type GetMeteredProductsUsageQueryResult = Apollo.QueryResult<GetMeteredProductsUsageQuery, GetMeteredProductsUsageQueryVariables>;
|
||||
export const SwitchSubscriptionToYearlyIntervalDocument = gql`
|
||||
mutation SwitchSubscriptionToYearlyInterval {
|
||||
switchToYearlyInterval {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SwitchSubscriptionToYearlyIntervalMutationFn = Apollo.MutationFunction<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>;
|
||||
|
||||
/**
|
||||
* __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<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>(SwitchSubscriptionToYearlyIntervalDocument, options);
|
||||
}
|
||||
export type SwitchSubscriptionToYearlyIntervalMutationHookResult = ReturnType<typeof useSwitchSubscriptionToYearlyIntervalMutation>;
|
||||
export type SwitchSubscriptionToYearlyIntervalMutationResult = Apollo.MutationResult<SwitchSubscriptionToYearlyIntervalMutation>;
|
||||
export type SwitchSubscriptionToYearlyIntervalMutationOptions = Apollo.BaseMutationOptions<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>;
|
||||
export const GetClientConfigDocument = gql`
|
||||
query GetClientConfig {
|
||||
clientConfig {
|
||||
|
||||
@ -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.`}
|
||||
/>
|
||||
<StyledMonthlyCreditsContainer>
|
||||
<SubscriptionInfoContainer>
|
||||
<SettingsBillingLabelValueItem
|
||||
label={t`Free Credits Used`}
|
||||
value={`${formattedFreeUsageQuantity}/${formatAmount(includedFreeQuantity)}`}
|
||||
@ -91,7 +80,7 @@ export const SettingsBillingMonthlyCreditsSection = () => {
|
||||
value={`$${formatNumber(totalCostCents / 100, 2)}`}
|
||||
/>
|
||||
)}
|
||||
</StyledMonthlyCreditsContainer>
|
||||
</SubscriptionInfoContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 ? (
|
||||
<Tag color={'sky'} text={t`Pro`} />
|
||||
) : isEnterprisePlan ? (
|
||||
<Tag color={'purple'} text={t`Organization`} />
|
||||
) : 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 (
|
||||
<Section>
|
||||
<H2Title title={t`Subscription`} description={t`About my subscription`} />
|
||||
<SubscriptionInfoContainer>
|
||||
<SubscriptionInfoRowContainer
|
||||
label={t`Plan`}
|
||||
Icon={IconTag}
|
||||
value={planTag}
|
||||
/>
|
||||
<SubscriptionInfoRowContainer
|
||||
label={t`Billing interval`}
|
||||
Icon={IconCalendarEvent}
|
||||
value={intervalLabel}
|
||||
/>
|
||||
<SubscriptionInfoRowContainer
|
||||
label={t`Seats`}
|
||||
Icon={IconUsers}
|
||||
value={seats}
|
||||
/>
|
||||
</SubscriptionInfoContainer>
|
||||
<StyledSwitchButtonContainer>
|
||||
{isMonthlyPlan && (
|
||||
<Button
|
||||
Icon={IconArrowUp}
|
||||
title={t`Switch to Yearly`}
|
||||
variant="secondary"
|
||||
onClick={() => openModal(SWITCH_BILLING_INTERVAL_MODAL_ID)}
|
||||
disabled={!canSwitchSubscription}
|
||||
/>
|
||||
)}
|
||||
{isProPlan && (
|
||||
<Button
|
||||
Icon={IconArrowUp}
|
||||
title={t`Switch to Organization`}
|
||||
variant="secondary"
|
||||
onClick={() => openModal(SWITCH_BILLING_PLAN_MODAL_ID)}
|
||||
disabled={!canSwitchSubscription}
|
||||
/>
|
||||
)}
|
||||
</StyledSwitchButtonContainer>
|
||||
<ConfirmationModal
|
||||
modalId={SWITCH_BILLING_INTERVAL_MODAL_ID}
|
||||
title={t`Change to Yearly?`}
|
||||
subtitle={t`You will be charged $${yearlyPrice} per user per month billed annually. A prorata with your current subscription will be applied.`}
|
||||
onConfirmClick={switchInterval}
|
||||
confirmButtonText={t`Confirm`}
|
||||
confirmButtonAccent={'blue'}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
modalId={SWITCH_BILLING_PLAN_MODAL_ID}
|
||||
title={t`Change to Organization Plan?`}
|
||||
subtitle={
|
||||
isYearlyPlan
|
||||
? t`You will be charged $${enterprisePrice} per user per month billed annually.`
|
||||
: t`You will be charged $${enterprisePrice} per user per month.`
|
||||
}
|
||||
onConfirmClick={switchPlan}
|
||||
confirmButtonText={t`Confirm`}
|
||||
confirmButtonAccent={'blue'}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -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 };
|
||||
@ -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 (
|
||||
<StyledContainer>
|
||||
<StyledIconLabelContainer>
|
||||
<Icon size={theme.icon.size.md} />
|
||||
<StyledLabelContainer>{label}</StyledLabelContainer>
|
||||
</StyledIconLabelContainer>
|
||||
{value}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SWITCH_SUBSCRIPTION_TO_ENTERPRISE_PLAN = gql`
|
||||
mutation SwitchSubscriptionToEnterprisePlan {
|
||||
switchToEnterprisePlan {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -38,11 +38,13 @@ const Wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
id: '1',
|
||||
interval: SubscriptionInterval.Month,
|
||||
status: SubscriptionStatus.Active,
|
||||
metadata: {},
|
||||
},
|
||||
billingSubscriptions: [
|
||||
{
|
||||
id: '1',
|
||||
status: SubscriptionStatus.Active,
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -50,6 +50,7 @@ describe('useSubscriptionStatus', () => {
|
||||
currentBillingSubscription: {
|
||||
id: v4(),
|
||||
status: subscriptionStatus,
|
||||
metadata: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`Billing`}
|
||||
@ -104,12 +64,15 @@ export const SettingsBilling = () => {
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
{hasNotCanceledCurrentSubscription && (
|
||||
<SettingsBillingSubscriptionInfo />
|
||||
)}
|
||||
{hasNotCanceledCurrentSubscription && (
|
||||
<SettingsBillingMonthlyCreditsSection />
|
||||
)}
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Manage your subscription`}
|
||||
title={t`Manage billing information`}
|
||||
description={t`Edit payment method, see your invoices and more`}
|
||||
/>
|
||||
<Button
|
||||
@ -120,45 +83,23 @@ export const SettingsBilling = () => {
|
||||
disabled={billingPortalButtonDisabled}
|
||||
/>
|
||||
</Section>
|
||||
{currentWorkspace?.currentBillingSubscription?.interval ===
|
||||
SubscriptionInterval.Month && (
|
||||
{hasNotCanceledCurrentSubscription && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Edit billing interval`}
|
||||
description={t`Switch from monthly to yearly`}
|
||||
title={t`Cancel your subscription`}
|
||||
description={t`Your workspace will be disabled`}
|
||||
/>
|
||||
<Button
|
||||
Icon={IconCalendarEvent}
|
||||
title={t`Switch to yearly`}
|
||||
Icon={IconCircleX}
|
||||
title={t`Cancel Plan`}
|
||||
variant="secondary"
|
||||
onClick={() => openModal(SWITCH_BILLING_INTERVAL_MODAL_ID)}
|
||||
disabled={!hasNotCanceledCurrentSubscription}
|
||||
accent="danger"
|
||||
onClick={openBillingPortal}
|
||||
disabled={billingPortalButtonDisabled}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Cancel your subscription`}
|
||||
description={t`Your workspace will be disabled`}
|
||||
/>
|
||||
<Button
|
||||
Icon={IconCircleX}
|
||||
title={t`Cancel Plan`}
|
||||
variant="secondary"
|
||||
accent="danger"
|
||||
onClick={openBillingPortal}
|
||||
disabled={!hasNotCanceledCurrentSubscription}
|
||||
/>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
<ConfirmationModal
|
||||
modalId={SWITCH_BILLING_INTERVAL_MODAL_ID}
|
||||
title={t`Switch billing to yearly`}
|
||||
subtitle={t`Are you sure that you want to change your billing interval? You will be charged immediately for the full year.`}
|
||||
onConfirmClick={switchInterval}
|
||||
confirmButtonText={t`Change to yearly`}
|
||||
confirmButtonAccent={'blue'}
|
||||
/>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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<BillingPlanOutput[]> {
|
||||
|
||||
@ -15,6 +15,9 @@ export class BillingSubscriptionItemDTO {
|
||||
@Field(() => Boolean)
|
||||
hasReachedCurrentPeriodCap: boolean;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
quantity: number | null;
|
||||
|
||||
@Field(() => BillingProductDTO, { nullable: true })
|
||||
billingProduct: BillingProductDTO;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -66,7 +66,7 @@ export class BillingPlanService {
|
||||
where: {
|
||||
active: true,
|
||||
},
|
||||
relations: ['billingPrices'],
|
||||
relations: ['billingPrices.billingProduct'],
|
||||
});
|
||||
|
||||
return planKeys.map((planKey) => {
|
||||
|
||||
@ -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<BillingPrice[]> {
|
||||
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<BillingProduct[]> {
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user