From 1d62133f4ffab448c7ac0d16eaa4188959dd2ad6 Mon Sep 17 00:00:00 2001 From: forehalo Date: Thu, 19 Oct 2023 10:08:16 +0800 Subject: [PATCH] feat(core): impl subscription plans setting --- .../setting-modal/general-setting/index.tsx | 23 +- .../general-setting/plans/index.tsx | 417 ++++++++++++++++++ .../general-setting/plans/style.css.ts | 104 +++++ .../src/graphql/cancel-subscription.gql | 8 + .../src/graphql/create-checkout-link.gql | 3 + .../frontend/graphql/src/graphql/index.ts | 105 +++++ .../frontend/graphql/src/graphql/invoices.gql | 15 + .../frontend/graphql/src/graphql/prices.gql | 9 + .../graphql/src/graphql/subscription.gql | 14 + .../graphql/update-subscription-billing.gql | 8 + packages/frontend/graphql/src/schema.ts | 124 ++++++ 11 files changed, 829 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx create mode 100644 packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts create mode 100644 packages/frontend/graphql/src/graphql/cancel-subscription.gql create mode 100644 packages/frontend/graphql/src/graphql/create-checkout-link.gql create mode 100644 packages/frontend/graphql/src/graphql/invoices.gql create mode 100644 packages/frontend/graphql/src/graphql/prices.gql create mode 100644 packages/frontend/graphql/src/graphql/subscription.gql create mode 100644 packages/frontend/graphql/src/graphql/update-subscription-billing.gql diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx index a57f442f1d..9a1c329fd4 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx @@ -9,6 +9,7 @@ import type { ReactElement, SVGProps } from 'react'; import { AboutAffine } from './about'; import { AppearanceSettings } from './appearance'; +import { AFFiNECloudPlans } from './plans'; import { Plugins } from './plugins'; import { Shortcuts } from './shortcuts'; @@ -16,7 +17,9 @@ export type GeneralSettingKeys = | 'shortcuts' | 'appearance' | 'plugins' - | 'about'; + | 'about' + | 'plans' + | 'billing'; interface GeneralSettingListItem { key: GeneralSettingKeys; @@ -43,6 +46,22 @@ export const useGeneralSettingList = (): GeneralSettingList => { icon: KeyboardIcon, testId: 'shortcuts-panel-trigger', }, + { + key: 'plans', + // TODO: i18n + title: 'AFFiNE Cloud Plans', + // TODO: icon + icon: KeyboardIcon, + testId: 'plans-panel-trigger', + }, + { + key: 'billing', + // TODO: i18n + title: 'Billing', + // TODO: icon + icon: KeyboardIcon, + testId: 'billing-panel-trigger', + }, { key: 'plugins', title: 'Plugins', @@ -72,6 +91,8 @@ export const GeneralSetting = ({ generalKey }: GeneralSettingProps) => { return ; case 'about': return ; + case 'plans': + return ; default: return null; } diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx new file mode 100644 index 0000000000..1c79de89f2 --- /dev/null +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx @@ -0,0 +1,417 @@ +import { RadioButton, RadioButtonGroup } from '@affine/component'; +import { SettingHeader } from '@affine/component/setting-components'; +import { + cancelSubscriptionMutation, + checkoutMutation, + pricesQuery, + SubscriptionPlan, + subscriptionQuery, + SubscriptionRecurring, + updateSubscriptionMutation, +} from '@affine/graphql'; +import { useMutation, useQuery } from '@affine/workspace/affine/gql'; +import { Button } from '@toeverything/components/button'; +import { + type PropsWithChildren, + Suspense, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; + +import * as styles from './style.css'; + +interface FixedPrice { + type: 'fixed'; + plan: SubscriptionPlan; + price: string; + yearlyPrice: string; + discount?: string; + benefits: string[]; +} + +interface DynamicPrice { + type: 'dynamic'; + plan: SubscriptionPlan; + contact: boolean; + benefits: string[]; +} + +// TODO: i18n all things +const planDetail = new Map([ + [ + SubscriptionPlan.Free, + { + type: 'fixed', + plan: SubscriptionPlan.Free, + price: '0', + yearlyPrice: '0', + benefits: [ + 'Unlimited local workspace', + 'Unlimited login devices', + 'Unlimited blocks', + 'AFFiNE Cloud Storage 10GB', + 'The maximum file size is 10M', + 'Number of members per Workspace ≤ 3', + ], + }, + ], + [ + SubscriptionPlan.Pro, + { + type: 'fixed', + plan: SubscriptionPlan.Pro, + price: '1', + yearlyPrice: '1', + benefits: [ + 'Unlimited local workspace', + 'Unlimited login devices', + 'Unlimited blocks', + 'AFFiNE Cloud Storage 100GB', + 'The maximum file size is 500M', + 'Number of members per Workspace ≤ 10', + ], + }, + ], + [ + SubscriptionPlan.Team, + { + type: 'dynamic', + plan: SubscriptionPlan.Team, + contact: true, + benefits: [ + 'Best team workspace for collaboration and knowledge distilling.', + 'Focusing on what really matters with team project management and automation.', + 'Pay for seats, fits all team size.', + ], + }, + ], + [ + SubscriptionPlan.Enterprise, + { + type: 'dynamic', + plan: SubscriptionPlan.Enterprise, + contact: true, + benefits: [ + 'Solutions & best practices for dedicated needs.', + 'Embedable & interrogations with IT support.', + ], + }, + ], +]); + +const Settings = () => { + const { data, mutate } = useQuery({ + query: subscriptionQuery, + }); + + const { + data: { prices }, + } = useQuery({ + query: pricesQuery, + }); + + prices.forEach(price => { + const detail = planDetail.get(price.plan); + + if (detail?.type === 'fixed') { + detail.price = (price.amount / 100).toFixed(2); + detail.yearlyPrice = (price.yearlyAmount / 100 / 12).toFixed(2); + detail.discount = ( + (1 - price.yearlyAmount / 12 / price.amount) * + 100 + ).toFixed(2); + } + }); + + const loggedIn = !!data.currentUser; + const subscription = data.currentUser?.subscription; + + const [recurring, setRecurring] = useState( + subscription?.recurring ?? SubscriptionRecurring.Monthly + ); + + const currentPlan = subscription?.plan ?? SubscriptionPlan.Free; + const currentRecurring = + subscription?.recurring ?? SubscriptionRecurring.Monthly; + + const refresh = useCallback(() => { + mutate(); + }, [mutate]); + + const yearlyDiscount = ( + planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined + )?.discount; + + return ( + <> + + You are current on the {currentPlan} plan. If you have any + questions, please contact our{' '} + {/*TODO: add action*/}customer support. +

+ } + /> +
+ + {Object.values(SubscriptionRecurring).map(plan => ( + + {plan} + {plan === SubscriptionRecurring.Yearly && yearlyDiscount && ( + + {yearlyDiscount}% off + + )} + + ))} + + {/* TODO: plan cards horizontal scroll behavior is not the same as design */} + {/* TODO: may scroll current plan into view when first loading? */} +
+ {Array.from(planDetail.values()).map(detail => { + const isCurrent = + currentPlan === detail.plan && currentRecurring === recurring; + return ( +
+
+

+ {detail.plan}{' '} + {'discount' in detail && ( + + {detail.discount}% off + + )} +

+

+ + $ + {detail.type === 'dynamic' + ? '?' + : recurring === SubscriptionRecurring.Monthly + ? detail.price + : detail.yearlyPrice} + + per month +

+ { + // branches: + // if contact => 'Contact Sales' + // if not signed in: + // if free => 'Sign up free' + // else => 'Buy Pro' + // else + // if isCurrent => 'Current Plan' + // else if free => 'Downgrade' + // else if currentRecurring !== recurring => 'Change to {recurring} Billing' + // else => 'Upgrade' + // TODO: should replace with components with proper actions + detail.type === 'dynamic' ? ( + + ) : loggedIn ? ( + isCurrent ? ( + + ) : detail.plan === SubscriptionPlan.Free ? ( + + ) : currentRecurring !== recurring ? ( + + ) : ( + + ) + ) : ( + + {detail.plan === SubscriptionPlan.Free + ? 'Sign up free' + : 'Buy Pro'} + + ) + } +
+
+ {detail.benefits.map((content, i) => ( +
+
+ {/* TODO: icons */} + {detail.type == 'dynamic' ? '·' : '✅'} +
+ {content} +
+ ))} +
+
+ ); + })} +
+ + See all plans →{/* TODO: icon */} + +
+ + ); +}; + +const Downgrade = ({ onActionDone }: { onActionDone: () => void }) => { + const { isMutating, trigger } = useMutation({ + mutation: cancelSubscriptionMutation, + }); + + const downgrade = useCallback(() => { + trigger(null, { onSuccess: onActionDone }); + }, [trigger, onActionDone]); + + return ( + + ); +}; + +const Upgrade = ({ + recurring, + onActionDone, +}: { + recurring: SubscriptionRecurring; + onActionDone: () => void; +}) => { + const { isMutating, trigger, data } = useMutation({ + mutation: checkoutMutation, + }); + + const upgrade = useCallback(() => { + trigger({ recurring }); + }, [trigger, recurring]); + + const newTabRef = useRef(null); + + useEffect(() => { + if (data?.checkout) { + if (newTabRef.current) { + newTabRef.current.focus(); + } else { + // FIXME: safari prevents from opening new tab by window api + // TODO(@xp): what if electron? + const newTab = window.open( + data.checkout, + '_blank', + 'noopener noreferrer' + ); + + if (newTab) { + newTabRef.current = newTab; + const update = () => { + onActionDone(); + }; + newTab.addEventListener('close', update); + + return () => newTab.removeEventListener('close', update); + } + } + } + + return; + }, [data?.checkout, onActionDone]); + + return ( + + ); +}; + +const ChangeRecurring = ({ + from: _from /* TODO: from can be useful when showing confirmation modal */, + to, + onActionDone, +}: { + from: SubscriptionRecurring; + to: SubscriptionRecurring; + onActionDone: () => void; +}) => { + const { isMutating, trigger } = useMutation({ + mutation: updateSubscriptionMutation, + }); + + const change = useCallback(() => { + trigger({ recurring: to }, { onSuccess: onActionDone }); + }, [trigger, onActionDone, to]); + + return ( + + ); +}; + +const ContactSales = () => { + return ( + // TODO: add action + + ); +}; + +const CurrentPlan = () => { + return ; +}; + +const SignupAction = ({ children }: PropsWithChildren) => { + // TODO: add login action + return ( + + ); +}; + +export const AFFiNECloudPlans = () => { + return ( + // TODO: loading skeleton + // TODO: Error Boundary + + + + ); +}; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts new file mode 100644 index 0000000000..d12bb790eb --- /dev/null +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts @@ -0,0 +1,104 @@ +import { style } from '@vanilla-extract/css'; + +export const wrapper = style({ + width: '100%', +}); +export const recurringRadioGroup = style({ + width: '256px', +}); + +export const radioButtonDiscount = style({ + marginLeft: '4px', + color: 'var(--affine-primary-color)', +}); + +export const planCardsWrapper = style({ + marginTop: '24px', + display: 'flex', + overflowX: 'auto', +}); + +export const planCard = style({ + minHeight: '426px', + minWidth: '258px', + borderRadius: '16px', + padding: '20px', + border: '1px solid var(--affine-border-color)', + + selectors: { + '&:not(:last-child)': { + marginRight: '16px', + }, + }, +}); + +export const currentPlanCard = style([ + planCard, + { + borderWidth: '2px', + borderColor: 'var(--affine-primary-color)', + }, +]); + +export const discountLabel = style({ + color: 'var(--affine-primary-color)', + marginLeft: '8px', + lineHeight: '20px', + fontSize: 'var(--affine-font-xs)', + fontWeight: 500, + padding: '0 4px', + backgroundColor: 'var(--affine-blue-50)', + borderRadius: '4px', + display: 'inline-block', + height: '100%', +}); + +export const planTitle = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '10px', + fontWeight: 600, +}); + +export const planPrice = style({ + fontSize: 'var(--affine-font-h-5)', + marginRight: '8px', +}); + +export const planPriceDesc = style({ + color: 'var(--affine-text-secondary-color)', + fontSize: 'var(--affine-font-sm)', +}); + +export const planAction = style({ + width: '100%', +}); + +export const planBenefits = style({ + marginTop: '20px', + fontSize: 'var(--affine-font-xs)', +}); + +export const planBenefit = style({ + display: 'flex', + selectors: { + '&:not(:last-child)': { + marginBottom: '8px', + }, + }, +}); + +export const planBenefitIcon = style({ + display: 'inline-block', + marginRight: '8px', +}); + +export const allPlansLink = style({ + display: 'block', + marginTop: '36px', + color: 'var(--affine-primary-color)', + background: 'transparent', + borderColor: 'transparent', + fontSize: 'var(--affine-font-xs)', +}); diff --git a/packages/frontend/graphql/src/graphql/cancel-subscription.gql b/packages/frontend/graphql/src/graphql/cancel-subscription.gql new file mode 100644 index 0000000000..128e206781 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/cancel-subscription.gql @@ -0,0 +1,8 @@ +mutation cancelSubscription { + cancelSubscription { + id + status + nextBillAt + canceledAt + } +} diff --git a/packages/frontend/graphql/src/graphql/create-checkout-link.gql b/packages/frontend/graphql/src/graphql/create-checkout-link.gql new file mode 100644 index 0000000000..b4e4704e97 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/create-checkout-link.gql @@ -0,0 +1,3 @@ +mutation checkout($recurring: SubscriptionRecurring!) { + checkout(recurring: $recurring) +} diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index f31b3c5c8c..29330c8fed 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -79,6 +79,22 @@ query allBlobSizes { }`, }; +export const cancelSubscriptionMutation = { + id: 'cancelSubscriptionMutation' as const, + operationName: 'cancelSubscription', + definitionName: 'cancelSubscription', + containsFile: false, + query: ` +mutation cancelSubscription { + cancelSubscription { + id + status + nextBillAt + canceledAt + } +}`, +}; + export const changeEmailMutation = { id: 'changeEmailMutation' as const, operationName: 'changeEmail', @@ -111,6 +127,17 @@ mutation changePassword($token: String!, $newPassword: String!) { }`, }; +export const checkoutMutation = { + id: 'checkoutMutation' as const, + operationName: 'checkout', + definitionName: 'checkout', + containsFile: false, + query: ` +mutation checkout($recurring: SubscriptionRecurring!) { + checkout(recurring: $recurring) +}`, +}; + export const createWorkspaceMutation = { id: 'createWorkspaceMutation' as const, operationName: 'createWorkspace', @@ -321,6 +348,29 @@ query getWorkspaces { }`, }; +export const invoicesQuery = { + id: 'invoicesQuery' as const, + operationName: 'invoices', + definitionName: 'currentUser', + containsFile: false, + query: ` +query invoices($take: Int!, $skip: Int!) { + currentUser { + invoices(take: $take, skip: $skip) { + id + status + plan + recurring + currency + amount + reason + lastPaymentError + createdAt + } + } +}`, +}; + export const leaveWorkspaceMutation = { id: 'leaveWorkspaceMutation' as const, operationName: 'leaveWorkspace', @@ -336,6 +386,23 @@ mutation leaveWorkspace($workspaceId: String!, $workspaceName: String!, $sendLea }`, }; +export const pricesQuery = { + id: 'pricesQuery' as const, + operationName: 'prices', + definitionName: 'prices', + containsFile: false, + query: ` +query prices { + prices { + type + plan + currency + amount + yearlyAmount + } +}`, +}; + export const removeAvatarMutation = { id: 'removeAvatarMutation' as const, operationName: 'removeAvatar', @@ -469,6 +536,44 @@ mutation signUp($name: String!, $email: String!, $password: String!) { }`, }; +export const subscriptionQuery = { + id: 'subscriptionQuery' as const, + operationName: 'subscription', + definitionName: 'currentUser', + containsFile: false, + query: ` +query subscription { + currentUser { + subscription { + id + status + plan + recurring + start + end + nextBillAt + canceledAt + } + } +}`, +}; + +export const updateSubscriptionMutation = { + id: 'updateSubscriptionMutation' as const, + operationName: 'updateSubscription', + definitionName: 'updateSubscriptionRecurring', + containsFile: false, + query: ` +mutation updateSubscription($recurring: SubscriptionRecurring!) { + updateSubscriptionRecurring(recurring: $recurring) { + id + plan + recurring + nextBillAt + } +}`, +}; + export const uploadAvatarMutation = { id: 'uploadAvatarMutation' as const, operationName: 'uploadAvatar', diff --git a/packages/frontend/graphql/src/graphql/invoices.gql b/packages/frontend/graphql/src/graphql/invoices.gql new file mode 100644 index 0000000000..569b77730f --- /dev/null +++ b/packages/frontend/graphql/src/graphql/invoices.gql @@ -0,0 +1,15 @@ +query invoices($take: Int!, $skip: Int!) { + currentUser { + invoices(take: $take, skip: $skip) { + id + status + plan + recurring + currency + amount + reason + lastPaymentError + createdAt + } + } +} diff --git a/packages/frontend/graphql/src/graphql/prices.gql b/packages/frontend/graphql/src/graphql/prices.gql new file mode 100644 index 0000000000..eb4a75e7bd --- /dev/null +++ b/packages/frontend/graphql/src/graphql/prices.gql @@ -0,0 +1,9 @@ +query prices { + prices { + type + plan + currency + amount + yearlyAmount + } +} diff --git a/packages/frontend/graphql/src/graphql/subscription.gql b/packages/frontend/graphql/src/graphql/subscription.gql new file mode 100644 index 0000000000..0072350f1f --- /dev/null +++ b/packages/frontend/graphql/src/graphql/subscription.gql @@ -0,0 +1,14 @@ +query subscription { + currentUser { + subscription { + id + status + plan + recurring + start + end + nextBillAt + canceledAt + } + } +} diff --git a/packages/frontend/graphql/src/graphql/update-subscription-billing.gql b/packages/frontend/graphql/src/graphql/update-subscription-billing.gql new file mode 100644 index 0000000000..1464d399d6 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/update-subscription-billing.gql @@ -0,0 +1,8 @@ +mutation updateSubscription($recurring: SubscriptionRecurring!) { + updateSubscriptionRecurring(recurring: $recurring) { + id + plan + recurring + nextBillAt + } +} diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 3a84e42fe4..e38e9b760c 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -130,6 +130,21 @@ export type AllBlobSizesQuery = { collectAllBlobSizes: { __typename?: 'WorkspaceBlobSizes'; size: number }; }; +export type CancelSubscriptionMutationVariables = Exact<{ + [key: string]: never; +}>; + +export type CancelSubscriptionMutation = { + __typename?: 'Mutation'; + cancelSubscription: { + __typename?: 'UserSubscription'; + id: string; + status: SubscriptionStatus; + nextBillAt: string | null; + canceledAt: string | null; + }; +}; + export type ChangeEmailMutationVariables = Exact<{ token: Scalars['String']['input']; }>; @@ -161,6 +176,12 @@ export type ChangePasswordMutation = { }; }; +export type CheckoutMutationVariables = Exact<{ + recurring: SubscriptionRecurring; +}>; + +export type CheckoutMutation = { __typename?: 'Mutation'; checkout: string }; + export type CreateWorkspaceMutationVariables = Exact<{ init: Scalars['Upload']['input']; }>; @@ -328,6 +349,30 @@ export type GetWorkspacesQuery = { workspaces: Array<{ __typename?: 'WorkspaceType'; id: string }>; }; +export type InvoicesQueryVariables = Exact<{ + take: Scalars['Int']['input']; + skip: Scalars['Int']['input']; +}>; + +export type InvoicesQuery = { + __typename?: 'Query'; + currentUser: { + __typename?: 'UserType'; + invoices: Array<{ + __typename?: 'UserInvoice'; + id: string; + status: InvoiceStatus; + plan: SubscriptionPlan; + recurring: SubscriptionRecurring; + currency: string; + amount: number; + reason: string; + lastPaymentError: string | null; + createdAt: string; + }>; + } | null; +}; + export type LeaveWorkspaceMutationVariables = Exact<{ workspaceId: Scalars['String']['input']; workspaceName: Scalars['String']['input']; @@ -339,6 +384,20 @@ export type LeaveWorkspaceMutation = { leaveWorkspace: boolean; }; +export type PricesQueryVariables = Exact<{ [key: string]: never }>; + +export type PricesQuery = { + __typename?: 'Query'; + prices: Array<{ + __typename?: 'SubscriptionPrice'; + type: string; + plan: SubscriptionPlan; + currency: string; + amount: number; + yearlyAmount: number; + }>; +}; + export type RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>; export type RemoveAvatarMutation = { @@ -451,6 +510,41 @@ export type SignUpMutation = { }; }; +export type SubscriptionQueryVariables = Exact<{ [key: string]: never }>; + +export type SubscriptionQuery = { + __typename?: 'Query'; + currentUser: { + __typename?: 'UserType'; + subscription: { + __typename?: 'UserSubscription'; + id: string; + status: SubscriptionStatus; + plan: SubscriptionPlan; + recurring: SubscriptionRecurring; + start: string; + end: string; + nextBillAt: string | null; + canceledAt: string | null; + } | null; + } | null; +}; + +export type UpdateSubscriptionMutationVariables = Exact<{ + recurring: SubscriptionRecurring; +}>; + +export type UpdateSubscriptionMutation = { + __typename?: 'Mutation'; + updateSubscriptionRecurring: { + __typename?: 'UserSubscription'; + id: string; + plan: SubscriptionPlan; + recurring: SubscriptionRecurring; + nextBillAt: string | null; + }; +}; + export type UploadAvatarMutationVariables = Exact<{ avatar: Scalars['Upload']['input']; }>; @@ -570,6 +664,21 @@ export type Queries = name: 'getWorkspacesQuery'; variables: GetWorkspacesQueryVariables; response: GetWorkspacesQuery; + } + | { + name: 'invoicesQuery'; + variables: InvoicesQueryVariables; + response: InvoicesQuery; + } + | { + name: 'pricesQuery'; + variables: PricesQueryVariables; + response: PricesQuery; + } + | { + name: 'subscriptionQuery'; + variables: SubscriptionQueryVariables; + response: SubscriptionQuery; }; export type Mutations = @@ -583,6 +692,11 @@ export type Mutations = variables: SetBlobMutationVariables; response: SetBlobMutation; } + | { + name: 'cancelSubscriptionMutation'; + variables: CancelSubscriptionMutationVariables; + response: CancelSubscriptionMutation; + } | { name: 'changeEmailMutation'; variables: ChangeEmailMutationVariables; @@ -593,6 +707,11 @@ export type Mutations = variables: ChangePasswordMutationVariables; response: ChangePasswordMutation; } + | { + name: 'checkoutMutation'; + variables: CheckoutMutationVariables; + response: CheckoutMutation; + } | { name: 'createWorkspaceMutation'; variables: CreateWorkspaceMutationVariables; @@ -668,6 +787,11 @@ export type Mutations = variables: SignUpMutationVariables; response: SignUpMutation; } + | { + name: 'updateSubscriptionMutation'; + variables: UpdateSubscriptionMutationVariables; + response: UpdateSubscriptionMutation; + } | { name: 'uploadAvatarMutation'; variables: UploadAvatarMutationVariables;