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",