mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
fix(core): free cloud and ai onetime payment adaptation (#8558)
close AF-1515, AF-1516
This commit is contained in:
@@ -39,6 +39,7 @@ import {
|
|||||||
} from '../../../../atoms';
|
} from '../../../../atoms';
|
||||||
import { CancelAction, ResumeAction } from '../plans/actions';
|
import { CancelAction, ResumeAction } from '../plans/actions';
|
||||||
import { AICancel, AIResume, AISubscribe } from '../plans/ai/actions';
|
import { AICancel, AIResume, AISubscribe } from '../plans/ai/actions';
|
||||||
|
import { AIRedeemCodeButton } from '../plans/ai/actions/redeem';
|
||||||
import { BelieverCard } from '../plans/lifetime/believer-card';
|
import { BelieverCard } from '../plans/lifetime/believer-card';
|
||||||
import { BelieverBenefits } from '../plans/lifetime/benefits';
|
import { BelieverBenefits } from '../plans/lifetime/benefits';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
@@ -94,7 +95,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 isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$);
|
||||||
|
|
||||||
const [openCancelModal, setOpenCancelModal] = useState(false);
|
const [openCancelModal, setOpenCancelModal] = useState(false);
|
||||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
@@ -347,6 +348,7 @@ const AIPlanCard = ({ onClick }: { onClick: () => void }) => {
|
|||||||
}, [subscriptionService]);
|
}, [subscriptionService]);
|
||||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||||
const subscription = useLiveData(subscriptionService.subscription.ai$);
|
const subscription = useLiveData(subscriptionService.subscription.ai$);
|
||||||
|
const isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$);
|
||||||
|
|
||||||
const priceReadable = price?.yearlyAmount
|
const priceReadable = price?.yearlyAmount
|
||||||
? `$${(price.yearlyAmount / 100).toFixed(2)}`
|
? `$${(price.yearlyAmount / 100).toFixed(2)}`
|
||||||
@@ -389,7 +391,9 @@ const AIPlanCard = ({ onClick }: { onClick: () => void }) => {
|
|||||||
/>
|
/>
|
||||||
{price?.yearlyAmount ? (
|
{price?.yearlyAmount ? (
|
||||||
subscription ? (
|
subscription ? (
|
||||||
subscription.canceledAt ? (
|
isOnetime ? (
|
||||||
|
<AIRedeemCodeButton className={styles.planAction} />
|
||||||
|
) : subscription.canceledAt ? (
|
||||||
<AIResume className={styles.planAction} />
|
<AIResume className={styles.planAction} />
|
||||||
) : (
|
) : (
|
||||||
<AICancel className={styles.planAction} />
|
<AICancel className={styles.planAction} />
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Button, type ButtonProps } from '@affine/component';
|
||||||
|
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||||
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
|
import {
|
||||||
|
SubscriptionPlan,
|
||||||
|
SubscriptionRecurring,
|
||||||
|
SubscriptionVariant,
|
||||||
|
} from '@affine/graphql';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import track from '@affine/track';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { CheckoutSlot } from '../../checkout-slot';
|
||||||
|
|
||||||
|
export const AIRedeemCodeButton = (btnProps: ButtonProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const authService = useService(AuthService);
|
||||||
|
|
||||||
|
const onBeforeCheckout = useCallback(() => {
|
||||||
|
track.$.settingsPanel.plans.checkout({
|
||||||
|
plan: SubscriptionPlan.AI,
|
||||||
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const checkoutOptions = useMemo(
|
||||||
|
() => ({
|
||||||
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
|
plan: SubscriptionPlan.AI,
|
||||||
|
variant: SubscriptionVariant.Onetime,
|
||||||
|
coupon: null,
|
||||||
|
successCallbackLink: generateSubscriptionCallbackLink(
|
||||||
|
authService.session.account$.value,
|
||||||
|
SubscriptionPlan.AI,
|
||||||
|
SubscriptionRecurring.Yearly
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[authService.session.account$.value]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckoutSlot
|
||||||
|
onBeforeCheckout={onBeforeCheckout}
|
||||||
|
checkoutOptions={checkoutOptions}
|
||||||
|
renderer={props => (
|
||||||
|
<Button variant="primary" {...btnProps} {...props}>
|
||||||
|
{t['com.affine.payment.redeem-code']()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,79 +1,49 @@
|
|||||||
import { Button, type ButtonProps, Skeleton } from '@affine/component';
|
import { Button, type ButtonProps, Skeleton } from '@affine/component';
|
||||||
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||||
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 { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { nanoid } from 'nanoid';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
import { CheckoutSlot } from '../../checkout-slot';
|
||||||
|
|
||||||
export interface AISubscribeProps extends ButtonProps {
|
export interface AISubscribeProps extends ButtonProps {
|
||||||
displayedFrequency?: 'yearly' | 'monthly';
|
displayedFrequency?: 'yearly' | 'monthly' | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AISubscribe = ({
|
export const AISubscribe = ({
|
||||||
displayedFrequency = 'yearly',
|
displayedFrequency = 'yearly',
|
||||||
...btnProps
|
...btnProps
|
||||||
}: AISubscribeProps) => {
|
}: AISubscribeProps) => {
|
||||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
|
||||||
const [isMutating, setMutating] = useState(false);
|
|
||||||
const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false);
|
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
|
|
||||||
const subscriptionService = useService(SubscriptionService);
|
const subscriptionService = useService(SubscriptionService);
|
||||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||||
useEffect(() => {
|
|
||||||
subscriptionService.prices.revalidate();
|
|
||||||
}, [subscriptionService]);
|
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
useEffect(() => {
|
const onBeforeCheckout = useCallback(() => {
|
||||||
if (isOpenedExternalWindow) {
|
|
||||||
// when the external window is opened, revalidate the subscription when window get focus
|
|
||||||
window.addEventListener(
|
|
||||||
'focus',
|
|
||||||
subscriptionService.subscription.revalidate
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener(
|
|
||||||
'focus',
|
|
||||||
subscriptionService.subscription.revalidate
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}, [isOpenedExternalWindow, subscriptionService]);
|
|
||||||
|
|
||||||
const subscribe = useAsyncCallback(async () => {
|
|
||||||
setMutating(true);
|
|
||||||
track.$.settingsPanel.plans.checkout({
|
track.$.settingsPanel.plans.checkout({
|
||||||
plan: SubscriptionPlan.AI,
|
plan: SubscriptionPlan.AI,
|
||||||
recurring: SubscriptionRecurring.Yearly,
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
});
|
});
|
||||||
try {
|
}, []);
|
||||||
const session = await subscriptionService.createCheckoutSession({
|
const checkoutOptions = useMemo(
|
||||||
recurring: SubscriptionRecurring.Yearly,
|
() => ({
|
||||||
idempotencyKey,
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
plan: SubscriptionPlan.AI,
|
plan: SubscriptionPlan.AI,
|
||||||
variant: null,
|
variant: null,
|
||||||
coupon: null,
|
coupon: null,
|
||||||
successCallbackLink: generateSubscriptionCallbackLink(
|
successCallbackLink: generateSubscriptionCallbackLink(
|
||||||
authService.session.account$.value,
|
authService.session.account$.value,
|
||||||
SubscriptionPlan.AI,
|
SubscriptionPlan.AI,
|
||||||
SubscriptionRecurring.Yearly
|
SubscriptionRecurring.Yearly
|
||||||
),
|
),
|
||||||
});
|
}),
|
||||||
popupWindow(session);
|
[authService.session.account$.value]
|
||||||
setOpenedExternalWindow(true);
|
);
|
||||||
setIdempotencyKey(nanoid());
|
|
||||||
} finally {
|
|
||||||
setMutating(false);
|
|
||||||
}
|
|
||||||
}, [authService, idempotencyKey, subscriptionService]);
|
|
||||||
|
|
||||||
if (!price || !price.yearlyAmount) {
|
if (!price || !price.yearlyAmount) {
|
||||||
return (
|
return (
|
||||||
@@ -100,25 +70,26 @@ export const AISubscribe = ({
|
|||||||
: t['com.affine.payment.billing-setting.month']();
|
: t['com.affine.payment.billing-setting.month']();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<CheckoutSlot
|
||||||
loading={isMutating}
|
onBeforeCheckout={onBeforeCheckout}
|
||||||
onClick={subscribe}
|
checkoutOptions={checkoutOptions}
|
||||||
variant="primary"
|
renderer={props => (
|
||||||
{...btnProps}
|
<Button variant="primary" {...props} {...btnProps}>
|
||||||
>
|
{btnProps.children ?? `${priceReadable} / ${priceFrequency}`}
|
||||||
{btnProps.children ?? `${priceReadable} / ${priceFrequency}`}
|
{displayedFrequency === 'monthly' ? (
|
||||||
{displayedFrequency === 'monthly' ? (
|
<span
|
||||||
<span
|
style={{
|
||||||
style={{
|
fontSize: 10,
|
||||||
fontSize: 10,
|
opacity: 0.75,
|
||||||
opacity: 0.75,
|
letterSpacing: -0.2,
|
||||||
letterSpacing: -0.2,
|
paddingLeft: 4,
|
||||||
paddingLeft: 4,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{t['com.affine.payment.ai.subscribe.billed-annually']()}
|
||||||
{t['com.affine.payment.ai.subscribe.billed-annually']()}
|
</span>
|
||||||
</span>
|
) : null}
|
||||||
) : null}
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useLiveData, useService } from '@toeverything/infra';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { AICancel, AILogin, AIResume, AISubscribe } from './actions';
|
import { AICancel, AILogin, AIResume, AISubscribe } from './actions';
|
||||||
|
import { AIRedeemCodeButton } from './actions/redeem';
|
||||||
import * as styles from './ai-plan.css';
|
import * as styles from './ai-plan.css';
|
||||||
import { AIPlanLayout } from './layout';
|
import { AIPlanLayout } from './layout';
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export const AIPlan = () => {
|
|||||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||||
const isLoggedIn =
|
const isLoggedIn =
|
||||||
useLiveData(authService.session.status$) === 'authenticated';
|
useLiveData(authService.session.status$) === 'authenticated';
|
||||||
|
const isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
subscriptionService.subscription.revalidate();
|
subscriptionService.subscription.revalidate();
|
||||||
@@ -52,7 +54,9 @@ export const AIPlan = () => {
|
|||||||
actionButtons={
|
actionButtons={
|
||||||
isLoggedIn ? (
|
isLoggedIn ? (
|
||||||
subscription ? (
|
subscription ? (
|
||||||
subscription.canceledAt ? (
|
isOnetime ? (
|
||||||
|
<AIRedeemCodeButton className={styles.purchaseButton} />
|
||||||
|
) : subscription.canceledAt ? (
|
||||||
<AIResume className={styles.purchaseButton} />
|
<AIResume className={styles.purchaseButton} />
|
||||||
) : (
|
) : (
|
||||||
<AICancel className={styles.purchaseButton} />
|
<AICancel className={styles.purchaseButton} />
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
|
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||||
|
import { popupWindow } from '@affine/core/utils';
|
||||||
|
import type { CreateCheckoutSessionInput } from '@affine/graphql';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import {
|
||||||
|
type PropsWithChildren,
|
||||||
|
type ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export interface CheckoutSlotProps extends PropsWithChildren {
|
||||||
|
checkoutOptions: Omit<CreateCheckoutSessionInput, 'idempotencyKey'>;
|
||||||
|
onBeforeCheckout?: () => void;
|
||||||
|
onCheckoutError?: (error: any) => void;
|
||||||
|
onCheckoutSuccess?: () => void;
|
||||||
|
renderer: (props: { onClick: () => void; loading: boolean }) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper component for checkout action
|
||||||
|
*/
|
||||||
|
export const CheckoutSlot = ({
|
||||||
|
checkoutOptions,
|
||||||
|
onBeforeCheckout,
|
||||||
|
onCheckoutError,
|
||||||
|
onCheckoutSuccess,
|
||||||
|
renderer: Renderer,
|
||||||
|
}: CheckoutSlotProps) => {
|
||||||
|
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||||
|
const [isMutating, setMutating] = useState(false);
|
||||||
|
const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false);
|
||||||
|
|
||||||
|
const subscriptionService = useService(SubscriptionService);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
subscriptionService.prices.revalidate();
|
||||||
|
}, [subscriptionService]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpenedExternalWindow) {
|
||||||
|
// when the external window is opened, revalidate the subscription when window get focus
|
||||||
|
window.addEventListener(
|
||||||
|
'focus',
|
||||||
|
subscriptionService.subscription.revalidate
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
'focus',
|
||||||
|
subscriptionService.subscription.revalidate
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}, [isOpenedExternalWindow, subscriptionService]);
|
||||||
|
|
||||||
|
const subscribe = useAsyncCallback(async () => {
|
||||||
|
setMutating(true);
|
||||||
|
onBeforeCheckout?.();
|
||||||
|
try {
|
||||||
|
const session = await subscriptionService.createCheckoutSession({
|
||||||
|
idempotencyKey,
|
||||||
|
...checkoutOptions,
|
||||||
|
});
|
||||||
|
popupWindow(session);
|
||||||
|
setOpenedExternalWindow(true);
|
||||||
|
setIdempotencyKey(nanoid());
|
||||||
|
onCheckoutSuccess?.();
|
||||||
|
} catch (e) {
|
||||||
|
onCheckoutError?.(e);
|
||||||
|
} finally {
|
||||||
|
setMutating(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
checkoutOptions,
|
||||||
|
idempotencyKey,
|
||||||
|
onBeforeCheckout,
|
||||||
|
onCheckoutError,
|
||||||
|
onCheckoutSuccess,
|
||||||
|
subscriptionService,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <Renderer onClick={subscribe} loading={isMutating} />;
|
||||||
|
};
|
||||||
@@ -17,7 +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$);
|
const isOnetime = useLiveData(subscriptionService.subscription.isOnetimePro$);
|
||||||
|
|
||||||
if (!readableLifetimePrice) return null;
|
if (!readableLifetimePrice) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Tooltip } from '@affine/component/ui/tooltip';
|
|||||||
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||||
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 {
|
import {
|
||||||
type CreateCheckoutSessionInput,
|
type CreateCheckoutSessionInput,
|
||||||
SubscriptionRecurring,
|
SubscriptionRecurring,
|
||||||
@@ -21,10 +20,11 @@ import clsx from 'clsx';
|
|||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { authAtom } from '../../../../atoms/index';
|
import { authAtom } from '../../../../atoms/index';
|
||||||
import { CancelAction, ResumeAction } from './actions';
|
import { CancelAction, ResumeAction } from './actions';
|
||||||
|
import { CheckoutSlot } from './checkout-slot';
|
||||||
import type { DynamicPrice, FixedPrice } from './cloud-plans';
|
import type { DynamicPrice, FixedPrice } from './cloud-plans';
|
||||||
import { ConfirmLoadingModal } from './modals';
|
import { ConfirmLoadingModal } from './modals';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
@@ -103,7 +103,8 @@ 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$);
|
const isOnetime = useLiveData(subscriptionService.subscription.isOnetimePro$);
|
||||||
|
const isFree = detail.plan === SubscriptionPlan.Free;
|
||||||
|
|
||||||
// branches:
|
// branches:
|
||||||
// if contact => 'Contact Sales'
|
// if contact => 'Contact Sales'
|
||||||
@@ -112,7 +113,9 @@ 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 onetime
|
||||||
|
// if free => 'Included in Pro'
|
||||||
|
// else => 'Redeem Code'
|
||||||
// if isCurrent
|
// if isCurrent
|
||||||
// if canceled => 'Resume'
|
// if canceled => 'Resume'
|
||||||
// else => 'Current Plan'
|
// else => 'Current Plan'
|
||||||
@@ -147,11 +150,16 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => {
|
|||||||
|
|
||||||
// onetime
|
// onetime
|
||||||
if (isOnetime) {
|
if (isOnetime) {
|
||||||
return <RedeemCode recurring={recurring} />;
|
return isFree ? (
|
||||||
|
<Button className={styles.planAction} disabled>
|
||||||
|
{t['com.affine.payment.cloud.onetime.included']()}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<RedeemCode recurring={recurring} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCanceled = !!primarySubscription?.canceledAt;
|
const isCanceled = !!primarySubscription?.canceledAt;
|
||||||
const isFree = detail.plan === SubscriptionPlan.Free;
|
|
||||||
const isCurrent =
|
const isCurrent =
|
||||||
detail.plan === currentPlan &&
|
detail.plan === currentPlan &&
|
||||||
(isFree
|
(isFree
|
||||||
@@ -261,42 +269,20 @@ export const Upgrade = ({
|
|||||||
recurring: SubscriptionRecurring;
|
recurring: SubscriptionRecurring;
|
||||||
checkoutInput?: Partial<CreateCheckoutSessionInput>;
|
checkoutInput?: Partial<CreateCheckoutSessionInput>;
|
||||||
}) => {
|
}) => {
|
||||||
const [isMutating, setMutating] = useState(false);
|
|
||||||
const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false);
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const subscriptionService = useService(SubscriptionService);
|
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
|
|
||||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
const onBeforeCheckout = useCallback(() => {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpenedExternalWindow) {
|
|
||||||
// when the external window is opened, revalidate the subscription when window get focus
|
|
||||||
window.addEventListener(
|
|
||||||
'focus',
|
|
||||||
subscriptionService.subscription.revalidate
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener(
|
|
||||||
'focus',
|
|
||||||
subscriptionService.subscription.revalidate
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}, [isOpenedExternalWindow, subscriptionService]);
|
|
||||||
|
|
||||||
const upgrade = useAsyncCallback(async () => {
|
|
||||||
setMutating(true);
|
|
||||||
track.$.settingsPanel.plans.checkout({
|
track.$.settingsPanel.plans.checkout({
|
||||||
plan: SubscriptionPlan.Pro,
|
plan: SubscriptionPlan.Pro,
|
||||||
recurring: recurring,
|
recurring: recurring,
|
||||||
});
|
});
|
||||||
const link = await subscriptionService.createCheckoutSession({
|
}, [recurring]);
|
||||||
|
|
||||||
|
const checkoutOptions = useMemo(
|
||||||
|
() => ({
|
||||||
recurring,
|
recurring,
|
||||||
idempotencyKey,
|
plan: SubscriptionPlan.Pro,
|
||||||
plan: SubscriptionPlan.Pro, // Only support prod plan now.
|
|
||||||
variant: null,
|
variant: null,
|
||||||
coupon: null,
|
coupon: null,
|
||||||
successCallbackLink: generateSubscriptionCallbackLink(
|
successCallbackLink: generateSubscriptionCallbackLink(
|
||||||
@@ -305,30 +291,25 @@ export const Upgrade = ({
|
|||||||
recurring
|
recurring
|
||||||
),
|
),
|
||||||
...checkoutInput,
|
...checkoutInput,
|
||||||
});
|
}),
|
||||||
setMutating(false);
|
[authService.session.account$.value, checkoutInput, recurring]
|
||||||
setIdempotencyKey(nanoid());
|
);
|
||||||
popupWindow(link);
|
|
||||||
setOpenedExternalWindow(true);
|
|
||||||
}, [
|
|
||||||
recurring,
|
|
||||||
authService.session.account$.value,
|
|
||||||
subscriptionService,
|
|
||||||
idempotencyKey,
|
|
||||||
checkoutInput,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<CheckoutSlot
|
||||||
className={clsx(styles.planAction, className)}
|
onBeforeCheckout={onBeforeCheckout}
|
||||||
variant="primary"
|
checkoutOptions={checkoutOptions}
|
||||||
onClick={upgrade}
|
renderer={props => (
|
||||||
disabled={isMutating}
|
<Button
|
||||||
loading={isMutating}
|
className={clsx(styles.planAction, className)}
|
||||||
{...btnProps}
|
variant="primary"
|
||||||
>
|
{...props}
|
||||||
{children ?? t['com.affine.payment.upgrade']()}
|
{...btnProps}
|
||||||
</Button>
|
>
|
||||||
|
{children ?? t['com.affine.payment.upgrade']()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ 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(
|
isOnetimePro$ = this.pro$.map(
|
||||||
|
sub => sub?.variant === SubscriptionVariant.Onetime
|
||||||
|
);
|
||||||
|
isOnetimeAI$ = this.ai$.map(
|
||||||
sub => sub?.variant === SubscriptionVariant.Onetime
|
sub => sub?.variant === SubscriptionVariant.Onetime
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -789,6 +789,7 @@
|
|||||||
"com.affine.payment.cloud.free.description": "Open-source under MIT license.",
|
"com.affine.payment.cloud.free.description": "Open-source under MIT license.",
|
||||||
"com.affine.payment.cloud.free.name": "FOSS + Basic",
|
"com.affine.payment.cloud.free.name": "FOSS + Basic",
|
||||||
"com.affine.payment.cloud.free.title": "Free forever",
|
"com.affine.payment.cloud.free.title": "Free forever",
|
||||||
|
"com.affine.payment.cloud.onetime.included": "Included in Pro plan",
|
||||||
"com.affine.payment.cloud.lifetime.included": "Included in Believer plan",
|
"com.affine.payment.cloud.lifetime.included": "Included in Believer plan",
|
||||||
"com.affine.payment.cloud.pricing-plan.select.caption": "We host, no technical setup required.",
|
"com.affine.payment.cloud.pricing-plan.select.caption": "We host, no technical setup required.",
|
||||||
"com.affine.payment.cloud.pricing-plan.select.title": "Hosted by AFFiNE.Pro",
|
"com.affine.payment.cloud.pricing-plan.select.title": "Hosted by AFFiNE.Pro",
|
||||||
|
|||||||
Reference in New Issue
Block a user