feat: add idempotent request support for payment apis (#4753)

This commit is contained in:
DarkSky
2023-10-30 00:54:09 -05:00
committed by GitHub
parent 3798293d3e
commit de9e7f97a4
12 changed files with 244 additions and 138 deletions

View File

@@ -189,12 +189,14 @@ export class SubscriptionResolver {
async checkout( async checkout(
@CurrentUser() user: User, @CurrentUser() user: User,
@Args({ name: 'recurring', type: () => SubscriptionRecurring }) @Args({ name: 'recurring', type: () => SubscriptionRecurring })
recurring: SubscriptionRecurring recurring: SubscriptionRecurring,
@Args('idempotencyKey') idempotencyKey: string
) { ) {
const session = await this.service.createCheckoutSession({ const session = await this.service.createCheckoutSession({
user, user,
recurring, recurring,
redirectUrl: `${this.config.baseUrl}/upgrade-success`, redirectUrl: `${this.config.baseUrl}/upgrade-success`,
idempotencyKey,
}); });
if (!session.url) { if (!session.url) {
@@ -217,22 +219,33 @@ export class SubscriptionResolver {
} }
@Mutation(() => UserSubscriptionType) @Mutation(() => UserSubscriptionType)
async cancelSubscription(@CurrentUser() user: User) { async cancelSubscription(
return this.service.cancelSubscription(user.id); @CurrentUser() user: User,
@Args('idempotencyKey') idempotencyKey: string
) {
return this.service.cancelSubscription(idempotencyKey, user.id);
} }
@Mutation(() => UserSubscriptionType) @Mutation(() => UserSubscriptionType)
async resumeSubscription(@CurrentUser() user: User) { async resumeSubscription(
return this.service.resumeCanceledSubscription(user.id); @CurrentUser() user: User,
@Args('idempotencyKey') idempotencyKey: string
) {
return this.service.resumeCanceledSubscription(idempotencyKey, user.id);
} }
@Mutation(() => UserSubscriptionType) @Mutation(() => UserSubscriptionType)
async updateSubscriptionRecurring( async updateSubscriptionRecurring(
@CurrentUser() user: User, @CurrentUser() user: User,
@Args({ name: 'recurring', type: () => SubscriptionRecurring }) @Args({ name: 'recurring', type: () => SubscriptionRecurring })
recurring: SubscriptionRecurring recurring: SubscriptionRecurring,
@Args('idempotencyKey') idempotencyKey: string
) { ) {
return this.service.updateSubscriptionRecurring(user.id, recurring); return this.service.updateSubscriptionRecurring(
idempotencyKey,
user.id,
recurring
);
} }
} }

View File

@@ -1,9 +1,10 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import Stripe from 'stripe'; import Stripe from 'stripe';
@Injectable() @Injectable()
export class ScheduleManager { export class ScheduleManager {
private _schedule: Stripe.SubscriptionSchedule | null = null; private _schedule: Stripe.SubscriptionSchedule | null = null;
private readonly logger = new Logger(ScheduleManager.name);
constructor(private readonly stripe: Stripe) {} constructor(private readonly stripe: Stripe) {}
@@ -50,7 +51,10 @@ export class ScheduleManager {
if (typeof schedule === 'string') { if (typeof schedule === 'string') {
const s = await this.stripe.subscriptionSchedules const s = await this.stripe.subscriptionSchedules
.retrieve(schedule) .retrieve(schedule)
.catch(() => undefined); .catch(e => {
this.logger.error('Failed to retrieve subscription schedule', e);
return undefined;
});
return ScheduleManager.create(this.stripe, s); return ScheduleManager.create(this.stripe, s);
} else { } else {
@@ -58,7 +62,10 @@ export class ScheduleManager {
} }
} }
async fromSubscription(subscription: string | Stripe.Subscription) { async fromSubscription(
idempotencyKey: string,
subscription: string | Stripe.Subscription
) {
if (typeof subscription === 'string') { if (typeof subscription === 'string') {
subscription = await this.stripe.subscriptions.retrieve(subscription, { subscription = await this.stripe.subscriptions.retrieve(subscription, {
expand: ['schedule'], expand: ['schedule'],
@@ -68,9 +75,10 @@ export class ScheduleManager {
if (subscription.schedule) { if (subscription.schedule) {
return await this.fromSchedule(subscription.schedule); return await this.fromSchedule(subscription.schedule);
} else { } else {
const schedule = await this.stripe.subscriptionSchedules.create({ const schedule = await this.stripe.subscriptionSchedules.create(
from_subscription: subscription.id, { from_subscription: subscription.id },
}); { idempotencyKey }
);
return await this.fromSchedule(schedule); return await this.fromSchedule(schedule);
} }
@@ -80,7 +88,7 @@ export class ScheduleManager {
* Cancel a subscription by marking schedule's end behavior to `cancel`. * Cancel a subscription by marking schedule's end behavior to `cancel`.
* At the same time, the coming phase's price and coupon will be saved to metadata for later resuming to correction subscription. * At the same time, the coming phase's price and coupon will be saved to metadata for later resuming to correction subscription.
*/ */
async cancel() { async cancel(idempotencyKey: string) {
if (!this._schedule) { if (!this._schedule) {
throw new Error('No schedule'); throw new Error('No schedule');
} }
@@ -111,13 +119,17 @@ export class ScheduleManager {
}; };
} }
await this.stripe.subscriptionSchedules.update(this._schedule.id, { await this.stripe.subscriptionSchedules.update(
this._schedule.id,
{
phases: [phases], phases: [phases],
end_behavior: 'cancel', end_behavior: 'cancel',
}); },
{ idempotencyKey }
);
} }
async resume() { async resume(idempotencyKey: string) {
if (!this._schedule) { if (!this._schedule) {
throw new Error('No schedule'); throw new Error('No schedule');
} }
@@ -156,21 +168,27 @@ export class ScheduleManager {
}); });
} }
await this.stripe.subscriptionSchedules.update(this._schedule.id, { await this.stripe.subscriptionSchedules.update(
this._schedule.id,
{
phases: phases, phases: phases,
end_behavior: 'release', end_behavior: 'release',
}); },
{ idempotencyKey }
);
} }
async release() { async release(idempotencyKey: string) {
if (!this._schedule) { if (!this._schedule) {
throw new Error('No schedule'); throw new Error('No schedule');
} }
await this.stripe.subscriptionSchedules.release(this._schedule.id); await this.stripe.subscriptionSchedules.release(this._schedule.id, {
idempotencyKey,
});
} }
async update(price: string, coupon?: string) { async update(idempotencyKey: string, price: string, coupon?: string) {
if (!this._schedule) { if (!this._schedule) {
throw new Error('No schedule'); throw new Error('No schedule');
} }
@@ -184,10 +202,14 @@ export class ScheduleManager {
this.currentPhase.items[0].price === price && this.currentPhase.items[0].price === price &&
(!coupon || this.currentPhase.coupon === coupon) (!coupon || this.currentPhase.coupon === coupon)
) { ) {
await this.stripe.subscriptionSchedules.release(this._schedule.id); await this.stripe.subscriptionSchedules.release(this._schedule.id, {
idempotencyKey,
});
this._schedule = null; this._schedule = null;
} else { } else {
await this.stripe.subscriptionSchedules.update(this._schedule.id, { await this.stripe.subscriptionSchedules.update(
this._schedule.id,
{
phases: [ phases: [
{ {
items: [ items: [
@@ -208,7 +230,9 @@ export class ScheduleManager {
coupon, coupon,
}, },
], ],
}); },
{ idempotencyKey }
);
} }
} }
} }

View File

@@ -103,12 +103,14 @@ export class SubscriptionService {
user, user,
recurring, recurring,
redirectUrl, redirectUrl,
idempotencyKey,
plan = SubscriptionPlan.Pro, plan = SubscriptionPlan.Pro,
}: { }: {
user: User; user: User;
plan?: SubscriptionPlan; plan?: SubscriptionPlan;
recurring: SubscriptionRecurring; recurring: SubscriptionRecurring;
redirectUrl: string; redirectUrl: string;
idempotencyKey: string;
}) { }) {
const currentSubscription = await this.db.userSubscription.findUnique({ const currentSubscription = await this.db.userSubscription.findUnique({
where: { where: {
@@ -121,10 +123,11 @@ export class SubscriptionService {
} }
const price = await this.getPrice(plan, recurring); const price = await this.getPrice(plan, recurring);
const customer = await this.getOrCreateCustomer(user); const customer = await this.getOrCreateCustomer(idempotencyKey, user);
const coupon = await this.getAvailableCoupon(user, CouponType.EarlyAccess); const coupon = await this.getAvailableCoupon(user, CouponType.EarlyAccess);
return await this.stripe.checkout.sessions.create({ return await this.stripe.checkout.sessions.create(
{
line_items: [ line_items: [
{ {
price, price,
@@ -148,10 +151,15 @@ export class SubscriptionService {
address: 'auto', address: 'auto',
name: 'auto', name: 'auto',
}, },
}); },
{ idempotencyKey }
);
} }
async cancelSubscription(userId: string): Promise<UserSubscription> { async cancelSubscription(
idempotencyKey: string,
userId: string
): Promise<UserSubscription> {
const user = await this.db.user.findUnique({ const user = await this.db.user.findUnique({
where: { where: {
id: userId, id: userId,
@@ -174,7 +182,7 @@ export class SubscriptionService {
const manager = await this.scheduleManager.fromSchedule( const manager = await this.scheduleManager.fromSchedule(
user.subscription.stripeScheduleId user.subscription.stripeScheduleId
); );
await manager.cancel(); await manager.cancel(idempotencyKey);
return this.saveSubscription( return this.saveSubscription(
user, user,
await this.stripe.subscriptions.retrieve( await this.stripe.subscriptions.retrieve(
@@ -187,15 +195,17 @@ export class SubscriptionService {
// see https://stripe.com/docs/billing/subscriptions/cancel // see https://stripe.com/docs/billing/subscriptions/cancel
const subscription = await this.stripe.subscriptions.update( const subscription = await this.stripe.subscriptions.update(
user.subscription.stripeSubscriptionId, user.subscription.stripeSubscriptionId,
{ { cancel_at_period_end: true },
cancel_at_period_end: true, { idempotencyKey }
}
); );
return await this.saveSubscription(user, subscription); return await this.saveSubscription(user, subscription);
} }
} }
async resumeCanceledSubscription(userId: string): Promise<UserSubscription> { async resumeCanceledSubscription(
idempotencyKey: string,
userId: string
): Promise<UserSubscription> {
const user = await this.db.user.findUnique({ const user = await this.db.user.findUnique({
where: { where: {
id: userId, id: userId,
@@ -221,7 +231,7 @@ export class SubscriptionService {
const manager = await this.scheduleManager.fromSchedule( const manager = await this.scheduleManager.fromSchedule(
user.subscription.stripeScheduleId user.subscription.stripeScheduleId
); );
await manager.resume(); await manager.resume(idempotencyKey);
return this.saveSubscription( return this.saveSubscription(
user, user,
await this.stripe.subscriptions.retrieve( await this.stripe.subscriptions.retrieve(
@@ -232,9 +242,8 @@ export class SubscriptionService {
} else { } else {
const subscription = await this.stripe.subscriptions.update( const subscription = await this.stripe.subscriptions.update(
user.subscription.stripeSubscriptionId, user.subscription.stripeSubscriptionId,
{ { cancel_at_period_end: false },
cancel_at_period_end: false, { idempotencyKey }
}
); );
return await this.saveSubscription(user, subscription); return await this.saveSubscription(user, subscription);
@@ -242,6 +251,7 @@ export class SubscriptionService {
} }
async updateSubscriptionRecurring( async updateSubscriptionRecurring(
idempotencyKey: string,
userId: string, userId: string,
recurring: SubscriptionRecurring recurring: SubscriptionRecurring
): Promise<UserSubscription> { ): Promise<UserSubscription> {
@@ -272,10 +282,12 @@ export class SubscriptionService {
); );
const manager = await this.scheduleManager.fromSubscription( const manager = await this.scheduleManager.fromSubscription(
idempotencyKey,
user.subscription.stripeSubscriptionId user.subscription.stripeSubscriptionId
); );
await manager.update( await manager.update(
idempotencyKey,
price, price,
// if user is early access user, use early access coupon // if user is early access user, use early access coupon
manager.currentPhase?.coupon === CouponType.EarlyAccess || manager.currentPhase?.coupon === CouponType.EarlyAccess ||
@@ -355,10 +367,16 @@ export class SubscriptionService {
// deal with early access user // deal with early access user
if (stripeInvoice.discount?.coupon.id === CouponType.EarlyAccess) { if (stripeInvoice.discount?.coupon.id === CouponType.EarlyAccess) {
const idempotencyKey = stripeInvoice.id + '_earlyaccess';
const manager = await this.scheduleManager.fromSubscription( const manager = await this.scheduleManager.fromSubscription(
idempotencyKey,
line.subscription as string line.subscription as string
); );
await manager.update(line.price.id, CouponType.EarlyAccessRenew); await manager.update(
idempotencyKey,
line.price.id,
CouponType.EarlyAccessRenew
);
} }
} }
@@ -530,7 +548,10 @@ export class SubscriptionService {
} }
} }
private async getOrCreateCustomer(user: User): Promise<UserStripeCustomer> { private async getOrCreateCustomer(
idempotencyKey: string,
user: User
): Promise<UserStripeCustomer> {
const customer = await this.db.userStripeCustomer.findUnique({ const customer = await this.db.userStripeCustomer.findUnique({
where: { where: {
userId: user.id, userId: user.id,
@@ -550,9 +571,10 @@ export class SubscriptionService {
if (stripeCustomersList.data.length) { if (stripeCustomersList.data.length) {
stripeCustomer = stripeCustomersList.data[0]; stripeCustomer = stripeCustomersList.data[0];
} else { } else {
stripeCustomer = await this.stripe.customers.create({ stripeCustomer = await this.stripe.customers.create(
email: user.email, { email: user.email },
}); { idempotencyKey }
);
} }
return await this.db.userStripeCustomer.create({ return await this.db.userStripeCustomer.create({

View File

@@ -278,13 +278,13 @@ type Mutation {
addToNewFeaturesWaitingList(type: NewFeaturesKind!, email: String!): AddToNewFeaturesWaitingList! addToNewFeaturesWaitingList(type: NewFeaturesKind!, email: String!): AddToNewFeaturesWaitingList!
"""Create a subscription checkout link of stripe""" """Create a subscription checkout link of stripe"""
checkout(recurring: SubscriptionRecurring!): String! checkout(recurring: SubscriptionRecurring!, idempotencyKey: String!): String!
"""Create a stripe customer portal to manage payment methods""" """Create a stripe customer portal to manage payment methods"""
createCustomerPortal: String! createCustomerPortal: String!
cancelSubscription: UserSubscription! cancelSubscription(idempotencyKey: String!): UserSubscription!
resumeSubscription: UserSubscription! resumeSubscription(idempotencyKey: String!): UserSubscription!
updateSubscriptionRecurring(recurring: SubscriptionRecurring!): UserSubscription! updateSubscriptionRecurring(recurring: SubscriptionRecurring!, idempotencyKey: String!): UserSubscription!
} }
"""The `Upload` scalar type represents a file upload.""" """The `Upload` scalar type represents a file upload."""

View File

@@ -21,6 +21,7 @@ import { useMutation, useQuery } from '@affine/workspace/affine/gql';
import { ArrowRightSmallIcon } from '@blocksuite/icons'; import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { Button, IconButton } from '@toeverything/components/button'; import { Button, IconButton } from '@toeverything/components/button';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { nanoid } from 'nanoid';
import { Suspense, useCallback, useMemo, useState } from 'react'; import { Suspense, useCallback, useMemo, useState } from 'react';
import { openSettingModalAtom } from '../../../../../atoms'; import { openSettingModalAtom } from '../../../../../atoms';
@@ -89,13 +90,19 @@ const SubscriptionSettings = () => {
}); });
const [openCancelModal, setOpenCancelModal] = useState(false); const [openCancelModal, setOpenCancelModal] = useState(false);
// allow replay request on network error until component unmount
const idempotencyKey = useMemo(() => nanoid(), []);
const cancel = useCallback(() => { const cancel = useCallback(() => {
trigger(null, { trigger(
{ idempotencyKey },
{
onSuccess: data => { onSuccess: data => {
mutateSubscription(data.cancelSubscription); mutateSubscription(data.cancelSubscription);
}, },
}); }
}, [trigger, mutateSubscription]); );
}, [trigger, idempotencyKey, mutateSubscription]);
const { data: pricesQueryResult } = useQuery({ const { data: pricesQueryResult } = useQuery({
query: pricesQuery, query: pricesQuery,
@@ -288,13 +295,19 @@ const ResumeSubscription = ({
mutation: resumeSubscriptionMutation, mutation: resumeSubscriptionMutation,
}); });
// allow replay request on network error until component unmount
const idempotencyKey = useMemo(() => nanoid(), []);
const resume = useCallback(() => { const resume = useCallback(() => {
trigger(null, { trigger(
{ idempotencyKey },
{
onSuccess: data => { onSuccess: data => {
onSubscriptionUpdate(data.resumeSubscription); onSubscriptionUpdate(data.resumeSubscription);
}, },
}); }
}, [trigger, onSubscriptionUpdate]); );
}, [trigger, idempotencyKey, onSubscriptionUpdate]);
return ( return (
<Button <Button

View File

@@ -18,10 +18,12 @@ import { Button } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip'; import { Tooltip } from '@toeverything/components/tooltip';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { nanoid } from 'nanoid';
import { import {
type PropsWithChildren, type PropsWithChildren,
useCallback, useCallback,
useEffect, useEffect,
useMemo,
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
@@ -303,13 +305,19 @@ const Downgrade = ({
mutation: cancelSubscriptionMutation, mutation: cancelSubscriptionMutation,
}); });
// allow replay request on network error until component unmount
const idempotencyKey = useMemo(() => nanoid(), []);
const downgrade = useCallback(() => { const downgrade = useCallback(() => {
trigger(null, { trigger(
{ idempotencyKey },
{
onSuccess: data => { onSuccess: data => {
onSubscriptionUpdate(data.cancelSubscription); onSubscriptionUpdate(data.cancelSubscription);
}, },
}); }
}, [trigger, onSubscriptionUpdate]); );
}, [trigger, idempotencyKey, onSubscriptionUpdate]);
const tooltipContent = disabled const tooltipContent = disabled
? t['com.affine.payment.downgraded-tooltip']() ? t['com.affine.payment.downgraded-tooltip']()
@@ -365,6 +373,9 @@ const Upgrade = ({
const newTabRef = useRef<Window | null>(null); const newTabRef = useRef<Window | null>(null);
// allow replay request on network error until component unmount
const idempotencyKey = useMemo(() => nanoid(), []);
const onClose = useCallback(() => { const onClose = useCallback(() => {
newTabRef.current = null; newTabRef.current = null;
onSubscriptionUpdate(); onSubscriptionUpdate();
@@ -381,7 +392,7 @@ const Upgrade = ({
newTabRef.current.focus(); newTabRef.current.focus();
} else { } else {
trigger( trigger(
{ recurring }, { recurring, idempotencyKey },
{ {
onSuccess: data => { onSuccess: data => {
// FIXME: safari prevents from opening new tab by window api // FIXME: safari prevents from opening new tab by window api
@@ -401,7 +412,7 @@ const Upgrade = ({
} }
); );
} }
}, [trigger, recurring, onClose, openPaymentDisableModal]); }, [openPaymentDisableModal, trigger, recurring, idempotencyKey, onClose]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -446,16 +457,19 @@ const ChangeRecurring = ({
mutation: updateSubscriptionMutation, mutation: updateSubscriptionMutation,
}); });
// allow replay request on network error until component unmount
const idempotencyKey = useMemo(() => nanoid(), []);
const change = useCallback(() => { const change = useCallback(() => {
trigger( trigger(
{ recurring: to }, { recurring: to, idempotencyKey },
{ {
onSuccess: data => { onSuccess: data => {
onSubscriptionUpdate(data.updateSubscriptionRecurring); onSubscriptionUpdate(data.updateSubscriptionRecurring);
}, },
} }
); );
}, [trigger, onSubscriptionUpdate, to]); }, [trigger, to, idempotencyKey, onSubscriptionUpdate]);
const changeCurringContent = ( const changeCurringContent = (
<Trans values={{ from, to, due }} className={styles.downgradeContent}> <Trans values={{ from, to, due }} className={styles.downgradeContent}>
@@ -523,13 +537,19 @@ const ResumeAction = ({
mutation: resumeSubscriptionMutation, mutation: resumeSubscriptionMutation,
}); });
// allow replay request on network error until component unmount
const idempotencyKey = useMemo(() => nanoid(), []);
const resume = useCallback(() => { const resume = useCallback(() => {
trigger(null, { trigger(
{ idempotencyKey },
{
onSuccess: data => { onSuccess: data => {
onSubscriptionUpdate(data.resumeSubscription); onSubscriptionUpdate(data.resumeSubscription);
}, },
}); }
}, [trigger, onSubscriptionUpdate]); );
}, [trigger, idempotencyKey, onSubscriptionUpdate]);
return ( return (
<> <>

View File

@@ -1,5 +1,5 @@
mutation cancelSubscription { mutation cancelSubscription($idempotencyKey: String!) {
cancelSubscription { cancelSubscription(idempotencyKey: $idempotencyKey) {
id id
status status
nextBillAt nextBillAt

View File

@@ -1,3 +1,6 @@
mutation checkout($recurring: SubscriptionRecurring!) { mutation checkout(
checkout(recurring: $recurring) $recurring: SubscriptionRecurring!
$idempotencyKey: String!
) {
checkout(recurring: $recurring, idempotencyKey: $idempotencyKey)
} }

View File

@@ -85,8 +85,8 @@ export const cancelSubscriptionMutation = {
definitionName: 'cancelSubscription', definitionName: 'cancelSubscription',
containsFile: false, containsFile: false,
query: ` query: `
mutation cancelSubscription { mutation cancelSubscription($idempotencyKey: String!) {
cancelSubscription { cancelSubscription(idempotencyKey: $idempotencyKey) {
id id
status status
nextBillAt nextBillAt
@@ -133,8 +133,8 @@ export const checkoutMutation = {
definitionName: 'checkout', definitionName: 'checkout',
containsFile: false, containsFile: false,
query: ` query: `
mutation checkout($recurring: SubscriptionRecurring!) { mutation checkout($recurring: SubscriptionRecurring!, $idempotencyKey: String!) {
checkout(recurring: $recurring) checkout(recurring: $recurring, idempotencyKey: $idempotencyKey)
}`, }`,
}; };
@@ -434,8 +434,8 @@ export const resumeSubscriptionMutation = {
definitionName: 'resumeSubscription', definitionName: 'resumeSubscription',
containsFile: false, containsFile: false,
query: ` query: `
mutation resumeSubscription { mutation resumeSubscription($idempotencyKey: String!) {
resumeSubscription { resumeSubscription(idempotencyKey: $idempotencyKey) {
id id
status status
nextBillAt nextBillAt
@@ -593,8 +593,11 @@ export const updateSubscriptionMutation = {
definitionName: 'updateSubscriptionRecurring', definitionName: 'updateSubscriptionRecurring',
containsFile: false, containsFile: false,
query: ` query: `
mutation updateSubscription($recurring: SubscriptionRecurring!) { mutation updateSubscription($recurring: SubscriptionRecurring!, $idempotencyKey: String!) {
updateSubscriptionRecurring(recurring: $recurring) { updateSubscriptionRecurring(
recurring: $recurring
idempotencyKey: $idempotencyKey
) {
id id
plan plan
recurring recurring

View File

@@ -1,5 +1,5 @@
mutation resumeSubscription { mutation resumeSubscription($idempotencyKey: String!) {
resumeSubscription { resumeSubscription(idempotencyKey: $idempotencyKey) {
id id
status status
nextBillAt nextBillAt

View File

@@ -1,5 +1,11 @@
mutation updateSubscription($recurring: SubscriptionRecurring!) { mutation updateSubscription(
updateSubscriptionRecurring(recurring: $recurring) { $recurring: SubscriptionRecurring!
$idempotencyKey: String!
) {
updateSubscriptionRecurring(
recurring: $recurring
idempotencyKey: $idempotencyKey
) {
id id
plan plan
recurring recurring

View File

@@ -131,7 +131,7 @@ export type AllBlobSizesQuery = {
}; };
export type CancelSubscriptionMutationVariables = Exact<{ export type CancelSubscriptionMutationVariables = Exact<{
[key: string]: never; idempotencyKey: Scalars['String']['input'];
}>; }>;
export type CancelSubscriptionMutation = { export type CancelSubscriptionMutation = {
@@ -178,6 +178,7 @@ export type ChangePasswordMutation = {
export type CheckoutMutationVariables = Exact<{ export type CheckoutMutationVariables = Exact<{
recurring: SubscriptionRecurring; recurring: SubscriptionRecurring;
idempotencyKey: Scalars['String']['input'];
}>; }>;
export type CheckoutMutation = { __typename?: 'Mutation'; checkout: string }; export type CheckoutMutation = { __typename?: 'Mutation'; checkout: string };
@@ -416,7 +417,7 @@ export type RemoveAvatarMutation = {
}; };
export type ResumeSubscriptionMutationVariables = Exact<{ export type ResumeSubscriptionMutationVariables = Exact<{
[key: string]: never; idempotencyKey: Scalars['String']['input'];
}>; }>;
export type ResumeSubscriptionMutation = { export type ResumeSubscriptionMutation = {
@@ -558,6 +559,7 @@ export type SubscriptionQuery = {
export type UpdateSubscriptionMutationVariables = Exact<{ export type UpdateSubscriptionMutationVariables = Exact<{
recurring: SubscriptionRecurring; recurring: SubscriptionRecurring;
idempotencyKey: Scalars['String']['input'];
}>; }>;
export type UpdateSubscriptionMutation = { export type UpdateSubscriptionMutation = {