From 0a7a2c3083cefaa9c0980e0631f0f8b9062d917e Mon Sep 17 00:00:00 2001 From: JimmFly Date: Tue, 10 Dec 2024 06:31:36 +0000 Subject: [PATCH] feat(core): add workspace billing (#9043) --- .../setting/general-setting/plans/actions.tsx | 14 +- .../workspace-setting/billing/index.tsx | 37 ++--- .../cloud/entities/workspace-invoices.ts | 83 +++++++++++ .../cloud/entities/workspace-subscription.ts | 132 ++++++++++++++++++ .../frontend/core/src/modules/cloud/index.ts | 18 ++- .../cloud/services/workspace-invoices.ts | 7 + .../cloud/services/workspace-subscription.ts | 19 +++ .../core/src/modules/cloud/stores/invoices.ts | 21 ++- .../src/modules/cloud/stores/subscription.ts | 47 ++++++- .../graphql/get-workspace-subscription.gql | 15 ++ .../frontend/graphql/src/graphql/index.ts | 46 ++++++ .../src/graphql/workspace-invoices.gql | 15 ++ packages/frontend/graphql/src/schema.ts | 58 ++++++++ 13 files changed, 487 insertions(+), 25 deletions(-) create mode 100644 packages/frontend/core/src/modules/cloud/entities/workspace-invoices.ts create mode 100644 packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts create mode 100644 packages/frontend/core/src/modules/cloud/services/workspace-invoices.ts create mode 100644 packages/frontend/core/src/modules/cloud/services/workspace-subscription.ts create mode 100644 packages/frontend/graphql/src/graphql/get-workspace-subscription.gql create mode 100644 packages/frontend/graphql/src/graphql/workspace-invoices.gql diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/actions.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/actions.tsx index 037cfccb66..4990205ef0 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/actions.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/actions.tsx @@ -8,7 +8,11 @@ import { nanoid } from 'nanoid'; import type { PropsWithChildren } from 'react'; import { useEffect, useState } from 'react'; -import { AuthService, SubscriptionService } from '../../../../../modules/cloud'; +import { + AuthService, + SubscriptionService, + WorkspaceSubscriptionService, +} from '../../../../../modules/cloud'; import { ConfirmLoadingModal, DowngradeModal } from './modals'; /** @@ -101,15 +105,15 @@ export const CancelTeamAction = ({ } & PropsWithChildren) => { const [idempotencyKey, setIdempotencyKey] = useState(nanoid()); const [isMutating, setIsMutating] = useState(false); - const subscription = useService(SubscriptionService).subscription; - const teamSubscription = useLiveData(subscription.team$); + const subscription = useService(WorkspaceSubscriptionService).subscription; + const workspaceSubscription = useLiveData(subscription.subscription$); const authService = useService(AuthService); const downgradeNotify = useDowngradeNotify(); const downgrade = useAsyncCallback(async () => { try { const account = authService.session.account$.value; - const prevRecurring = teamSubscription?.recurring; + const prevRecurring = workspaceSubscription?.recurring; setIsMutating(true); await subscription.cancelSubscription(idempotencyKey); await subscription.waitForRevalidation(); @@ -133,7 +137,7 @@ export const CancelTeamAction = ({ } }, [ authService.session.account$.value, - teamSubscription, + workspaceSubscription, subscription, idempotencyKey, onOpenChange, diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx index 7ed07dc437..4ba34cfeab 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx @@ -10,8 +10,8 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hoo import { useMutation } from '@affine/core/components/hooks/use-mutation'; import { AuthService, - InvoicesService, - SubscriptionService, + WorkspaceInvoicesService, + WorkspaceSubscriptionService, } from '@affine/core/modules/cloud'; import { UrlService } from '@affine/core/modules/url'; import { @@ -41,8 +41,10 @@ import * as styles from './styles.css'; export const WorkspaceSettingBilling = () => { const t = useI18n(); const workspace = useService(WorkspaceService).workspace; - const subscriptionService = useService(SubscriptionService); - const team = useLiveData(subscriptionService.subscription.team$); + const subscriptionService = useService(WorkspaceSubscriptionService); + const subscription = useLiveData( + subscriptionService.subscription.subscription$ + ); const title = useLiveData(workspace.name$) || 'untitled'; if (workspace === null) { @@ -51,7 +53,7 @@ export const WorkspaceSettingBilling = () => { return null; } - if (!team) { + if (!subscription) { return ; } @@ -67,8 +69,8 @@ export const WorkspaceSettingBilling = () => { - {team.end && team.canceledAt ? ( - + {subscription?.end && subscription.canceledAt ? ( + ) : null} @@ -81,8 +83,10 @@ export const WorkspaceSettingBilling = () => { const TeamCard = () => { const t = useI18n(); - const subscriptionService = useService(SubscriptionService); - const teamSubscription = useLiveData(subscriptionService.subscription.team$); + const subscriptionService = useService(WorkspaceSubscriptionService); + const teamSubscription = useLiveData( + subscriptionService.subscription.subscription$ + ); const teamPrices = useLiveData(subscriptionService.prices.teamPrice$); const [openCancelModal, setOpenCancelModal] = useState(false); @@ -198,23 +202,22 @@ const ResumeSubscription = ({ expirationDate }: { expirationDate: string }) => { const TypeFormLink = () => { const t = useI18n(); - const subscriptionService = useService(SubscriptionService); + const workspaceSubscriptionService = useService(WorkspaceSubscriptionService); const authService = useService(AuthService); - const team = useLiveData(subscriptionService.subscription.team$); + const workspaceSubscription = useLiveData( + workspaceSubscriptionService.subscription.subscription$ + ); const account = useLiveData(authService.session.account$); if (!account) return null; - const plan = []; - if (team) plan.push(SubscriptionPlan.Team); - const link = getUpgradeQuestionnaireLink({ name: account.info?.name, id: account.id, email: account.email, - recurring: team?.recurring ?? SubscriptionRecurring.Yearly, - plan, + recurring: workspaceSubscription?.recurring ?? SubscriptionRecurring.Yearly, + plan: SubscriptionPlan.Team, }); return ( @@ -263,7 +266,7 @@ const PaymentMethodUpdater = () => { const BillingHistory = () => { const t = useI18n(); - const invoicesService = useService(InvoicesService); + const invoicesService = useService(WorkspaceInvoicesService); const pageInvoices = useLiveData(invoicesService.invoices.pageInvoices$); const invoiceCount = useLiveData(invoicesService.invoices.invoiceCount$); const isLoading = useLiveData(invoicesService.invoices.isLoading$); diff --git a/packages/frontend/core/src/modules/cloud/entities/workspace-invoices.ts b/packages/frontend/core/src/modules/cloud/entities/workspace-invoices.ts new file mode 100644 index 0000000000..ba6d8fe6dc --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/entities/workspace-invoices.ts @@ -0,0 +1,83 @@ +import type { InvoicesQuery } from '@affine/graphql'; +import type { WorkspaceService } from '@toeverything/infra'; +import { + backoffRetry, + catchErrorInto, + effect, + Entity, + exhaustMapSwitchUntilChanged, + fromPromise, + LiveData, + onComplete, + onStart, +} from '@toeverything/infra'; +import { EMPTY, map, mergeMap } from 'rxjs'; + +import { isBackendError, isNetworkError } from '../error'; +import type { InvoicesStore } from '../stores/invoices'; + +export type Invoice = NonNullable< + InvoicesQuery['currentUser'] +>['invoices'][number]; + +export class WorkspaceInvoices extends Entity { + constructor( + private readonly store: InvoicesStore, + private readonly workspaceService: WorkspaceService + ) { + super(); + } + + pageNum$ = new LiveData(0); + invoiceCount$ = new LiveData(undefined); + pageInvoices$ = new LiveData(undefined); + + isLoading$ = new LiveData(false); + error$ = new LiveData(null); + + readonly PAGE_SIZE = 8; + + readonly revalidate = effect( + map(() => this.pageNum$.value), + exhaustMapSwitchUntilChanged( + (a, b) => a === b, + pageNum => { + return fromPromise(async signal => { + return this.store.fetchWorkspaceInvoices( + pageNum * this.PAGE_SIZE, + this.PAGE_SIZE, + this.workspaceService.workspace.id, + signal + ); + }).pipe( + mergeMap(data => { + this.invoiceCount$.setValue(data.invoiceCount); + this.pageInvoices$.setValue(data.invoices); + return EMPTY; + }), + backoffRetry({ + when: isNetworkError, + count: Infinity, + }), + backoffRetry({ + when: isBackendError, + }), + catchErrorInto(this.error$), + onStart(() => { + this.pageInvoices$.setValue(undefined); + this.isLoading$.setValue(true); + }), + onComplete(() => this.isLoading$.setValue(false)) + ); + } + ) + ); + + setPageNum(pageNum: number) { + this.pageNum$.setValue(pageNum); + } + + override dispose(): void { + this.revalidate.unsubscribe(); + } +} diff --git a/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts b/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts new file mode 100644 index 0000000000..e9f6a74825 --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts @@ -0,0 +1,132 @@ +import type { SubscriptionQuery, SubscriptionRecurring } from '@affine/graphql'; +import { SubscriptionPlan } from '@affine/graphql'; +import type { WorkspaceService } from '@toeverything/infra'; +import { + backoffRetry, + catchErrorInto, + effect, + Entity, + exhaustMapWithTrailing, + fromPromise, + LiveData, + onComplete, + onStart, +} from '@toeverything/infra'; +import { EMPTY, mergeMap } from 'rxjs'; + +import { isBackendError, isNetworkError } from '../error'; +import type { ServerService } from '../services/server'; +import type { SubscriptionStore } from '../stores/subscription'; + +export type SubscriptionType = NonNullable< + SubscriptionQuery['currentUser'] +>['subscriptions'][number]; + +export class WorkspaceSubscription extends Entity { + subscription$ = new LiveData(null); + isRevalidating$ = new LiveData(false); + error$ = new LiveData(null); + + team$ = this.subscription$.map( + subscription => subscription?.plan === SubscriptionPlan.Team + ); + + constructor( + private readonly workspaceService: WorkspaceService, + private readonly serverService: ServerService, + private readonly store: SubscriptionStore + ) { + super(); + } + + async resumeSubscription(idempotencyKey: string, plan?: SubscriptionPlan) { + await this.store.mutateResumeSubscription(idempotencyKey, plan); + await this.waitForRevalidation(); + } + + async cancelSubscription(idempotencyKey: string, plan?: SubscriptionPlan) { + await this.store.mutateCancelSubscription(idempotencyKey, plan); + await this.waitForRevalidation(); + } + + async setSubscriptionRecurring( + idempotencyKey: string, + recurring: SubscriptionRecurring, + plan?: SubscriptionPlan + ) { + await this.store.setSubscriptionRecurring(idempotencyKey, recurring, plan); + await this.waitForRevalidation(); + } + + async waitForRevalidation(signal?: AbortSignal) { + this.revalidate(); + await this.isRevalidating$.waitFor( + isRevalidating => !isRevalidating, + signal + ); + } + + revalidate = effect( + exhaustMapWithTrailing(() => { + return fromPromise(async signal => { + const currentWorkspaceId = this.workspaceService.workspace.id; + if (!currentWorkspaceId) { + return undefined; // no subscription if no user + } + const serverConfig = + await this.serverService.server.features$.waitForNonNull(signal); + + if (!serverConfig.payment) { + // No payment feature, no subscription + return { + workspaceId: currentWorkspaceId, + subscription: null, + }; + } + const { workspaceId, subscription } = + await this.store.fetchWorkspaceSubscriptions( + currentWorkspaceId, + signal + ); + return { + workspaceId: workspaceId, + subscription: subscription, + }; + }).pipe( + backoffRetry({ + when: isNetworkError, + count: Infinity, + }), + backoffRetry({ + when: isBackendError, + }), + mergeMap(data => { + if (data && data.subscription && data.workspaceId) { + this.store.setCachedWorkspaceSubscription( + data.workspaceId, + data.subscription + ); + this.subscription$.next(data.subscription); + } else { + this.subscription$.next(undefined); + } + return EMPTY; + }), + catchErrorInto(this.error$), + onStart(() => this.isRevalidating$.next(true)), + onComplete(() => this.isRevalidating$.next(false)) + ); + }) + ); + + reset() { + this.subscription$.next(null); + this.team$.next(false); + this.isRevalidating$.next(false); + this.error$.next(null); + } + + override dispose(): void { + this.revalidate.unsubscribe(); + } +} diff --git a/packages/frontend/core/src/modules/cloud/index.ts b/packages/frontend/core/src/modules/cloud/index.ts index 96754a34eb..852fbeb75c 100644 --- a/packages/frontend/core/src/modules/cloud/index.ts +++ b/packages/frontend/core/src/modules/cloud/index.ts @@ -28,7 +28,9 @@ export { UserCopilotQuotaService } from './services/user-copilot-quota'; export { UserFeatureService } from './services/user-feature'; export { UserQuotaService } from './services/user-quota'; export { WebSocketService } from './services/websocket'; +export { WorkspaceInvoicesService } from './services/workspace-invoices'; export { WorkspaceServerService } from './services/workspace-server'; +export { WorkspaceSubscriptionService } from './services/workspace-subscription'; export type { ServerConfig } from './types'; import { @@ -39,6 +41,7 @@ import { GlobalState, GlobalStateService, WorkspaceScope, + WorkspaceService, } from '@toeverything/infra'; import { UrlService } from '../url'; @@ -51,6 +54,8 @@ import { SubscriptionPrices } from './entities/subscription-prices'; import { UserCopilotQuota } from './entities/user-copilot-quota'; import { UserFeature } from './entities/user-feature'; import { UserQuota } from './entities/user-quota'; +import { WorkspaceInvoices } from './entities/workspace-invoices'; +import { WorkspaceSubscription } from './entities/workspace-subscription'; import { DefaultRawFetchProvider, RawFetchProvider } from './provider/fetch'; import { ValidatorProvider } from './provider/validator'; import { WebSocketAuthProvider } from './provider/websocket-auth'; @@ -70,7 +75,9 @@ import { UserCopilotQuotaService } from './services/user-copilot-quota'; import { UserFeatureService } from './services/user-feature'; import { UserQuotaService } from './services/user-quota'; import { WebSocketService } from './services/websocket'; +import { WorkspaceInvoicesService } from './services/workspace-invoices'; import { WorkspaceServerService } from './services/workspace-server'; +import { WorkspaceSubscriptionService } from './services/workspace-subscription'; import { AuthStore } from './stores/auth'; import { CloudDocMetaStore } from './stores/cloud-doc-meta'; import { InvoicesStore } from './stores/invoices'; @@ -142,7 +149,16 @@ export function configureCloudModule(framework: Framework) { .store(UserFeatureStore, [GraphQLService]) .service(InvoicesService) .store(InvoicesStore, [GraphQLService]) - .entity(Invoices, [InvoicesStore]); + .entity(Invoices, [InvoicesStore]) + .scope(WorkspaceScope) + .service(WorkspaceSubscriptionService, [SubscriptionStore]) + .entity(WorkspaceSubscription, [ + WorkspaceService, + ServerService, + SubscriptionStore, + ]) + .service(WorkspaceInvoicesService) + .entity(WorkspaceInvoices, [InvoicesStore, WorkspaceService]); framework .scope(WorkspaceScope) diff --git a/packages/frontend/core/src/modules/cloud/services/workspace-invoices.ts b/packages/frontend/core/src/modules/cloud/services/workspace-invoices.ts new file mode 100644 index 0000000000..fb47d16139 --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/services/workspace-invoices.ts @@ -0,0 +1,7 @@ +import { Service } from '@toeverything/infra'; + +import { WorkspaceInvoices } from '../entities/workspace-invoices'; + +export class WorkspaceInvoicesService extends Service { + invoices = this.framework.createEntity(WorkspaceInvoices); +} diff --git a/packages/frontend/core/src/modules/cloud/services/workspace-subscription.ts b/packages/frontend/core/src/modules/cloud/services/workspace-subscription.ts new file mode 100644 index 0000000000..c9850da2c5 --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/services/workspace-subscription.ts @@ -0,0 +1,19 @@ +import { type CreateCheckoutSessionInput } from '@affine/graphql'; +import { Service } from '@toeverything/infra'; + +import { SubscriptionPrices } from '../entities/subscription-prices'; +import { WorkspaceSubscription } from '../entities/workspace-subscription'; +import type { SubscriptionStore } from '../stores/subscription'; + +export class WorkspaceSubscriptionService extends Service { + subscription = this.framework.createEntity(WorkspaceSubscription); + prices = this.framework.createEntity(SubscriptionPrices); + + constructor(private readonly store: SubscriptionStore) { + super(); + } + + async createCheckoutSession(input: CreateCheckoutSessionInput) { + return await this.store.createCheckoutSession(input); + } +} diff --git a/packages/frontend/core/src/modules/cloud/stores/invoices.ts b/packages/frontend/core/src/modules/cloud/stores/invoices.ts index 96192b3c3f..2481419352 100644 --- a/packages/frontend/core/src/modules/cloud/stores/invoices.ts +++ b/packages/frontend/core/src/modules/cloud/stores/invoices.ts @@ -1,4 +1,4 @@ -import { invoicesQuery } from '@affine/graphql'; +import { invoicesQuery, workspaceInvoicesQuery } from '@affine/graphql'; import { Store } from '@toeverything/infra'; import type { GraphQLService } from '../services/graphql'; @@ -21,4 +21,23 @@ export class InvoicesStore extends Store { return data.currentUser; } + + async fetchWorkspaceInvoices( + skip: number, + take: number, + workspaceId: string, + signal?: AbortSignal + ) { + const data = await this.graphqlService.gql({ + query: workspaceInvoicesQuery, + variables: { skip, take, workspaceId }, + context: { signal }, + }); + + if (!data.workspace) { + throw new Error('No workspace'); + } + + return data.workspace; + } } diff --git a/packages/frontend/core/src/modules/cloud/stores/subscription.ts b/packages/frontend/core/src/modules/cloud/stores/subscription.ts index 5e32a1eac6..dd7af73caa 100644 --- a/packages/frontend/core/src/modules/cloud/stores/subscription.ts +++ b/packages/frontend/core/src/modules/cloud/stores/subscription.ts @@ -5,6 +5,7 @@ import type { import { cancelSubscriptionMutation, createCheckoutSessionMutation, + getWorkspaceSubscriptionQuery, pricesQuery, resumeSubscriptionMutation, SubscriptionPlan, @@ -27,7 +28,11 @@ const getDefaultSubscriptionSuccessCallbackLink = ( scheme?: string ) => { const path = - plan === SubscriptionPlan.AI ? '/ai-upgrade-success' : '/upgrade-success'; + plan === SubscriptionPlan.Team + ? '/upgrade-success/team' + : plan === SubscriptionPlan.AI + ? '/ai-upgrade-success' + : '/upgrade-success'; const urlString = baseUrl + path; const url = new URL(urlString); if (scheme) { @@ -64,6 +69,30 @@ export class SubscriptionStore extends Store { }; } + async fetchWorkspaceSubscriptions( + workspaceId: string, + abortSignal?: AbortSignal + ) { + const data = await this.gqlService.gql({ + query: getWorkspaceSubscriptionQuery, + variables: { + workspaceId, + }, + context: { + signal: abortSignal, + }, + }); + + if (!data.workspace) { + throw new Error('No workspace'); + } + + return { + workspaceId: data.workspace.subscription?.id, + subscription: data.workspace.subscription, + }; + } + async mutateResumeSubscription( idempotencyKey: string, plan?: SubscriptionPlan, @@ -114,6 +143,22 @@ export class SubscriptionStore extends Store { return this.globalCache.set(SUBSCRIPTION_CACHE_KEY + userId, subscriptions); } + getCachedWorkspaceSubscription(workspaceId: string) { + return this.globalCache.get( + SUBSCRIPTION_CACHE_KEY + workspaceId + ); + } + + setCachedWorkspaceSubscription( + workspaceId: string, + subscription: SubscriptionType + ) { + return this.globalCache.set( + SUBSCRIPTION_CACHE_KEY + workspaceId, + subscription + ); + } + setSubscriptionRecurring( idempotencyKey: string, recurring: SubscriptionRecurring, diff --git a/packages/frontend/graphql/src/graphql/get-workspace-subscription.gql b/packages/frontend/graphql/src/graphql/get-workspace-subscription.gql new file mode 100644 index 0000000000..c438998af3 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/get-workspace-subscription.gql @@ -0,0 +1,15 @@ +query getWorkspaceSubscription($workspaceId: String!) { + workspace(id: $workspaceId) { + subscription { + id + status + plan + recurring + start + end + nextBillAt + canceledAt + variant + } + } +} diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index 6a9761a1b5..638cfd3846 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -706,6 +706,29 @@ query getWorkspacePublicPages($workspaceId: String!) { }`, }; +export const getWorkspaceSubscriptionQuery = { + id: 'getWorkspaceSubscriptionQuery' as const, + operationName: 'getWorkspaceSubscription', + definitionName: 'workspace', + containsFile: false, + query: ` +query getWorkspaceSubscription($workspaceId: String!) { + workspace(id: $workspaceId) { + subscription { + id + status + plan + recurring + start + end + nextBillAt + canceledAt + variant + } + } +}`, +}; + export const getWorkspaceQuery = { id: 'getWorkspaceQuery' as const, operationName: 'getWorkspace', @@ -1408,6 +1431,29 @@ mutation revokeInviteLink($workspaceId: String!) { }`, }; +export const workspaceInvoicesQuery = { + id: 'workspaceInvoicesQuery' as const, + operationName: 'workspaceInvoices', + definitionName: 'workspace', + containsFile: false, + query: ` +query workspaceInvoices($take: Int!, $skip: Int!, $workspaceId: String!) { + workspace(id: $workspaceId) { + invoiceCount + invoices(take: $take, skip: $skip) { + id + status + currency + amount + reason + lastPaymentError + link + createdAt + } + } +}`, +}; + export const workspaceQuotaQuery = { id: 'workspaceQuotaQuery' as const, operationName: 'workspaceQuota', diff --git a/packages/frontend/graphql/src/graphql/workspace-invoices.gql b/packages/frontend/graphql/src/graphql/workspace-invoices.gql new file mode 100644 index 0000000000..43e1bd6d41 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/workspace-invoices.gql @@ -0,0 +1,15 @@ +query workspaceInvoices($take: Int!, $skip: Int!, $workspaceId: String!) { + workspace(id: $workspaceId) { + invoiceCount + invoices(take: $take, skip: $skip) { + id + status + currency + amount + reason + lastPaymentError + link + createdAt + } + } +} diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 19e80086d2..4a7fd44c55 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -2014,6 +2014,29 @@ export type GetWorkspacePublicPagesQuery = { }; }; +export type GetWorkspaceSubscriptionQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; +}>; + +export type GetWorkspaceSubscriptionQuery = { + __typename?: 'Query'; + workspace: { + __typename?: 'WorkspaceType'; + subscription: { + __typename?: 'SubscriptionType'; + id: string | null; + status: SubscriptionStatus; + plan: SubscriptionPlan; + recurring: SubscriptionRecurring; + start: string; + end: string | null; + nextBillAt: string | null; + canceledAt: string | null; + variant: SubscriptionVariant | null; + } | null; + }; +}; + export type GetWorkspaceQueryVariables = Exact<{ id: Scalars['String']['input']; }>; @@ -2627,6 +2650,31 @@ export type RevokeInviteLinkMutation = { revokeInviteLink: boolean; }; +export type WorkspaceInvoicesQueryVariables = Exact<{ + take: Scalars['Int']['input']; + skip: Scalars['Int']['input']; + workspaceId: Scalars['String']['input']; +}>; + +export type WorkspaceInvoicesQuery = { + __typename?: 'Query'; + workspace: { + __typename?: 'WorkspaceType'; + invoiceCount: number; + invoices: Array<{ + __typename?: 'InvoiceType'; + id: string | null; + status: InvoiceStatus; + currency: string; + amount: number; + reason: string; + lastPaymentError: string | null; + link: string | null; + createdAt: string; + }>; + }; +}; + export type WorkspaceQuotaQueryVariables = Exact<{ id: Scalars['String']['input']; }>; @@ -2813,6 +2861,11 @@ export type Queries = variables: GetWorkspacePublicPagesQueryVariables; response: GetWorkspacePublicPagesQuery; } + | { + name: 'getWorkspaceSubscriptionQuery'; + variables: GetWorkspaceSubscriptionQueryVariables; + response: GetWorkspaceSubscriptionQuery; + } | { name: 'getWorkspaceQuery'; variables: GetWorkspaceQueryVariables; @@ -2883,6 +2936,11 @@ export type Queries = variables: ListWorkspaceFeaturesQueryVariables; response: ListWorkspaceFeaturesQuery; } + | { + name: 'workspaceInvoicesQuery'; + variables: WorkspaceInvoicesQueryVariables; + response: WorkspaceInvoicesQuery; + } | { name: 'workspaceQuotaQuery'; variables: WorkspaceQuotaQueryVariables;