mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-09-20 07:57:29 +03:00
fix(core): billing cancel confirm dialog (#4795)
This commit is contained in:
parent
e5be570f54
commit
e5c86a9249
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user