feat: add idempotent request support for payment apis (#4753)
This commit is contained in:
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mutation cancelSubscription {
|
mutation cancelSubscription($idempotencyKey: String!) {
|
||||||
cancelSubscription {
|
cancelSubscription(idempotencyKey: $idempotencyKey) {
|
||||||
id
|
id
|
||||||
status
|
status
|
||||||
nextBillAt
|
nextBillAt
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
mutation checkout($recurring: SubscriptionRecurring!) {
|
mutation checkout(
|
||||||
checkout(recurring: $recurring)
|
$recurring: SubscriptionRecurring!
|
||||||
|
$idempotencyKey: String!
|
||||||
|
) {
|
||||||
|
checkout(recurring: $recurring, idempotencyKey: $idempotencyKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mutation resumeSubscription {
|
mutation resumeSubscription($idempotencyKey: String!) {
|
||||||
resumeSubscription {
|
resumeSubscription(idempotencyKey: $idempotencyKey) {
|
||||||
id
|
id
|
||||||
status
|
status
|
||||||
nextBillAt
|
nextBillAt
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user