fix(core): billing cancel confirm dialog (#4795)
This commit is contained in:
@@ -5,14 +5,12 @@ import {
|
|||||||
SettingWrapper,
|
SettingWrapper,
|
||||||
} from '@affine/component/setting-components';
|
} from '@affine/component/setting-components';
|
||||||
import {
|
import {
|
||||||
cancelSubscriptionMutation,
|
|
||||||
createCustomerPortalMutation,
|
createCustomerPortalMutation,
|
||||||
getInvoicesCountQuery,
|
getInvoicesCountQuery,
|
||||||
type InvoicesQuery,
|
type InvoicesQuery,
|
||||||
invoicesQuery,
|
invoicesQuery,
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
pricesQuery,
|
pricesQuery,
|
||||||
resumeSubscriptionMutation,
|
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
SubscriptionRecurring,
|
SubscriptionRecurring,
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
@@ -25,7 +23,6 @@ import { Skeleton } from '@mui/material';
|
|||||||
import { Button, IconButton } from '@toeverything/components/button';
|
import { Button, IconButton } from '@toeverything/components/button';
|
||||||
import { Loading } from '@toeverything/components/loading';
|
import { Loading } from '@toeverything/components/loading';
|
||||||
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';
|
||||||
@@ -34,7 +31,7 @@ import {
|
|||||||
type SubscriptionMutator,
|
type SubscriptionMutator,
|
||||||
useUserSubscription,
|
useUserSubscription,
|
||||||
} from '../../../../../hooks/use-subscription';
|
} from '../../../../../hooks/use-subscription';
|
||||||
import { DowngradeModal } from '../plans/modals';
|
import { CancelAction, ResumeAction } from '../plans/actions';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
enum DescriptionI18NKey {
|
enum DescriptionI18NKey {
|
||||||
@@ -89,25 +86,8 @@ export const BillingSettings = () => {
|
|||||||
|
|
||||||
const SubscriptionSettings = () => {
|
const SubscriptionSettings = () => {
|
||||||
const [subscription, mutateSubscription] = useUserSubscription();
|
const [subscription, mutateSubscription] = useUserSubscription();
|
||||||
const { isMutating, trigger } = useMutation({
|
|
||||||
mutation: cancelSubscriptionMutation,
|
|
||||||
});
|
|
||||||
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(() => {
|
|
||||||
trigger(
|
|
||||||
{ idempotencyKey },
|
|
||||||
{
|
|
||||||
onSuccess: data => {
|
|
||||||
mutateSubscription(data.cancelSubscription);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [trigger, idempotencyKey, mutateSubscription]);
|
|
||||||
|
|
||||||
const { data: pricesQueryResult } = useQuery({
|
const { data: pricesQueryResult } = useQuery({
|
||||||
query: pricesQuery,
|
query: pricesQuery,
|
||||||
});
|
});
|
||||||
@@ -213,9 +193,14 @@ const SubscriptionSettings = () => {
|
|||||||
<ResumeSubscription onSubscriptionUpdate={mutateSubscription} />
|
<ResumeSubscription onSubscriptionUpdate={mutateSubscription} />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
) : (
|
) : (
|
||||||
|
<CancelAction
|
||||||
|
open={openCancelModal}
|
||||||
|
onOpenChange={setOpenCancelModal}
|
||||||
|
onSubscriptionUpdate={mutateSubscription}
|
||||||
|
>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => (isMutating ? null : setOpenCancelModal(true))}
|
onClick={() => setOpenCancelModal(true)}
|
||||||
className="dangerous-setting"
|
className="dangerous-setting"
|
||||||
name={t[
|
name={t[
|
||||||
'com.affine.payment.billing-setting.cancel-subscription'
|
'com.affine.payment.billing-setting.cancel-subscription'
|
||||||
@@ -226,14 +211,10 @@ const SubscriptionSettings = () => {
|
|||||||
cancelDate: new Date(subscription.end).toLocaleDateString(),
|
cancelDate: new Date(subscription.end).toLocaleDateString(),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<CancelSubscription loading={isMutating} />
|
<CancelSubscription />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
</CancelAction>
|
||||||
)}
|
)}
|
||||||
<DowngradeModal
|
|
||||||
open={openCancelModal}
|
|
||||||
onCancel={() => (isMutating ? null : cancel())}
|
|
||||||
onOpenChange={setOpenCancelModal}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -295,33 +276,18 @@ const ResumeSubscription = ({
|
|||||||
onSubscriptionUpdate: SubscriptionMutator;
|
onSubscriptionUpdate: SubscriptionMutator;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { isMutating, trigger } = useMutation({
|
const [open, setOpen] = useState(false);
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<ResumeAction
|
||||||
className={styles.button}
|
open={open}
|
||||||
onClick={resume}
|
onOpenChange={setOpen}
|
||||||
loading={isMutating}
|
onSubscriptionUpdate={onSubscriptionUpdate}
|
||||||
disabled={isMutating}
|
|
||||||
>
|
>
|
||||||
|
<Button className={styles.button} onClick={() => setOpen(true)}>
|
||||||
{t['com.affine.payment.billing-setting.resume-subscription']()}
|
{t['com.affine.payment.billing-setting.resume-subscription']()}
|
||||||
</Button>
|
</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 = ({
|
export const DowngradeModal = ({
|
||||||
open,
|
open,
|
||||||
|
loading,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: {
|
}: {
|
||||||
@@ -81,6 +82,14 @@ export const DowngradeModal = ({
|
|||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const canceled = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && open && canceled.current) {
|
||||||
|
onOpenChange?.(false);
|
||||||
|
canceled.current = false;
|
||||||
|
}
|
||||||
|
}, [loading, open, onOpenChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -102,14 +111,19 @@ export const DowngradeModal = ({
|
|||||||
<footer className={styles.downgradeFooter}>
|
<footer className={styles.downgradeFooter}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onOpenChange?.(false);
|
canceled.current = true;
|
||||||
onCancel?.();
|
onCancel?.();
|
||||||
}}
|
}}
|
||||||
|
loading={loading}
|
||||||
>
|
>
|
||||||
{t['com.affine.payment.modal.downgrade.cancel']()}
|
{t['com.affine.payment.modal.downgrade.cancel']()}
|
||||||
</Button>
|
</Button>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button onClick={() => onOpenChange?.(false)} type="primary">
|
<Button
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => onOpenChange?.(false)}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
{t['com.affine.payment.modal.downgrade.confirm']()}
|
{t['com.affine.payment.modal.downgrade.confirm']()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import type {
|
|||||||
SubscriptionMutator,
|
SubscriptionMutator,
|
||||||
} from '@affine/core/hooks/use-subscription';
|
} from '@affine/core/hooks/use-subscription';
|
||||||
import {
|
import {
|
||||||
cancelSubscriptionMutation,
|
|
||||||
checkoutMutation,
|
checkoutMutation,
|
||||||
resumeSubscriptionMutation,
|
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
SubscriptionRecurring,
|
SubscriptionRecurring,
|
||||||
updateSubscriptionMutation,
|
updateSubscriptionMutation,
|
||||||
@@ -31,8 +29,9 @@ import {
|
|||||||
import { openPaymentDisableAtom } from '../../../../../atoms';
|
import { openPaymentDisableAtom } from '../../../../../atoms';
|
||||||
import { authAtom } from '../../../../../atoms/index';
|
import { authAtom } from '../../../../../atoms/index';
|
||||||
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
|
||||||
|
import { CancelAction, ResumeAction } from './actions';
|
||||||
import { BulledListIcon } from './icons/bulled-list';
|
import { BulledListIcon } from './icons/bulled-list';
|
||||||
import { ConfirmLoadingModal, DowngradeModal } from './modals';
|
import { ConfirmLoadingModal } from './modals';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
export interface FixedPrice {
|
export interface FixedPrice {
|
||||||
@@ -130,14 +129,8 @@ export const PlanCard = (props: PlanCardProps) => {
|
|||||||
const { detail, subscription, recurring } = props;
|
const { detail, subscription, recurring } = props;
|
||||||
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
||||||
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
|
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||||
const currentRecurring = subscription?.recurring;
|
|
||||||
|
|
||||||
const isCurrent =
|
const isCurrent = loggedIn && detail.plan === currentPlan;
|
||||||
loggedIn &&
|
|
||||||
detail.plan === currentPlan &&
|
|
||||||
(currentPlan === SubscriptionPlan.Free
|
|
||||||
? true
|
|
||||||
: currentRecurring === recurring);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -255,7 +248,7 @@ const ActionButton = ({
|
|||||||
// is current
|
// is current
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
return isCanceled ? (
|
return isCanceled ? (
|
||||||
<ResumeAction onSubscriptionUpdate={mutateAndNotify} />
|
<ResumeButton onSubscriptionUpdate={mutateAndNotify} />
|
||||||
) : (
|
) : (
|
||||||
<CurrentPlan />
|
<CurrentPlan />
|
||||||
);
|
);
|
||||||
@@ -301,46 +294,30 @@ const Downgrade = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [open, setOpen] = useState(false);
|
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
|
const tooltipContent = disabled
|
||||||
? t['com.affine.payment.downgraded-tooltip']()
|
? t['com.affine.payment.downgraded-tooltip']()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<CancelAction
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
onSubscriptionUpdate={onSubscriptionUpdate}
|
||||||
|
>
|
||||||
<Tooltip content={tooltipContent} rootOptions={{ delayDuration: 0 }}>
|
<Tooltip content={tooltipContent} rootOptions={{ delayDuration: 0 }}>
|
||||||
<div className={styles.planAction}>
|
<div className={styles.planAction}>
|
||||||
<Button
|
<Button
|
||||||
className={styles.planAction}
|
className={styles.planAction}
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
disabled={disabled || isMutating}
|
disabled={disabled}
|
||||||
loading={isMutating}
|
|
||||||
>
|
>
|
||||||
{t['com.affine.payment.downgrade']()}
|
{t['com.affine.payment.downgrade']()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DowngradeModal open={open} onCancel={downgrade} onOpenChange={setOpen} />
|
</CancelAction>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -527,55 +504,31 @@ const SignUpAction = ({ children }: PropsWithChildren) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ResumeAction = ({
|
const ResumeButton = ({
|
||||||
onSubscriptionUpdate,
|
onSubscriptionUpdate,
|
||||||
}: {
|
}: {
|
||||||
onSubscriptionUpdate: SubscriptionMutator;
|
onSubscriptionUpdate: SubscriptionMutator;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [open, setOpen] = useState(false);
|
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 [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 (
|
return (
|
||||||
<>
|
<ResumeAction
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
onSubscriptionUpdate={onSubscriptionUpdate}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
className={styles.planAction}
|
className={styles.planAction}
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
loading={isMutating}
|
|
||||||
disabled={isMutating}
|
|
||||||
>
|
>
|
||||||
{hovered
|
{hovered
|
||||||
? t['com.affine.payment.resume-renewal']()
|
? t['com.affine.payment.resume-renewal']()
|
||||||
: t['com.affine.payment.current-plan']()}
|
: t['com.affine.payment.current-plan']()}
|
||||||
</Button>
|
</Button>
|
||||||
|
</ResumeAction>
|
||||||
<ConfirmLoadingModal
|
|
||||||
type={'resume'}
|
|
||||||
open={open}
|
|
||||||
onConfirm={resume}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
loading={isMutating}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user