refactor(core): make subscription hook (#4669)

This commit is contained in:
liuyi
2023-10-24 11:40:46 +08:00
committed by forehalo
parent 858a1da35f
commit 95d37fc63f
3 changed files with 146 additions and 72 deletions
@@ -12,7 +12,6 @@ import {
pricesQuery, pricesQuery,
resumeSubscriptionMutation, resumeSubscriptionMutation,
SubscriptionPlan, SubscriptionPlan,
subscriptionQuery,
SubscriptionRecurring, SubscriptionRecurring,
SubscriptionStatus, SubscriptionStatus,
} from '@affine/graphql'; } from '@affine/graphql';
@@ -20,10 +19,14 @@ 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 { Suspense, useCallback, useEffect } from 'react'; import { Suspense, useCallback } from 'react';
import { openSettingModalAtom } from '../../../../../atoms'; import { openSettingModalAtom } from '../../../../../atoms';
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
import {
type SubscriptionMutator,
useUserSubscription,
} from '../../../../../hooks/use-subscription';
import * as styles from './style.css'; import * as styles from './style.css';
export const BillingSettings = () => { export const BillingSettings = () => {
@@ -56,14 +59,11 @@ export const BillingSettings = () => {
}; };
const SubscriptionSettings = () => { const SubscriptionSettings = () => {
const { data: subscriptionQueryResult } = useQuery({ const [subscription, mutateSubscription] = useUserSubscription();
query: subscriptionQuery,
});
const { data: pricesQueryResult } = useQuery({ const { data: pricesQueryResult } = useQuery({
query: pricesQuery, query: pricesQuery,
}); });
const subscription = subscriptionQueryResult.currentUser?.subscription;
const plan = subscription?.plan ?? SubscriptionPlan.Free; const plan = subscription?.plan ?? SubscriptionPlan.Free;
const recurring = subscription?.recurring ?? SubscriptionRecurring.Monthly; const recurring = subscription?.recurring ?? SubscriptionRecurring.Monthly;
@@ -123,7 +123,7 @@ const SubscriptionSettings = () => {
subscription.end subscription.end
).toLocaleDateString()}`} ).toLocaleDateString()}`}
> >
<ResumeSubscription /> <ResumeSubscription onSubscriptionUpdate={mutateSubscription} />
</SettingRow> </SettingRow>
) : ( ) : (
<SettingRow <SettingRow
@@ -133,7 +133,7 @@ const SubscriptionSettings = () => {
subscription.end subscription.end
).toLocaleDateString()}`} ).toLocaleDateString()}`}
> >
<CancelSubscription /> <CancelSubscription onSubscriptionUpdate={mutateSubscription} />
</SettingRow> </SettingRow>
)} )}
</> </>
@@ -166,20 +166,18 @@ const PlanAction = ({ plan }: { plan: string }) => {
const PaymentMethodUpdater = () => { const PaymentMethodUpdater = () => {
// TODO: open stripe customer portal // TODO: open stripe customer portal
const { isMutating, trigger, data } = useMutation({ const { isMutating, trigger } = useMutation({
mutation: createCustomerPortalMutation, mutation: createCustomerPortalMutation,
}); });
const update = useCallback(() => { const update = useCallback(() => {
trigger(); trigger(null, {
onSuccess: data => {
window.open(data.createCustomerPortal, '_blank', 'noopener noreferrer');
},
});
}, [trigger]); }, [trigger]);
useEffect(() => {
if (data?.createCustomerPortal) {
window.open(data.createCustomerPortal, '_blank', 'noopener noreferrer');
}
}, [data]);
return ( return (
<Button onClick={update} loading={isMutating} disabled={isMutating}> <Button onClick={update} loading={isMutating} disabled={isMutating}>
Update Update
@@ -187,14 +185,22 @@ const PaymentMethodUpdater = () => {
); );
}; };
const ResumeSubscription = () => { const ResumeSubscription = ({
onSubscriptionUpdate,
}: {
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const { isMutating, trigger } = useMutation({ const { isMutating, trigger } = useMutation({
mutation: resumeSubscriptionMutation, mutation: resumeSubscriptionMutation,
}); });
const resume = useCallback(() => { const resume = useCallback(() => {
trigger(); trigger(null, {
}, [trigger]); onSuccess: data => {
onSubscriptionUpdate(data.resumeSubscription);
},
});
}, [trigger, onSubscriptionUpdate]);
return ( return (
<Button onClick={resume} loading={isMutating} disabled={isMutating}> <Button onClick={resume} loading={isMutating} disabled={isMutating}>
@@ -203,14 +209,22 @@ const ResumeSubscription = () => {
); );
}; };
const CancelSubscription = () => { const CancelSubscription = ({
onSubscriptionUpdate,
}: {
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const { isMutating, trigger } = useMutation({ const { isMutating, trigger } = useMutation({
mutation: cancelSubscriptionMutation, mutation: cancelSubscriptionMutation,
}); });
const cancel = useCallback(() => { const cancel = useCallback(() => {
trigger(); trigger(null, {
}, [trigger]); onSuccess: data => {
onSubscriptionUpdate(data.cancelSubscription);
},
});
}, [trigger, onSubscriptionUpdate]);
return ( return (
<IconButton <IconButton
@@ -5,7 +5,6 @@ import {
checkoutMutation, checkoutMutation,
pricesQuery, pricesQuery,
SubscriptionPlan, SubscriptionPlan,
subscriptionQuery,
SubscriptionRecurring, SubscriptionRecurring,
updateSubscriptionMutation, updateSubscriptionMutation,
} from '@affine/graphql'; } from '@affine/graphql';
@@ -20,6 +19,11 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
import {
type SubscriptionMutator,
useUserSubscription,
} from '../../../../../hooks/use-subscription';
import * as styles from './style.css'; import * as styles from './style.css';
interface FixedPrice { interface FixedPrice {
@@ -102,9 +106,8 @@ const planDetail = new Map<SubscriptionPlan, FixedPrice | DynamicPrice>([
]); ]);
const Settings = () => { const Settings = () => {
const { data, mutate } = useQuery({ const [subscription, mutateSubscription] = useUserSubscription();
query: subscriptionQuery, const loggedIn = useCurrentLoginStatus() === 'authenticated';
});
const { const {
data: { prices }, data: { prices },
@@ -125,9 +128,6 @@ const Settings = () => {
} }
}); });
const loggedIn = !!data.currentUser;
const subscription = data.currentUser?.subscription;
const [recurring, setRecurring] = useState<string>( const [recurring, setRecurring] = useState<string>(
subscription?.recurring ?? SubscriptionRecurring.Yearly subscription?.recurring ?? SubscriptionRecurring.Yearly
); );
@@ -135,10 +135,6 @@ const Settings = () => {
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free; const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
const currentRecurring = subscription?.recurring; const currentRecurring = subscription?.recurring;
const refresh = useCallback(() => {
mutate();
}, [mutate]);
const yearlyDiscount = ( const yearlyDiscount = (
planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined
)?.discount; )?.discount;
@@ -228,19 +224,19 @@ const Settings = () => {
detail.plan === SubscriptionPlan.Free)) ? ( detail.plan === SubscriptionPlan.Free)) ? (
<CurrentPlan /> <CurrentPlan />
) : detail.plan === SubscriptionPlan.Free ? ( ) : detail.plan === SubscriptionPlan.Free ? (
<Downgrade onActionDone={refresh} /> <Downgrade onSubscriptionUpdate={mutateSubscription} />
) : currentRecurring !== recurring && ) : currentRecurring !== recurring &&
currentPlan === detail.plan ? ( currentPlan === detail.plan ? (
<ChangeRecurring <ChangeRecurring
// @ts-expect-error must exist // @ts-expect-error must exist
from={currentRecurring} from={currentRecurring}
to={recurring as SubscriptionRecurring} to={recurring as SubscriptionRecurring}
onActionDone={refresh} onSubscriptionUpdate={mutateSubscription}
/> />
) : ( ) : (
<Upgrade <Upgrade
recurring={recurring as SubscriptionRecurring} recurring={recurring as SubscriptionRecurring}
onActionDone={refresh} onSubscriptionUpdate={mutateSubscription}
/> />
) )
) : ( ) : (
@@ -280,14 +276,22 @@ const Settings = () => {
); );
}; };
const Downgrade = ({ onActionDone }: { onActionDone: () => void }) => { const Downgrade = ({
onSubscriptionUpdate,
}: {
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const { isMutating, trigger } = useMutation({ const { isMutating, trigger } = useMutation({
mutation: cancelSubscriptionMutation, mutation: cancelSubscriptionMutation,
}); });
const downgrade = useCallback(() => { const downgrade = useCallback(() => {
trigger(null, { onSuccess: onActionDone }); trigger(null, {
}, [trigger, onActionDone]); onSuccess: data => {
onSubscriptionUpdate(data.cancelSubscription);
},
});
}, [trigger, onSubscriptionUpdate]);
return ( return (
<Button <Button
@@ -304,48 +308,57 @@ const Downgrade = ({ onActionDone }: { onActionDone: () => void }) => {
const Upgrade = ({ const Upgrade = ({
recurring, recurring,
onActionDone, onSubscriptionUpdate,
}: { }: {
recurring: SubscriptionRecurring; recurring: SubscriptionRecurring;
onActionDone: () => void; onSubscriptionUpdate: SubscriptionMutator;
}) => { }) => {
const { isMutating, trigger, data } = useMutation({ const { isMutating, trigger } = useMutation({
mutation: checkoutMutation, mutation: checkoutMutation,
}); });
const upgrade = useCallback(() => {
trigger({ recurring });
}, [trigger, recurring]);
const newTabRef = useRef<Window | null>(null); const newTabRef = useRef<Window | null>(null);
useEffect(() => { const onClose = useCallback(() => {
if (data?.checkout) { newTabRef.current = null;
if (newTabRef.current) { onSubscriptionUpdate();
newTabRef.current.focus(); }, [onSubscriptionUpdate]);
} else {
// FIXME: safari prevents from opening new tab by window api
// TODO(@xp): what if electron?
const newTab = window.open(
data.checkout,
'_blank',
'noopener noreferrer'
);
if (newTab) { const upgrade = useCallback(() => {
newTabRef.current = newTab; if (newTabRef.current) {
const update = () => { newTabRef.current.focus();
onActionDone(); } else {
}; trigger(
newTab.addEventListener('close', update); { recurring },
{
onSuccess: data => {
// FIXME: safari prevents from opening new tab by window api
// TODO(@xp): what if electron?
const newTab = window.open(
data.checkout,
'_blank',
'noopener noreferrer'
);
return () => newTab.removeEventListener('close', update); if (newTab) {
newTabRef.current = newTab;
newTab.addEventListener('close', onClose);
}
},
} }
} );
} }
}, [trigger, recurring, onClose]);
return; useEffect(() => {
}, [data?.checkout, onActionDone]); return () => {
if (newTabRef.current) {
newTabRef.current.removeEventListener('close', onClose);
newTabRef.current = null;
}
};
}, [onClose]);
return ( return (
<Button <Button
@@ -363,19 +376,26 @@ const Upgrade = ({
const ChangeRecurring = ({ const ChangeRecurring = ({
from: _from /* TODO: from can be useful when showing confirmation modal */, from: _from /* TODO: from can be useful when showing confirmation modal */,
to, to,
onActionDone, onSubscriptionUpdate,
}: { }: {
from: SubscriptionRecurring; from: SubscriptionRecurring;
to: SubscriptionRecurring; to: SubscriptionRecurring;
onActionDone: () => void; onSubscriptionUpdate: SubscriptionMutator;
}) => { }) => {
const { isMutating, trigger } = useMutation({ const { isMutating, trigger } = useMutation({
mutation: updateSubscriptionMutation, mutation: updateSubscriptionMutation,
}); });
const change = useCallback(() => { const change = useCallback(() => {
trigger({ recurring: to }, { onSuccess: onActionDone }); trigger(
}, [trigger, onActionDone, to]); { recurring: to },
{
onSuccess: data => {
onSubscriptionUpdate(data.updateSubscriptionRecurring);
},
}
);
}, [trigger, onSubscriptionUpdate, to]);
return ( return (
<Button <Button
@@ -0,0 +1,40 @@
import { type SubscriptionQuery, subscriptionQuery } from '@affine/graphql';
import { useQuery } from '@affine/workspace/affine/gql';
import { useCallback } from 'react';
export type Subscription = NonNullable<
NonNullable<SubscriptionQuery['currentUser']>['subscription']
>;
export type SubscriptionMutator = (update?: Partial<Subscription>) => void;
const selector = (data: SubscriptionQuery) =>
data.currentUser?.subscription ?? null;
export const useUserSubscription = () => {
const { data, mutate } = useQuery({
query: subscriptionQuery,
});
const set: SubscriptionMutator = useCallback(
(update?: Partial<Subscription>) => {
mutate(prev => {
if (!update || !prev?.currentUser?.subscription) {
return;
}
return {
currentUser: {
subscription: {
...prev.currentUser?.subscription,
...update,
},
},
};
});
},
[mutate]
);
return [selector(data), set] as const;
};