fix(core): billing cancel confirm dialog (#4795)

This commit is contained in:
Cats Juice 2023-11-01 17:38:43 +08:00 committed by GitHub
parent e5be570f54
commit e5c86a9249
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 171 additions and 131 deletions

View File

@ -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 = () => {
<ResumeSubscription onSubscriptionUpdate={mutateSubscription} />
</SettingRow>
) : (
<SettingRow
style={{ cursor: 'pointer' }}
onClick={() => (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(),
})}
<CancelAction
open={openCancelModal}
onOpenChange={setOpenCancelModal}
onSubscriptionUpdate={mutateSubscription}
>
<CancelSubscription loading={isMutating} />
</SettingRow>
<SettingRow
style={{ cursor: 'pointer' }}
onClick={() => 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(),
})}
>
<CancelSubscription />
</SettingRow>
</CancelAction>
)}
<DowngradeModal
open={openCancelModal}
onCancel={() => (isMutating ? null : cancel())}
onOpenChange={setOpenCancelModal}
/>
</>
)}
</div>
@ -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 (
<Button
className={styles.button}
onClick={resume}
loading={isMutating}
disabled={isMutating}
<ResumeAction
open={open}
onOpenChange={setOpen}
onSubscriptionUpdate={onSubscriptionUpdate}
>
{t['com.affine.payment.billing-setting.resume-subscription']()}
</Button>
<Button className={styles.button} onClick={() => setOpen(true)}>
{t['com.affine.payment.billing-setting.resume-subscription']()}
</Button>
</ResumeAction>
);
};

View File

@ -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}
<DowngradeModal
open={open}
onCancel={downgrade}
onOpenChange={onOpenChange}
loading={isMutating}
/>
</>
);
};
/**
* 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}
<ConfirmLoadingModal
type={'resume'}
open={open}
onConfirm={resume}
onOpenChange={onOpenChange}
loading={isMutating}
/>
</>
);
};

View File

@ -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 (
<Modal
@ -102,14 +111,19 @@ export const DowngradeModal = ({
<footer className={styles.downgradeFooter}>
<Button
onClick={() => {
onOpenChange?.(false);
canceled.current = true;
onCancel?.();
}}
loading={loading}
>
{t['com.affine.payment.modal.downgrade.cancel']()}
</Button>
<DialogTrigger asChild>
<Button onClick={() => onOpenChange?.(false)} type="primary">
<Button
disabled={loading}
onClick={() => onOpenChange?.(false)}
type="primary"
>
{t['com.affine.payment.modal.downgrade.confirm']()}
</Button>
</DialogTrigger>

View File

@ -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 (
<div
@ -255,7 +248,7 @@ const ActionButton = ({
// is current
if (isCurrent) {
return isCanceled ? (
<ResumeAction onSubscriptionUpdate={mutateAndNotify} />
<ResumeButton onSubscriptionUpdate={mutateAndNotify} />
) : (
<CurrentPlan />
);
@ -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 (
<>
<CancelAction
open={open}
onOpenChange={setOpen}
onSubscriptionUpdate={onSubscriptionUpdate}
>
<Tooltip content={tooltipContent} rootOptions={{ delayDuration: 0 }}>
<div className={styles.planAction}>
<Button
className={styles.planAction}
type="primary"
onClick={() => setOpen(true)}
disabled={disabled || isMutating}
loading={isMutating}
disabled={disabled}
>
{t['com.affine.payment.downgrade']()}
</Button>
</div>
</Tooltip>
<DowngradeModal open={open} onCancel={downgrade} onOpenChange={setOpen} />
</>
</CancelAction>
);
};
@ -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 (
<>
<ResumeAction
open={open}
onOpenChange={setOpen}
onSubscriptionUpdate={onSubscriptionUpdate}
>
<Button
className={styles.planAction}
onMouseEnter={() => 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']()}
</Button>
<ConfirmLoadingModal
type={'resume'}
open={open}
onConfirm={resume}
onOpenChange={setOpen}
loading={isMutating}
/>
</>
</ResumeAction>
);
};