feat(core): onetime subscription ui (#8462)

This commit is contained in:
CatsJuice
2024-10-10 10:12:43 +00:00
parent 69fb5c06f4
commit 29a31110cd
8 changed files with 74 additions and 8 deletions
@@ -94,6 +94,7 @@ const SubscriptionSettings = () => {
const proSubscription = useLiveData(subscriptionService.subscription.pro$); const proSubscription = useLiveData(subscriptionService.subscription.pro$);
const proPrice = useLiveData(subscriptionService.prices.proPrice$); const proPrice = useLiveData(subscriptionService.prices.proPrice$);
const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$); const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$);
const isOnetime = useLiveData(subscriptionService.subscription.isOnetime$);
const [openCancelModal, setOpenCancelModal] = useState(false); const [openCancelModal, setOpenCancelModal] = useState(false);
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
@@ -206,7 +207,17 @@ const SubscriptionSettings = () => {
})} })}
/> />
)} )}
{isBeliever ? null : proSubscription.end && {isOnetime && proSubscription.end && (
<SettingRow
name={t['com.affine.payment.billing-setting.due-date']()}
desc={t[
'com.affine.payment.billing-setting.due-date.description'
]({
dueDate: new Date(proSubscription.end).toLocaleDateString(),
})}
/>
)}
{isBeliever || isOnetime ? null : proSubscription.end &&
proSubscription.canceledAt ? ( proSubscription.canceledAt ? (
<SettingRow <SettingRow
name={t['com.affine.payment.billing-setting.expiration-date']()} name={t['com.affine.payment.billing-setting.expiration-date']()}
@@ -4,7 +4,7 @@ import { SubscriptionRecurring } from '@affine/graphql';
import { Trans, useI18n } from '@affine/i18n'; import { Trans, useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { Upgrade } from '../plan-card'; import { RedeemCode, Upgrade } from '../plan-card';
import { BelieverCard } from './believer-card'; import { BelieverCard } from './believer-card';
import { BelieverBenefits } from './benefits'; import { BelieverBenefits } from './benefits';
import * as styles from './style.css'; import * as styles from './style.css';
@@ -17,6 +17,7 @@ export const LifetimePlan = () => {
subscriptionService.prices.readableLifetimePrice$ subscriptionService.prices.readableLifetimePrice$
); );
const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$); const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$);
const isOnetime = useLiveData(subscriptionService.subscription.isOnetime$);
if (!readableLifetimePrice) return null; if (!readableLifetimePrice) return null;
@@ -36,6 +37,8 @@ export const LifetimePlan = () => {
<Button className={styles.purchase} size="default" disabled> <Button className={styles.purchase} size="default" disabled>
{t['com.affine.payment.lifetime.purchased']()} {t['com.affine.payment.lifetime.purchased']()}
</Button> </Button>
) : isOnetime ? (
<RedeemCode className={styles.purchase} size="default" />
) : ( ) : (
<Upgrade <Upgrade
className={styles.purchase} className={styles.purchase}
@@ -4,8 +4,15 @@ import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud'; import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
import { popupWindow } from '@affine/core/utils'; import { popupWindow } from '@affine/core/utils';
import type { SubscriptionRecurring } from '@affine/graphql'; import {
import { SubscriptionPlan, SubscriptionStatus } from '@affine/graphql'; type CreateCheckoutSessionInput,
SubscriptionRecurring,
} from '@affine/graphql';
import {
SubscriptionPlan,
SubscriptionStatus,
SubscriptionVariant,
} from '@affine/graphql';
import { Trans, useI18n } from '@affine/i18n'; import { Trans, useI18n } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
import { DoneIcon } from '@blocksuite/icons/rc'; import { DoneIcon } from '@blocksuite/icons/rc';
@@ -96,6 +103,7 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => {
); );
const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free; const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free;
const currentRecurring = primarySubscription?.recurring; const currentRecurring = primarySubscription?.recurring;
const isOnetime = useLiveData(subscriptionService.subscription.isOnetime$);
// branches: // branches:
// if contact => 'Contact Sales' // if contact => 'Contact Sales'
@@ -104,12 +112,12 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => {
// else => 'Buy Pro' // else => 'Buy Pro'
// else // else
// if isBeliever => 'Included in Lifetime' // if isBeliever => 'Included in Lifetime'
// if onetime => 'Redeem Code'
// if isCurrent // if isCurrent
// if canceled => 'Resume' // if canceled => 'Resume'
// else => 'Current Plan' // else => 'Current Plan'
// if isCurrent => 'Current Plan' // if free => 'Downgrade'
// else if free => 'Downgrade' // if currentRecurring !== recurring => 'Change to {recurring} Billing'
// else if currentRecurring !== recurring => 'Change to {recurring} Billing'
// else => 'Upgrade' // else => 'Upgrade'
// contact // contact
@@ -137,6 +145,11 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => {
); );
} }
// onetime
if (isOnetime) {
return <RedeemCode recurring={recurring} />;
}
const isCanceled = !!primarySubscription?.canceledAt; const isCanceled = !!primarySubscription?.canceledAt;
const isFree = detail.plan === SubscriptionPlan.Free; const isFree = detail.plan === SubscriptionPlan.Free;
const isCurrent = const isCurrent =
@@ -242,9 +255,11 @@ export const Upgrade = ({
className, className,
recurring, recurring,
children, children,
checkoutInput,
...btnProps ...btnProps
}: ButtonProps & { }: ButtonProps & {
recurring: SubscriptionRecurring; recurring: SubscriptionRecurring;
checkoutInput?: Partial<CreateCheckoutSessionInput>;
}) => { }) => {
const [isMutating, setMutating] = useState(false); const [isMutating, setMutating] = useState(false);
const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false); const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false);
@@ -289,6 +304,7 @@ export const Upgrade = ({
SubscriptionPlan.Pro, SubscriptionPlan.Pro,
recurring recurring
), ),
...checkoutInput,
}); });
setMutating(false); setMutating(false);
setIdempotencyKey(nanoid()); setIdempotencyKey(nanoid());
@@ -299,6 +315,7 @@ export const Upgrade = ({
authService.session.account$.value, authService.session.account$.value,
subscriptionService, subscriptionService,
idempotencyKey, idempotencyKey,
checkoutInput,
]); ]);
return ( return (
@@ -435,3 +452,24 @@ const ResumeButton = () => {
</ResumeAction> </ResumeAction>
); );
}; };
const redeemCodeCheckoutInput = { variant: SubscriptionVariant.Onetime };
export const RedeemCode = ({
className,
recurring = SubscriptionRecurring.Yearly,
children,
...btnProps
}: ButtonProps & { recurring?: SubscriptionRecurring }) => {
const t = useI18n();
return (
<Upgrade
recurring={recurring}
className={className}
checkoutInput={redeemCodeCheckoutInput}
{...btnProps}
>
{children ?? t['com.affine.payment.redeem-code']()}
</Upgrade>
);
};
@@ -1,4 +1,8 @@
import { type SubscriptionQuery, SubscriptionRecurring } from '@affine/graphql'; import {
type SubscriptionQuery,
SubscriptionRecurring,
SubscriptionVariant,
} from '@affine/graphql';
import { SubscriptionPlan } from '@affine/graphql'; import { SubscriptionPlan } from '@affine/graphql';
import { import {
backoffRetry, backoffRetry,
@@ -41,6 +45,9 @@ export class Subscription extends Entity {
isBeliever$ = this.pro$.map( isBeliever$ = this.pro$.map(
sub => sub?.recurring === SubscriptionRecurring.Lifetime sub => sub?.recurring === SubscriptionRecurring.Lifetime
); );
isOnetime$ = this.pro$.map(
sub => sub?.variant === SubscriptionVariant.Onetime
);
constructor( constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
@@ -1037,6 +1037,7 @@ query subscription {
end end
nextBillAt nextBillAt
canceledAt canceledAt
variant
} }
} }
}`, }`,
@@ -10,6 +10,7 @@ query subscription {
end end
nextBillAt nextBillAt
canceledAt canceledAt
variant
} }
} }
} }
+2
View File
@@ -1141,6 +1141,7 @@ export interface UserSubscription {
trialEnd: Maybe<Scalars['DateTime']['output']>; trialEnd: Maybe<Scalars['DateTime']['output']>;
trialStart: Maybe<Scalars['DateTime']['output']>; trialStart: Maybe<Scalars['DateTime']['output']>;
updatedAt: Scalars['DateTime']['output']; updatedAt: Scalars['DateTime']['output'];
variant: Maybe<SubscriptionVariant>;
} }
export interface UserType { export interface UserType {
@@ -2210,6 +2211,7 @@ export type SubscriptionQuery = {
end: string | null; end: string | null;
nextBillAt: string | null; nextBillAt: string | null;
canceledAt: string | null; canceledAt: string | null;
variant: SubscriptionVariant | null;
}>; }>;
} | null; } | null;
}; };
@@ -742,6 +742,8 @@
"com.affine.payment.billing-setting.payment-method.description": "Provided by Stripe.", "com.affine.payment.billing-setting.payment-method.description": "Provided by Stripe.",
"com.affine.payment.billing-setting.renew-date": "Renew date", "com.affine.payment.billing-setting.renew-date": "Renew date",
"com.affine.payment.billing-setting.renew-date.description": "Next billing date: {{renewDate}}", "com.affine.payment.billing-setting.renew-date.description": "Next billing date: {{renewDate}}",
"com.affine.payment.billing-setting.due-date": "Due date",
"com.affine.payment.billing-setting.due-date.description": "Your subscription will end on {{dueDate}}",
"com.affine.payment.billing-setting.resume-subscription": "Resume", "com.affine.payment.billing-setting.resume-subscription": "Resume",
"com.affine.payment.billing-setting.subtitle": "Manage your billing information and invoices.", "com.affine.payment.billing-setting.subtitle": "Manage your billing information and invoices.",
"com.affine.payment.billing-setting.title": "Billing", "com.affine.payment.billing-setting.title": "Billing",
@@ -870,6 +872,7 @@
"com.affine.payment.updated-notify-msg": "You have changed your plan to {{plan}} billing.", "com.affine.payment.updated-notify-msg": "You have changed your plan to {{plan}} billing.",
"com.affine.payment.updated-notify-title": "Subscription updated", "com.affine.payment.updated-notify-title": "Subscription updated",
"com.affine.payment.upgrade": "Upgrade", "com.affine.payment.upgrade": "Upgrade",
"com.affine.payment.redeem-code": "Redeem code",
"com.affine.payment.upgrade-success-notify.content": "We'd like to hear more about your use case, so that we can make AFFiNE better.", "com.affine.payment.upgrade-success-notify.content": "We'd like to hear more about your use case, so that we can make AFFiNE better.",
"com.affine.payment.upgrade-success-notify.later": "Later", "com.affine.payment.upgrade-success-notify.later": "Later",
"com.affine.payment.upgrade-success-notify.ok-client": "Sure, open in browser", "com.affine.payment.upgrade-success-notify.ok-client": "Sure, open in browser",