mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-24 06:02:51 +03:00
feat(core): impl billing settings (#4652)
This commit is contained in:
parent
1d62133f4f
commit
858a1da35f
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "user_invoices" ADD COLUMN "link" TEXT;
|
@ -224,6 +224,8 @@ model UserInvoice {
|
||||
// billing reason
|
||||
reason String @db.VarChar
|
||||
lastPaymentError String? @map("last_payment_error") @db.Text
|
||||
// stripe hosted invoice link
|
||||
link String? @db.Text
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
|
@ -117,6 +117,9 @@ class UserInvoiceType implements Partial<UserInvoice> {
|
||||
@Field(() => String, { nullable: true })
|
||||
lastPaymentError?: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
link?: string | null;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt!: Date;
|
||||
|
||||
@ -183,6 +186,13 @@ export class SubscriptionResolver {
|
||||
return session.url;
|
||||
}
|
||||
|
||||
@Mutation(() => String, {
|
||||
description: 'Create a stripe customer portal to manage payment methods',
|
||||
})
|
||||
async createCustomerPortal(@CurrentUser() user: User) {
|
||||
return this.service.createCustomerPortal(user.id);
|
||||
}
|
||||
|
||||
@Mutation(() => UserSubscriptionType)
|
||||
async cancelSubscription(@CurrentUser() user: User) {
|
||||
return this.service.cancelSubscription(user.id);
|
||||
|
@ -296,6 +296,29 @@ export class SubscriptionService {
|
||||
}
|
||||
}
|
||||
|
||||
async createCustomerPortal(id: string) {
|
||||
const user = await this.db.userStripeCustomer.findUnique({
|
||||
where: {
|
||||
userId: id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Unknown user');
|
||||
}
|
||||
|
||||
try {
|
||||
const portal = await this.stripe.billingPortal.sessions.create({
|
||||
customer: user.stripeCustomerId,
|
||||
});
|
||||
|
||||
return portal.url;
|
||||
} catch (e) {
|
||||
this.logger.error('Failed to create customer portal.', e);
|
||||
throw new Error('Failed to create customer portal');
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent('customer.subscription.created')
|
||||
@OnEvent('customer.subscription.updated')
|
||||
async onSubscriptionChanges(subscription: Stripe.Subscription) {
|
||||
@ -519,6 +542,7 @@ export class SubscriptionService {
|
||||
currency: stripeInvoice.currency,
|
||||
amount: stripeInvoice.total,
|
||||
status: stripeInvoice.status ?? InvoiceStatus.Void,
|
||||
link: stripeInvoice.hosted_invoice_url,
|
||||
};
|
||||
|
||||
// handle payment error
|
||||
|
@ -112,6 +112,7 @@ type UserInvoice {
|
||||
status: InvoiceStatus!
|
||||
reason: String!
|
||||
lastPaymentError: String
|
||||
link: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
@ -278,6 +279,9 @@ type Mutation {
|
||||
|
||||
"""Create a subscription checkout link of stripe"""
|
||||
checkout(recurring: SubscriptionRecurring!): String!
|
||||
|
||||
"""Create a stripe customer portal to manage payment methods"""
|
||||
createCustomerPortal: String!
|
||||
cancelSubscription: UserSubscription!
|
||||
resumeSubscription: UserSubscription!
|
||||
updateSubscriptionRecurring(recurring: SubscriptionRecurring!): UserSubscription!
|
||||
|
@ -11,6 +11,7 @@ export type SettingRowProps = PropsWithChildren<{
|
||||
spreadCol?: boolean;
|
||||
'data-testid'?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export const SettingRow = ({
|
||||
@ -21,14 +22,19 @@ export const SettingRow = ({
|
||||
style,
|
||||
spreadCol = true,
|
||||
disabled = false,
|
||||
className,
|
||||
...props
|
||||
}: PropsWithChildren<SettingRowProps>) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(settingRow, {
|
||||
'two-col': spreadCol,
|
||||
disabled,
|
||||
})}
|
||||
className={clsx(
|
||||
settingRow,
|
||||
{
|
||||
'two-col': spreadCol,
|
||||
disabled,
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
data-testid={props['data-testid']}
|
||||
|
@ -86,7 +86,6 @@ globalStyle(`${settingRow} .desc`, {
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
globalStyle(`${settingRow} .right-col`, {
|
||||
width: '250px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
paddingLeft: '15px',
|
||||
|
@ -0,0 +1,272 @@
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import {
|
||||
cancelSubscriptionMutation,
|
||||
createCustomerPortalMutation,
|
||||
type InvoicesQuery,
|
||||
invoicesQuery,
|
||||
InvoiceStatus,
|
||||
pricesQuery,
|
||||
resumeSubscriptionMutation,
|
||||
SubscriptionPlan,
|
||||
subscriptionQuery,
|
||||
SubscriptionRecurring,
|
||||
SubscriptionStatus,
|
||||
} from '@affine/graphql';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Suspense, useCallback, useEffect } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../../../atoms';
|
||||
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
|
||||
import * as styles from './style.css';
|
||||
|
||||
export const BillingSettings = () => {
|
||||
const status = useCurrentLoginStatus();
|
||||
|
||||
if (status !== 'authenticated') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title="Billing"
|
||||
subtitle="Manage your billing information and invoices."
|
||||
/>
|
||||
{/* TODO: loading fallback */}
|
||||
<Suspense>
|
||||
<SettingWrapper title="information">
|
||||
<SubscriptionSettings />
|
||||
</SettingWrapper>
|
||||
</Suspense>
|
||||
{/* TODO: loading fallback */}
|
||||
<Suspense>
|
||||
<SettingWrapper title="Billing history">
|
||||
<BillingHistory />
|
||||
</SettingWrapper>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SubscriptionSettings = () => {
|
||||
const { data: subscriptionQueryResult } = useQuery({
|
||||
query: subscriptionQuery,
|
||||
});
|
||||
const { data: pricesQueryResult } = useQuery({
|
||||
query: pricesQuery,
|
||||
});
|
||||
|
||||
const subscription = subscriptionQueryResult.currentUser?.subscription;
|
||||
const plan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
const recurring = subscription?.recurring ?? SubscriptionRecurring.Monthly;
|
||||
|
||||
const price = pricesQueryResult.prices.find(price => price.plan === plan);
|
||||
const amount =
|
||||
plan === SubscriptionPlan.Free
|
||||
? '0'
|
||||
: price
|
||||
? recurring === SubscriptionRecurring.Monthly
|
||||
? String(price.amount / 100)
|
||||
: (price.yearlyAmount / 100 / 12).toFixed(2)
|
||||
: '?';
|
||||
|
||||
return (
|
||||
<div className={styles.subscription}>
|
||||
<div className={styles.planCard}>
|
||||
<div className={styles.currentPlan}>
|
||||
<SettingRow
|
||||
spreadCol={false}
|
||||
name="Current Plan"
|
||||
desc={
|
||||
<p>
|
||||
You are current on the{' '}
|
||||
<a>
|
||||
{/* TODO: Action */}
|
||||
{plan} plan
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<PlanAction plan={plan} />
|
||||
</div>
|
||||
<p className={styles.planPrice}>${amount}/month</p>
|
||||
</div>
|
||||
{subscription?.status === SubscriptionStatus.Active && (
|
||||
<>
|
||||
<SettingRow
|
||||
className={styles.paymentMethod}
|
||||
name="Payment Method"
|
||||
desc="Provided by Stripe."
|
||||
>
|
||||
<PaymentMethodUpdater />
|
||||
</SettingRow>
|
||||
{subscription.nextBillAt && (
|
||||
<SettingRow
|
||||
name="Renew Date"
|
||||
desc={`Next billing date: ${new Date(
|
||||
subscription.nextBillAt
|
||||
).toLocaleDateString()}`}
|
||||
/>
|
||||
)}
|
||||
{subscription.canceledAt ? (
|
||||
<SettingRow
|
||||
name="Expiration Date"
|
||||
desc={`Your subscription is valid until ${new Date(
|
||||
subscription.end
|
||||
).toLocaleDateString()}`}
|
||||
>
|
||||
<ResumeSubscription />
|
||||
</SettingRow>
|
||||
) : (
|
||||
<SettingRow
|
||||
className="dangerous-setting"
|
||||
name="Cancel Subscription"
|
||||
desc={`Subscription cancelled, your pro account will expire on ${new Date(
|
||||
subscription.end
|
||||
).toLocaleDateString()}`}
|
||||
>
|
||||
<CancelSubscription />
|
||||
</SettingRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PlanAction = ({ plan }: { plan: string }) => {
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const gotoPlansSetting = useCallback(() => {
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
workspaceId: null,
|
||||
});
|
||||
}, [setOpenSettingModalAtom]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.planAction}
|
||||
type="primary"
|
||||
onClick={gotoPlansSetting}
|
||||
>
|
||||
{plan === SubscriptionPlan.Free ? 'Upgrade' : 'Change Plan'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const PaymentMethodUpdater = () => {
|
||||
// TODO: open stripe customer portal
|
||||
const { isMutating, trigger, data } = useMutation({
|
||||
mutation: createCustomerPortalMutation,
|
||||
});
|
||||
|
||||
const update = useCallback(() => {
|
||||
trigger();
|
||||
}, [trigger]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.createCustomerPortal) {
|
||||
window.open(data.createCustomerPortal, '_blank', 'noopener noreferrer');
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Button onClick={update} loading={isMutating} disabled={isMutating}>
|
||||
Update
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const ResumeSubscription = () => {
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: resumeSubscriptionMutation,
|
||||
});
|
||||
|
||||
const resume = useCallback(() => {
|
||||
trigger();
|
||||
}, [trigger]);
|
||||
|
||||
return (
|
||||
<Button onClick={resume} loading={isMutating} disabled={isMutating}>
|
||||
Resume
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const CancelSubscription = () => {
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: cancelSubscriptionMutation,
|
||||
});
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
trigger();
|
||||
}, [trigger]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={<ArrowRightSmallIcon />}
|
||||
disabled={isMutating}
|
||||
loading={isMutating}
|
||||
onClick={cancel /* TODO: popup confirmation modal instead */}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const BillingHistory = () => {
|
||||
const { data: invoicesQueryResult } = useQuery({
|
||||
query: invoicesQuery,
|
||||
variables: {
|
||||
skip: 0,
|
||||
take: 12,
|
||||
},
|
||||
});
|
||||
|
||||
const invoices = invoicesQueryResult.currentUser?.invoices ?? [];
|
||||
|
||||
return (
|
||||
<div className={styles.billingHistory}>
|
||||
{invoices.length === 0 ? (
|
||||
<p className={styles.noInvoice}>There are no invoices to display.</p>
|
||||
) : (
|
||||
// TODO: pagination
|
||||
invoices.map(invoice => (
|
||||
<InvoiceLine key={invoice.id} invoice={invoice} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InvoiceLine = ({
|
||||
invoice,
|
||||
}: {
|
||||
invoice: NonNullable<InvoicesQuery['currentUser']>['invoices'][0];
|
||||
}) => {
|
||||
const open = useCallback(() => {
|
||||
if (invoice.link) {
|
||||
window.open(invoice.link, '_blank', 'noopener noreferrer');
|
||||
}
|
||||
}, [invoice.link]);
|
||||
return (
|
||||
<SettingRow
|
||||
key={invoice.id}
|
||||
name={new Date(invoice.createdAt).toLocaleDateString()}
|
||||
// TODO: currency to format: usd => $, cny => ¥
|
||||
desc={`${invoice.status === InvoiceStatus.Paid ? 'Paid' : ''} $${
|
||||
invoice.amount / 100
|
||||
}`}
|
||||
>
|
||||
<Button onClick={open}>View Invoice</Button>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const subscription = style({});
|
||||
|
||||
export const billingHistory = style({});
|
||||
|
||||
export const planCard = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
borderRadius: '8px',
|
||||
});
|
||||
|
||||
export const currentPlan = style({
|
||||
flex: '1 0 0',
|
||||
});
|
||||
|
||||
export const planAction = style({
|
||||
marginTop: '8px',
|
||||
});
|
||||
|
||||
export const planPrice = style({
|
||||
fontSize: 'var(--affine-font-h-6)',
|
||||
fontWeight: 600,
|
||||
});
|
||||
|
||||
export const paymentMethod = style({
|
||||
marginTop: '24px',
|
||||
});
|
||||
|
||||
globalStyle('.dangerous-setting .name', {
|
||||
color: 'var(--affine-error-color)',
|
||||
});
|
||||
|
||||
export const noInvoice = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
});
|
@ -7,8 +7,10 @@ import {
|
||||
} from '@blocksuite/icons';
|
||||
import type { ReactElement, SVGProps } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
|
||||
import { AboutAffine } from './about';
|
||||
import { AppearanceSettings } from './appearance';
|
||||
import { BillingSettings } from './billing';
|
||||
import { AFFiNECloudPlans } from './plans';
|
||||
import { Plugins } from './plugins';
|
||||
import { Shortcuts } from './shortcuts';
|
||||
@ -32,8 +34,9 @@ export type GeneralSettingList = GeneralSettingListItem[];
|
||||
|
||||
export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
const t = useAFFiNEI18N();
|
||||
const status = useCurrentLoginStatus();
|
||||
|
||||
return [
|
||||
const settings: GeneralSettingListItem[] = [
|
||||
{
|
||||
key: 'appearance',
|
||||
title: t['com.affine.settings.appearance'](),
|
||||
@ -54,14 +57,7 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
icon: KeyboardIcon,
|
||||
testId: 'plans-panel-trigger',
|
||||
},
|
||||
{
|
||||
key: 'billing',
|
||||
// TODO: i18n
|
||||
title: 'Billing',
|
||||
// TODO: icon
|
||||
icon: KeyboardIcon,
|
||||
testId: 'billing-panel-trigger',
|
||||
},
|
||||
|
||||
{
|
||||
key: 'plugins',
|
||||
title: 'Plugins',
|
||||
@ -75,6 +71,19 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
testId: 'about-panel-trigger',
|
||||
},
|
||||
];
|
||||
|
||||
if (status === 'authenticated') {
|
||||
settings.splice(3, 0, {
|
||||
key: 'billing',
|
||||
// TODO: i18n
|
||||
title: 'Billing',
|
||||
// TODO: icon
|
||||
icon: KeyboardIcon,
|
||||
testId: 'billing-panel-trigger',
|
||||
});
|
||||
}
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
interface GeneralSettingProps {
|
||||
@ -93,6 +102,8 @@ export const GeneralSetting = ({ generalKey }: GeneralSettingProps) => {
|
||||
return <AboutAffine />;
|
||||
case 'plans':
|
||||
return <AFFiNECloudPlans />;
|
||||
case 'billing':
|
||||
return <BillingSettings />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -129,12 +129,11 @@ const Settings = () => {
|
||||
const subscription = data.currentUser?.subscription;
|
||||
|
||||
const [recurring, setRecurring] = useState<string>(
|
||||
subscription?.recurring ?? SubscriptionRecurring.Monthly
|
||||
subscription?.recurring ?? SubscriptionRecurring.Yearly
|
||||
);
|
||||
|
||||
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
const currentRecurring =
|
||||
subscription?.recurring ?? SubscriptionRecurring.Monthly;
|
||||
const currentRecurring = subscription?.recurring;
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
mutate();
|
||||
@ -178,8 +177,6 @@ const Settings = () => {
|
||||
{/* TODO: may scroll current plan into view when first loading? */}
|
||||
<div className={styles.planCardsWrapper}>
|
||||
{Array.from(planDetail.values()).map(detail => {
|
||||
const isCurrent =
|
||||
currentPlan === detail.plan && currentRecurring === recurring;
|
||||
return (
|
||||
<div
|
||||
key={detail.plan}
|
||||
@ -192,11 +189,12 @@ const Settings = () => {
|
||||
<div className={styles.planTitle}>
|
||||
<p>
|
||||
{detail.plan}{' '}
|
||||
{'discount' in detail && (
|
||||
<span className={styles.discountLabel}>
|
||||
{detail.discount}% off
|
||||
</span>
|
||||
)}
|
||||
{'discount' in detail &&
|
||||
recurring === SubscriptionRecurring.Yearly && (
|
||||
<span className={styles.discountLabel}>
|
||||
{detail.discount}% off
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<span className={styles.planPrice}>
|
||||
@ -224,18 +222,26 @@ const Settings = () => {
|
||||
detail.type === 'dynamic' ? (
|
||||
<ContactSales />
|
||||
) : loggedIn ? (
|
||||
isCurrent ? (
|
||||
detail.plan === currentPlan &&
|
||||
(currentRecurring === recurring ||
|
||||
(!currentRecurring &&
|
||||
detail.plan === SubscriptionPlan.Free)) ? (
|
||||
<CurrentPlan />
|
||||
) : detail.plan === SubscriptionPlan.Free ? (
|
||||
<Downgrade onActionDone={refresh} />
|
||||
) : currentRecurring !== recurring ? (
|
||||
) : currentRecurring !== recurring &&
|
||||
currentPlan === detail.plan ? (
|
||||
<ChangeRecurring
|
||||
// @ts-expect-error must exist
|
||||
from={currentRecurring}
|
||||
to={recurring as SubscriptionRecurring}
|
||||
onActionDone={refresh}
|
||||
/>
|
||||
) : (
|
||||
<Upgrade recurring={recurring} onActionDone={refresh} />
|
||||
<Upgrade
|
||||
recurring={recurring as SubscriptionRecurring}
|
||||
onActionDone={refresh}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<SignupAction>
|
||||
|
@ -0,0 +1,3 @@
|
||||
mutation createCustomerPortal {
|
||||
createCustomerPortal
|
||||
}
|
@ -138,6 +138,17 @@ mutation checkout($recurring: SubscriptionRecurring!) {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const createCustomerPortalMutation = {
|
||||
id: 'createCustomerPortalMutation' as const,
|
||||
operationName: 'createCustomerPortal',
|
||||
definitionName: 'createCustomerPortal',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation createCustomerPortal {
|
||||
createCustomerPortal
|
||||
}`,
|
||||
};
|
||||
|
||||
export const createWorkspaceMutation = {
|
||||
id: 'createWorkspaceMutation' as const,
|
||||
operationName: 'createWorkspace',
|
||||
@ -365,6 +376,7 @@ query invoices($take: Int!, $skip: Int!) {
|
||||
amount
|
||||
reason
|
||||
lastPaymentError
|
||||
link
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
@ -416,6 +428,23 @@ mutation removeAvatar {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const resumeSubscriptionMutation = {
|
||||
id: 'resumeSubscriptionMutation' as const,
|
||||
operationName: 'resumeSubscription',
|
||||
definitionName: 'resumeSubscription',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation resumeSubscription {
|
||||
resumeSubscription {
|
||||
id
|
||||
status
|
||||
nextBillAt
|
||||
start
|
||||
end
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const revokeMemberPermissionMutation = {
|
||||
id: 'revokeMemberPermissionMutation' as const,
|
||||
operationName: 'revokeMemberPermission',
|
||||
|
@ -9,6 +9,7 @@ query invoices($take: Int!, $skip: Int!) {
|
||||
amount
|
||||
reason
|
||||
lastPaymentError
|
||||
link
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
mutation resumeSubscription {
|
||||
resumeSubscription {
|
||||
id
|
||||
status
|
||||
nextBillAt
|
||||
start
|
||||
end
|
||||
}
|
||||
}
|
@ -182,6 +182,15 @@ export type CheckoutMutationVariables = Exact<{
|
||||
|
||||
export type CheckoutMutation = { __typename?: 'Mutation'; checkout: string };
|
||||
|
||||
export type CreateCustomerPortalMutationVariables = Exact<{
|
||||
[key: string]: never;
|
||||
}>;
|
||||
|
||||
export type CreateCustomerPortalMutation = {
|
||||
__typename?: 'Mutation';
|
||||
createCustomerPortal: string;
|
||||
};
|
||||
|
||||
export type CreateWorkspaceMutationVariables = Exact<{
|
||||
init: Scalars['Upload']['input'];
|
||||
}>;
|
||||
@ -368,6 +377,7 @@ export type InvoicesQuery = {
|
||||
amount: number;
|
||||
reason: string;
|
||||
lastPaymentError: string | null;
|
||||
link: string | null;
|
||||
createdAt: string;
|
||||
}>;
|
||||
} | null;
|
||||
@ -405,6 +415,22 @@ export type RemoveAvatarMutation = {
|
||||
removeAvatar: { __typename?: 'RemoveAvatar'; success: boolean };
|
||||
};
|
||||
|
||||
export type ResumeSubscriptionMutationVariables = Exact<{
|
||||
[key: string]: never;
|
||||
}>;
|
||||
|
||||
export type ResumeSubscriptionMutation = {
|
||||
__typename?: 'Mutation';
|
||||
resumeSubscription: {
|
||||
__typename?: 'UserSubscription';
|
||||
id: string;
|
||||
status: SubscriptionStatus;
|
||||
nextBillAt: string | null;
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type RevokeMemberPermissionMutationVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
userId: Scalars['String']['input'];
|
||||
@ -712,6 +738,11 @@ export type Mutations =
|
||||
variables: CheckoutMutationVariables;
|
||||
response: CheckoutMutation;
|
||||
}
|
||||
| {
|
||||
name: 'createCustomerPortalMutation';
|
||||
variables: CreateCustomerPortalMutationVariables;
|
||||
response: CreateCustomerPortalMutation;
|
||||
}
|
||||
| {
|
||||
name: 'createWorkspaceMutation';
|
||||
variables: CreateWorkspaceMutationVariables;
|
||||
@ -737,6 +768,11 @@ export type Mutations =
|
||||
variables: RemoveAvatarMutationVariables;
|
||||
response: RemoveAvatarMutation;
|
||||
}
|
||||
| {
|
||||
name: 'resumeSubscriptionMutation';
|
||||
variables: ResumeSubscriptionMutationVariables;
|
||||
response: ResumeSubscriptionMutation;
|
||||
}
|
||||
| {
|
||||
name: 'revokeMemberPermissionMutation';
|
||||
variables: RevokeMemberPermissionMutationVariables;
|
||||
|
Loading…
Reference in New Issue
Block a user