From 29a31110cdc099a154616b85be0b2fc2d06f4627 Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Thu, 10 Oct 2024 10:12:43 +0000 Subject: [PATCH] feat(core): onetime subscription ui (#8462) --- .../general-setting/billing/index.tsx | 13 ++++- .../plans/lifetime/lifetime-plan.tsx | 5 +- .../general-setting/plans/plan-card.tsx | 48 +++++++++++++++++-- .../modules/cloud/entities/subscription.ts | 9 +++- .../frontend/graphql/src/graphql/index.ts | 1 + .../graphql/src/graphql/subscription.gql | 1 + packages/frontend/graphql/src/schema.ts | 2 + packages/frontend/i18n/src/resources/en.json | 3 ++ 8 files changed, 74 insertions(+), 8 deletions(-) diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx index a9d406fe0cae..69a0bbe4413e 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx @@ -94,6 +94,7 @@ const SubscriptionSettings = () => { const proSubscription = useLiveData(subscriptionService.subscription.pro$); const proPrice = useLiveData(subscriptionService.prices.proPrice$); const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$); + const isOnetime = useLiveData(subscriptionService.subscription.isOnetime$); const [openCancelModal, setOpenCancelModal] = useState(false); const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); @@ -206,7 +207,17 @@ const SubscriptionSettings = () => { })} /> )} - {isBeliever ? null : proSubscription.end && + {isOnetime && proSubscription.end && ( + + )} + {isBeliever || isOnetime ? null : proSubscription.end && proSubscription.canceledAt ? ( { subscriptionService.prices.readableLifetimePrice$ ); const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$); + const isOnetime = useLiveData(subscriptionService.subscription.isOnetime$); if (!readableLifetimePrice) return null; @@ -36,6 +37,8 @@ export const LifetimePlan = () => { + ) : isOnetime ? ( + ) : ( { ); const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free; const currentRecurring = primarySubscription?.recurring; + const isOnetime = useLiveData(subscriptionService.subscription.isOnetime$); // branches: // if contact => 'Contact Sales' @@ -104,12 +112,12 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => { // else => 'Buy Pro' // else // if isBeliever => 'Included in Lifetime' + // if onetime => 'Redeem Code' // if isCurrent // if canceled => 'Resume' // else => 'Current Plan' - // if isCurrent => 'Current Plan' - // else if free => 'Downgrade' - // else if currentRecurring !== recurring => 'Change to {recurring} Billing' + // if free => 'Downgrade' + // if currentRecurring !== recurring => 'Change to {recurring} Billing' // else => 'Upgrade' // contact @@ -137,6 +145,11 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => { ); } + // onetime + if (isOnetime) { + return ; + } + const isCanceled = !!primarySubscription?.canceledAt; const isFree = detail.plan === SubscriptionPlan.Free; const isCurrent = @@ -242,9 +255,11 @@ export const Upgrade = ({ className, recurring, children, + checkoutInput, ...btnProps }: ButtonProps & { recurring: SubscriptionRecurring; + checkoutInput?: Partial; }) => { const [isMutating, setMutating] = useState(false); const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false); @@ -289,6 +304,7 @@ export const Upgrade = ({ SubscriptionPlan.Pro, recurring ), + ...checkoutInput, }); setMutating(false); setIdempotencyKey(nanoid()); @@ -299,6 +315,7 @@ export const Upgrade = ({ authService.session.account$.value, subscriptionService, idempotencyKey, + checkoutInput, ]); return ( @@ -435,3 +452,24 @@ const ResumeButton = () => { ); }; + +const redeemCodeCheckoutInput = { variant: SubscriptionVariant.Onetime }; +export const RedeemCode = ({ + className, + recurring = SubscriptionRecurring.Yearly, + children, + ...btnProps +}: ButtonProps & { recurring?: SubscriptionRecurring }) => { + const t = useI18n(); + + return ( + + {children ?? t['com.affine.payment.redeem-code']()} + + ); +}; diff --git a/packages/frontend/core/src/modules/cloud/entities/subscription.ts b/packages/frontend/core/src/modules/cloud/entities/subscription.ts index c512a95ee944..550ea1840882 100644 --- a/packages/frontend/core/src/modules/cloud/entities/subscription.ts +++ b/packages/frontend/core/src/modules/cloud/entities/subscription.ts @@ -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 { backoffRetry, @@ -41,6 +45,9 @@ export class Subscription extends Entity { isBeliever$ = this.pro$.map( sub => sub?.recurring === SubscriptionRecurring.Lifetime ); + isOnetime$ = this.pro$.map( + sub => sub?.variant === SubscriptionVariant.Onetime + ); constructor( private readonly authService: AuthService, diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index 92c5c62af0fb..2bc168d6cf54 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -1037,6 +1037,7 @@ query subscription { end nextBillAt canceledAt + variant } } }`, diff --git a/packages/frontend/graphql/src/graphql/subscription.gql b/packages/frontend/graphql/src/graphql/subscription.gql index 61d90af087be..bea635c6de9f 100644 --- a/packages/frontend/graphql/src/graphql/subscription.gql +++ b/packages/frontend/graphql/src/graphql/subscription.gql @@ -10,6 +10,7 @@ query subscription { end nextBillAt canceledAt + variant } } } diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index c2c12d30e1e5..eaea9072bccd 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -1141,6 +1141,7 @@ export interface UserSubscription { trialEnd: Maybe; trialStart: Maybe; updatedAt: Scalars['DateTime']['output']; + variant: Maybe; } export interface UserType { @@ -2210,6 +2211,7 @@ export type SubscriptionQuery = { end: string | null; nextBillAt: string | null; canceledAt: string | null; + variant: SubscriptionVariant | null; }>; } | null; }; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index fa3c24942f29..550a987b5353 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -742,6 +742,8 @@ "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.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.subtitle": "Manage your billing information and invoices.", "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-title": "Subscription updated", "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.later": "Later", "com.affine.payment.upgrade-success-notify.ok-client": "Sure, open in browser",