diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx index c698d7767..bca216a52 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx @@ -5,14 +5,12 @@ import { SettingWrapper, } from '@affine/component/setting-components'; import { - cancelSubscriptionMutation, createCustomerPortalMutation, getInvoicesCountQuery, type InvoicesQuery, invoicesQuery, InvoiceStatus, pricesQuery, - resumeSubscriptionMutation, SubscriptionPlan, SubscriptionRecurring, SubscriptionStatus, @@ -25,7 +23,6 @@ import { Skeleton } from '@mui/material'; import { Button, IconButton } from '@toeverything/components/button'; import { Loading } from '@toeverything/components/loading'; import { useSetAtom } from 'jotai'; -import { nanoid } from 'nanoid'; import { Suspense, useCallback, useMemo, useState } from 'react'; import { openSettingModalAtom } from '../../../../../atoms'; @@ -34,7 +31,7 @@ import { type SubscriptionMutator, useUserSubscription, } from '../../../../../hooks/use-subscription'; -import { DowngradeModal } from '../plans/modals'; +import { CancelAction, ResumeAction } from '../plans/actions'; import * as styles from './style.css'; enum DescriptionI18NKey { @@ -89,25 +86,8 @@ export const BillingSettings = () => { const SubscriptionSettings = () => { const [subscription, mutateSubscription] = useUserSubscription(); - const { isMutating, trigger } = useMutation({ - mutation: cancelSubscriptionMutation, - }); const [openCancelModal, setOpenCancelModal] = useState(false); - // allow replay request on network error until component unmount - const idempotencyKey = useMemo(() => nanoid(), []); - - const cancel = useCallback(() => { - trigger( - { idempotencyKey }, - { - onSuccess: data => { - mutateSubscription(data.cancelSubscription); - }, - } - ); - }, [trigger, idempotencyKey, mutateSubscription]); - const { data: pricesQueryResult } = useQuery({ query: pricesQuery, }); @@ -213,27 +193,28 @@ const SubscriptionSettings = () => { ) : ( - (isMutating ? null : setOpenCancelModal(true))} - className="dangerous-setting" - name={t[ - 'com.affine.payment.billing-setting.cancel-subscription' - ]()} - desc={t[ - 'com.affine.payment.billing-setting.cancel-subscription.description' - ]({ - cancelDate: new Date(subscription.end).toLocaleDateString(), - })} + - - + setOpenCancelModal(true)} + className="dangerous-setting" + name={t[ + 'com.affine.payment.billing-setting.cancel-subscription' + ]()} + desc={t[ + 'com.affine.payment.billing-setting.cancel-subscription.description' + ]({ + cancelDate: new Date(subscription.end).toLocaleDateString(), + })} + > + + + )} - (isMutating ? null : cancel())} - onOpenChange={setOpenCancelModal} - /> > )} @@ -295,33 +276,18 @@ const ResumeSubscription = ({ onSubscriptionUpdate: SubscriptionMutator; }) => { const t = useAFFiNEI18N(); - const { isMutating, trigger } = useMutation({ - mutation: resumeSubscriptionMutation, - }); - - // allow replay request on network error until component unmount - const idempotencyKey = useMemo(() => nanoid(), []); - - const resume = useCallback(() => { - trigger( - { idempotencyKey }, - { - onSuccess: data => { - onSubscriptionUpdate(data.resumeSubscription); - }, - } - ); - }, [trigger, idempotencyKey, onSubscriptionUpdate]); + const [open, setOpen] = useState(false); return ( - - {t['com.affine.payment.billing-setting.resume-subscription']()} - + setOpen(true)}> + {t['com.affine.payment.billing-setting.resume-subscription']()} + + ); }; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx new file mode 100644 index 000000000..7bf7c0cc7 --- /dev/null +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/actions.tsx @@ -0,0 +1,107 @@ +import type { SubscriptionMutator } from '@affine/core/hooks/use-subscription'; +import { + cancelSubscriptionMutation, + resumeSubscriptionMutation, +} from '@affine/graphql'; +import { useMutation } from '@affine/workspace/affine/gql'; +import { nanoid } from 'nanoid'; +import type { PropsWithChildren } from 'react'; +import { useCallback, useState } from 'react'; + +import { ConfirmLoadingModal, DowngradeModal } from './modals'; + +/** + * Cancel action with modal & request + * @param param0 + * @returns + */ +export const CancelAction = ({ + children, + open, + onOpenChange, + onSubscriptionUpdate, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubscriptionUpdate: SubscriptionMutator; +} & PropsWithChildren) => { + const [idempotencyKey, setIdempotencyKey] = useState(nanoid()); + const { trigger, isMutating } = useMutation({ + mutation: cancelSubscriptionMutation, + }); + + const downgrade = useCallback(() => { + trigger( + { idempotencyKey }, + { + onSuccess: data => { + // refresh idempotency key + setIdempotencyKey(nanoid()); + onSubscriptionUpdate(data.cancelSubscription); + onOpenChange(false); + }, + } + ); + }, [trigger, idempotencyKey, onSubscriptionUpdate, onOpenChange]); + + return ( + <> + {children} + + > + ); +}; + +/** + * Resume payment action with modal & request + * @param param0 + * @returns + */ +export const ResumeAction = ({ + children, + open, + onOpenChange, + onSubscriptionUpdate, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubscriptionUpdate: SubscriptionMutator; +} & PropsWithChildren) => { + // allow replay request on network error until component unmount or success + const [idempotencyKey, setIdempotencyKey] = useState(nanoid()); + const { isMutating, trigger } = useMutation({ + mutation: resumeSubscriptionMutation, + }); + + const resume = useCallback(() => { + trigger( + { idempotencyKey }, + { + onSuccess: data => { + // refresh idempotency key + setIdempotencyKey(nanoid()); + onSubscriptionUpdate(data.resumeSubscription); + onOpenChange(false); + }, + } + ); + }, [trigger, idempotencyKey, onSubscriptionUpdate, onOpenChange]); + + return ( + <> + {children} + + > + ); +}; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx index cdd208228..cd3531ab0 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx @@ -72,6 +72,7 @@ export const ConfirmLoadingModal = ({ */ export const DowngradeModal = ({ open, + loading, onOpenChange, onCancel, }: { @@ -81,6 +82,14 @@ export const DowngradeModal = ({ onCancel?: () => void; }) => { const t = useAFFiNEI18N(); + const canceled = useRef(false); + + useEffect(() => { + if (!loading && open && canceled.current) { + onOpenChange?.(false); + canceled.current = false; + } + }, [loading, open, onOpenChange]); return ( { - onOpenChange?.(false); + canceled.current = true; onCancel?.(); }} + loading={loading} > {t['com.affine.payment.modal.downgrade.cancel']()} - onOpenChange?.(false)} type="primary"> + onOpenChange?.(false)} + type="primary" + > {t['com.affine.payment.modal.downgrade.confirm']()} diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx index fdf7d0049..fe868a44a 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx @@ -3,9 +3,7 @@ import type { SubscriptionMutator, } from '@affine/core/hooks/use-subscription'; import { - cancelSubscriptionMutation, checkoutMutation, - resumeSubscriptionMutation, SubscriptionPlan, SubscriptionRecurring, updateSubscriptionMutation, @@ -31,8 +29,9 @@ import { import { openPaymentDisableAtom } from '../../../../../atoms'; import { authAtom } from '../../../../../atoms/index'; import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status'; +import { CancelAction, ResumeAction } from './actions'; import { BulledListIcon } from './icons/bulled-list'; -import { ConfirmLoadingModal, DowngradeModal } from './modals'; +import { ConfirmLoadingModal } from './modals'; import * as styles from './style.css'; export interface FixedPrice { @@ -130,14 +129,8 @@ export const PlanCard = (props: PlanCardProps) => { const { detail, subscription, recurring } = props; const loggedIn = useCurrentLoginStatus() === 'authenticated'; const currentPlan = subscription?.plan ?? SubscriptionPlan.Free; - const currentRecurring = subscription?.recurring; - const isCurrent = - loggedIn && - detail.plan === currentPlan && - (currentPlan === SubscriptionPlan.Free - ? true - : currentRecurring === recurring); + const isCurrent = loggedIn && detail.plan === currentPlan; return ( + ) : ( ); @@ -301,46 +294,30 @@ const Downgrade = ({ }) => { const t = useAFFiNEI18N(); const [open, setOpen] = useState(false); - // allow replay request on network error until component unmount or success - const [idempotencyKey, setIdempotencyKey] = useState(nanoid()); - const { isMutating, trigger } = useMutation({ - mutation: cancelSubscriptionMutation, - }); - - const downgrade = useCallback(() => { - trigger( - { idempotencyKey }, - { - onSuccess: data => { - // refresh idempotency key - setIdempotencyKey(nanoid()); - onSubscriptionUpdate(data.cancelSubscription); - }, - } - ); - }, [trigger, idempotencyKey, onSubscriptionUpdate]); const tooltipContent = disabled ? t['com.affine.payment.downgraded-tooltip']() : null; return ( - <> + setOpen(true)} - disabled={disabled || isMutating} - loading={isMutating} + disabled={disabled} > {t['com.affine.payment.downgrade']()} - - > + ); }; @@ -527,55 +504,31 @@ const SignUpAction = ({ children }: PropsWithChildren) => { ); }; -const ResumeAction = ({ +const ResumeButton = ({ onSubscriptionUpdate, }: { onSubscriptionUpdate: SubscriptionMutator; }) => { const t = useAFFiNEI18N(); const [open, setOpen] = useState(false); - // allow replay request on network error until component unmount or success - const [idempotencyKey, setIdempotencyKey] = useState(nanoid()); const [hovered, setHovered] = useState(false); - const { isMutating, trigger } = useMutation({ - mutation: resumeSubscriptionMutation, - }); - - const resume = useCallback(() => { - trigger( - { idempotencyKey }, - { - onSuccess: data => { - // refresh idempotency key - setIdempotencyKey(nanoid()); - onSubscriptionUpdate(data.resumeSubscription); - }, - } - ); - }, [trigger, idempotencyKey, onSubscriptionUpdate]); return ( - <> + setHovered(true)} onMouseLeave={() => setHovered(false)} onClick={() => setOpen(true)} - loading={isMutating} - disabled={isMutating} > {hovered ? t['com.affine.payment.resume-renewal']() : t['com.affine.payment.current-plan']()} - - - > + ); };