mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 03:33:06 +08:00
feat(core): onetime subscription ui (#8462)
This commit is contained in:
+12
-1
@@ -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
-1
@@ -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}
|
||||||
|
|||||||
+43
-5
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user