mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-27 06:33:32 +03:00
feat(core): ai subscription in billing page (#6476)
This commit is contained in:
parent
97c4ae48b5
commit
4a93582799
@ -21,6 +21,7 @@ import {
|
|||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { Suspense, useCallback, useMemo, useState } from 'react';
|
import { Suspense, useCallback, useMemo, useState } from 'react';
|
||||||
@ -34,6 +35,8 @@ import { useUserSubscription } from '../../../../../hooks/use-subscription';
|
|||||||
import { mixpanel, popupWindow } from '../../../../../utils';
|
import { mixpanel, popupWindow } from '../../../../../utils';
|
||||||
import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary';
|
import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary';
|
||||||
import { CancelAction, ResumeAction } from '../plans/actions';
|
import { CancelAction, ResumeAction } from '../plans/actions';
|
||||||
|
import { useAffineAIPrice } from '../plans/ai/use-affine-ai-price';
|
||||||
|
import { useAffineAISubscription } from '../plans/ai/use-affine-ai-subscription';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
enum DescriptionI18NKey {
|
enum DescriptionI18NKey {
|
||||||
@ -93,6 +96,11 @@ export const BillingSettings = () => {
|
|||||||
const SubscriptionSettings = () => {
|
const SubscriptionSettings = () => {
|
||||||
const [subscription, mutateSubscription] = useUserSubscription();
|
const [subscription, mutateSubscription] = useUserSubscription();
|
||||||
const [openCancelModal, setOpenCancelModal] = useState(false);
|
const [openCancelModal, setOpenCancelModal] = useState(false);
|
||||||
|
const {
|
||||||
|
actionType: aiActionType,
|
||||||
|
Action: AIAction,
|
||||||
|
billingTip,
|
||||||
|
} = useAffineAISubscription();
|
||||||
|
|
||||||
const { data: pricesQueryResult } = useQuery({
|
const { data: pricesQueryResult } = useQuery({
|
||||||
query: pricesQuery,
|
query: pricesQuery,
|
||||||
@ -102,6 +110,10 @@ const SubscriptionSettings = () => {
|
|||||||
const recurring = subscription?.recurring ?? SubscriptionRecurring.Monthly;
|
const recurring = subscription?.recurring ?? SubscriptionRecurring.Monthly;
|
||||||
|
|
||||||
const price = pricesQueryResult.prices.find(price => price.plan === plan);
|
const price = pricesQueryResult.prices.find(price => price.plan === plan);
|
||||||
|
const aiPrice = pricesQueryResult.prices.find(
|
||||||
|
price => price.plan === SubscriptionPlan.AI
|
||||||
|
);
|
||||||
|
assertExists(aiPrice);
|
||||||
const amount =
|
const amount =
|
||||||
plan === SubscriptionPlan.Free
|
plan === SubscriptionPlan.Free
|
||||||
? '0'
|
? '0'
|
||||||
@ -111,6 +123,8 @@ const SubscriptionSettings = () => {
|
|||||||
: String((price.yearlyAmount ?? 0) / 100)
|
: String((price.yearlyAmount ?? 0) / 100)
|
||||||
: '?';
|
: '?';
|
||||||
|
|
||||||
|
const { priceReadable: aiPriceReadable, priceFrequency: aiPriceFrequency } =
|
||||||
|
useAffineAIPrice(aiPrice);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
@ -167,6 +181,30 @@ const SubscriptionSettings = () => {
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.planCard} style={{ marginTop: 24 }}>
|
||||||
|
<div className={styles.currentPlan}>
|
||||||
|
<SettingRow
|
||||||
|
spreadCol={false}
|
||||||
|
name={t['com.affine.payment.billing-setting.ai-plan']()}
|
||||||
|
desc={billingTip}
|
||||||
|
/>
|
||||||
|
{aiPrice?.yearlyAmount ? (
|
||||||
|
<AIAction
|
||||||
|
className={styles.planAction}
|
||||||
|
price={aiPrice}
|
||||||
|
recurring={SubscriptionRecurring.Yearly}
|
||||||
|
onSubscriptionUpdate={mutateSubscription}
|
||||||
|
>
|
||||||
|
{aiActionType === 'subscribe' ? 'Purchase' : null}
|
||||||
|
</AIAction>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<p className={styles.planPrice}>
|
||||||
|
{aiPriceReadable}
|
||||||
|
<span className={styles.billingFrequency}>/{aiPriceFrequency}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{subscription?.status === SubscriptionStatus.Active && (
|
{subscription?.status === SubscriptionStatus.Active && (
|
||||||
<>
|
<>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
@ -365,6 +403,13 @@ const InvoiceLine = ({
|
|||||||
}
|
}
|
||||||
}, [invoice.link]);
|
}, [invoice.link]);
|
||||||
|
|
||||||
|
const planText =
|
||||||
|
invoice.plan === SubscriptionPlan.AI
|
||||||
|
? 'AFFiNE AI'
|
||||||
|
: invoice.plan === SubscriptionPlan.Pro
|
||||||
|
? 'AFFiNE Cloud'
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingRow
|
<SettingRow
|
||||||
key={invoice.id}
|
key={invoice.id}
|
||||||
@ -374,7 +419,7 @@ const InvoiceLine = ({
|
|||||||
invoice.status === InvoiceStatus.Paid
|
invoice.status === InvoiceStatus.Paid
|
||||||
? t['com.affine.payment.billing-setting.paid']()
|
? t['com.affine.payment.billing-setting.paid']()
|
||||||
: ''
|
: ''
|
||||||
} $${invoice.amount / 100}`}
|
} $${invoice.amount / 100} - ${planText}`}
|
||||||
>
|
>
|
||||||
<Button className={styles.button} onClick={open}>
|
<Button className={styles.button} onClick={open}>
|
||||||
{t['com.affine.payment.billing-setting.view-invoice']()}
|
{t['com.affine.payment.billing-setting.view-invoice']()}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import { Button, useConfirmModal } from '@affine/component';
|
import { Button, type ButtonProps, useConfirmModal } from '@affine/component';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useMutation } from '@affine/core/hooks/use-mutation';
|
import { useMutation } from '@affine/core/hooks/use-mutation';
|
||||||
import { cancelSubscriptionMutation } from '@affine/graphql';
|
import { cancelSubscriptionMutation, SubscriptionPlan } from '@affine/graphql';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { purchaseButton } from './ai-plan.css';
|
import type { BaseActionProps } from '../types';
|
||||||
import type { BaseActionProps } from './types';
|
|
||||||
|
|
||||||
interface AICancelProps extends BaseActionProps {}
|
export interface AICancelProps extends BaseActionProps, ButtonProps {}
|
||||||
export const AICancel = ({ plan, onSubscriptionUpdate }: AICancelProps) => {
|
export const AICancel = ({
|
||||||
|
onSubscriptionUpdate,
|
||||||
|
...btnProps
|
||||||
|
}: AICancelProps) => {
|
||||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||||
const { trigger, isMutating } = useMutation({
|
const { trigger, isMutating } = useMutation({
|
||||||
mutation: cancelSubscriptionMutation,
|
mutation: cancelSubscriptionMutation,
|
||||||
@ -32,7 +34,7 @@ export const AICancel = ({ plan, onSubscriptionUpdate }: AICancelProps) => {
|
|||||||
},
|
},
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await trigger(
|
await trigger(
|
||||||
{ idempotencyKey, plan },
|
{ idempotencyKey, plan: SubscriptionPlan.AI },
|
||||||
{
|
{
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
// refresh idempotency key
|
// refresh idempotency key
|
||||||
@ -43,15 +45,10 @@ export const AICancel = ({ plan, onSubscriptionUpdate }: AICancelProps) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [openConfirmModal, trigger, idempotencyKey, plan, onSubscriptionUpdate]);
|
}, [openConfirmModal, trigger, idempotencyKey, onSubscriptionUpdate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button onClick={cancel} loading={isMutating} type="primary" {...btnProps}>
|
||||||
onClick={cancel}
|
|
||||||
loading={isMutating}
|
|
||||||
className={purchaseButton}
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
Cancel subscription
|
Cancel subscription
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
@ -0,0 +1,4 @@
|
|||||||
|
export * from './cancel';
|
||||||
|
export * from './login';
|
||||||
|
export * from './resume';
|
||||||
|
export * from './subscribe';
|
@ -1,9 +1,11 @@
|
|||||||
import { Button } from '@affine/component';
|
import { Button, type ButtonProps } from '@affine/component';
|
||||||
import { authAtom } from '@affine/core/atoms';
|
import { authAtom } from '@affine/core/atoms';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
export const AILogin = () => {
|
import type { BaseActionProps } from '../types';
|
||||||
|
|
||||||
|
export const AILogin = (btnProps: BaseActionProps & ButtonProps) => {
|
||||||
const setOpen = useSetAtom(authAtom);
|
const setOpen = useSetAtom(authAtom);
|
||||||
|
|
||||||
const onClickSignIn = useCallback(() => {
|
const onClickSignIn = useCallback(() => {
|
||||||
@ -13,5 +15,9 @@ export const AILogin = () => {
|
|||||||
}));
|
}));
|
||||||
}, [setOpen]);
|
}, [setOpen]);
|
||||||
|
|
||||||
return <Button onClick={onClickSignIn}>Login</Button>;
|
return (
|
||||||
|
<Button onClick={onClickSignIn} type="primary" {...btnProps}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
};
|
};
|
@ -1,18 +1,25 @@
|
|||||||
import { Button, notify, useConfirmModal } from '@affine/component';
|
import {
|
||||||
|
Button,
|
||||||
|
type ButtonProps,
|
||||||
|
notify,
|
||||||
|
useConfirmModal,
|
||||||
|
} from '@affine/component';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useMutation } from '@affine/core/hooks/use-mutation';
|
import { useMutation } from '@affine/core/hooks/use-mutation';
|
||||||
import { resumeSubscriptionMutation } from '@affine/graphql';
|
import { resumeSubscriptionMutation, SubscriptionPlan } from '@affine/graphql';
|
||||||
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons';
|
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons';
|
||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { purchaseButton } from './ai-plan.css';
|
import type { BaseActionProps } from '../types';
|
||||||
import type { BaseActionProps } from './types';
|
|
||||||
|
|
||||||
interface AIResumeProps extends BaseActionProps {}
|
export interface AIResumeProps extends BaseActionProps, ButtonProps {}
|
||||||
|
|
||||||
export const AIResume = ({ plan, onSubscriptionUpdate }: AIResumeProps) => {
|
export const AIResume = ({
|
||||||
|
onSubscriptionUpdate,
|
||||||
|
...btnProps
|
||||||
|
}: AIResumeProps) => {
|
||||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||||
|
|
||||||
const { isMutating, trigger } = useMutation({
|
const { isMutating, trigger } = useMutation({
|
||||||
@ -31,7 +38,7 @@ export const AIResume = ({ plan, onSubscriptionUpdate }: AIResumeProps) => {
|
|||||||
},
|
},
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await trigger(
|
await trigger(
|
||||||
{ idempotencyKey, plan },
|
{ idempotencyKey, plan: SubscriptionPlan.AI },
|
||||||
{
|
{
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
// refresh idempotency key
|
// refresh idempotency key
|
||||||
@ -51,15 +58,10 @@ export const AIResume = ({ plan, onSubscriptionUpdate }: AIResumeProps) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [openConfirmModal, trigger, idempotencyKey, plan, onSubscriptionUpdate]);
|
}, [openConfirmModal, trigger, idempotencyKey, onSubscriptionUpdate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button loading={isMutating} onClick={resume} type="primary" {...btnProps}>
|
||||||
loading={isMutating}
|
|
||||||
onClick={resume}
|
|
||||||
className={purchaseButton}
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
Resume
|
Resume
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
@ -1,23 +1,27 @@
|
|||||||
import { Button } from '@affine/component';
|
import { Button, type ButtonProps } from '@affine/component';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useMutation } from '@affine/core/hooks/use-mutation';
|
import { useMutation } from '@affine/core/hooks/use-mutation';
|
||||||
import { popupWindow } from '@affine/core/utils';
|
import { popupWindow } from '@affine/core/utils';
|
||||||
import { createCheckoutSessionMutation } from '@affine/graphql';
|
import {
|
||||||
|
createCheckoutSessionMutation,
|
||||||
|
SubscriptionPlan,
|
||||||
|
} from '@affine/graphql';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { purchaseButton } from './ai-plan.css';
|
import type { BaseActionProps } from '../types';
|
||||||
import type { BaseActionProps } from './types';
|
import { useAffineAIPrice } from '../use-affine-ai-price';
|
||||||
|
|
||||||
interface AISubscribeProps extends BaseActionProps {}
|
export interface AISubscribeProps extends BaseActionProps, ButtonProps {}
|
||||||
|
|
||||||
export const AISubscribe = ({
|
export const AISubscribe = ({
|
||||||
price,
|
price,
|
||||||
plan,
|
|
||||||
recurring,
|
recurring,
|
||||||
onSubscriptionUpdate,
|
onSubscriptionUpdate,
|
||||||
|
...btnProps
|
||||||
}: AISubscribeProps) => {
|
}: AISubscribeProps) => {
|
||||||
const idempotencyKey = useMemo(() => `${nanoid()}-${recurring}`, [recurring]);
|
const idempotencyKey = useMemo(() => `${nanoid()}-${recurring}`, [recurring]);
|
||||||
|
const { priceReadable, priceFrequency } = useAffineAIPrice(price);
|
||||||
|
|
||||||
const newTabRef = useRef<Window | null>(null);
|
const newTabRef = useRef<Window | null>(null);
|
||||||
|
|
||||||
@ -45,7 +49,7 @@ export const AISubscribe = ({
|
|||||||
input: {
|
input: {
|
||||||
recurring,
|
recurring,
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
plan,
|
plan: SubscriptionPlan.AI,
|
||||||
coupon: null,
|
coupon: null,
|
||||||
successCallbackLink: null,
|
successCallbackLink: null,
|
||||||
},
|
},
|
||||||
@ -60,7 +64,7 @@ export const AISubscribe = ({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [idempotencyKey, onClose, plan, recurring, trigger]);
|
}, [idempotencyKey, onClose, recurring, trigger]);
|
||||||
|
|
||||||
if (!price.yearlyAmount) return null;
|
if (!price.yearlyAmount) return null;
|
||||||
|
|
||||||
@ -68,10 +72,10 @@ export const AISubscribe = ({
|
|||||||
<Button
|
<Button
|
||||||
loading={isMutating}
|
loading={isMutating}
|
||||||
onClick={subscribe}
|
onClick={subscribe}
|
||||||
className={purchaseButton}
|
|
||||||
type="primary"
|
type="primary"
|
||||||
|
{...btnProps}
|
||||||
>
|
>
|
||||||
${(price.yearlyAmount / 100).toFixed(2)} / Year
|
{btnProps.children ?? `${priceReadable} / ${priceFrequency}`}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,9 +1,7 @@
|
|||||||
import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
|
|
||||||
import {
|
import {
|
||||||
type SubscriptionMutator,
|
type SubscriptionMutator,
|
||||||
useUserSubscription,
|
useUserSubscription,
|
||||||
} from '@affine/core/hooks/use-subscription';
|
} from '@affine/core/hooks/use-subscription';
|
||||||
import { timestampToLocalDate } from '@affine/core/utils';
|
|
||||||
import {
|
import {
|
||||||
type PricesQuery,
|
type PricesQuery,
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
@ -13,42 +11,27 @@ import {
|
|||||||
import { AIPlanLayout } from '../layout';
|
import { AIPlanLayout } from '../layout';
|
||||||
import * as styles from './ai-plan.css';
|
import * as styles from './ai-plan.css';
|
||||||
import { AIBenefits } from './benefits';
|
import { AIBenefits } from './benefits';
|
||||||
import { AICancel } from './cancel';
|
|
||||||
import { AILogin } from './login';
|
|
||||||
import { AIResume } from './resume';
|
|
||||||
import { AISubscribe } from './subscribe';
|
|
||||||
import type { BaseActionProps } from './types';
|
import type { BaseActionProps } from './types';
|
||||||
|
import { useAffineAISubscription } from './use-affine-ai-subscription';
|
||||||
|
|
||||||
interface AIPlanProps {
|
interface AIPlanProps {
|
||||||
price?: PricesQuery['prices'][number];
|
price?: PricesQuery['prices'][number];
|
||||||
onSubscriptionUpdate: SubscriptionMutator;
|
onSubscriptionUpdate: SubscriptionMutator;
|
||||||
}
|
}
|
||||||
export const AIPlan = ({ price, onSubscriptionUpdate }: AIPlanProps) => {
|
export const AIPlan = ({ price, onSubscriptionUpdate }: AIPlanProps) => {
|
||||||
const plan = SubscriptionPlan.AI;
|
|
||||||
const recurring = SubscriptionRecurring.Yearly;
|
const recurring = SubscriptionRecurring.Yearly;
|
||||||
|
|
||||||
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
const { Action, billingTip } = useAffineAISubscription();
|
||||||
|
const [subscription] = useUserSubscription(SubscriptionPlan.AI);
|
||||||
const [subscription] = useUserSubscription(plan);
|
|
||||||
|
|
||||||
// yearly subscription should always be available
|
// yearly subscription should always be available
|
||||||
if (!price?.yearlyAmount) return null;
|
if (!price?.yearlyAmount) return null;
|
||||||
|
|
||||||
const baseActionProps: BaseActionProps = {
|
const baseActionProps: BaseActionProps = {
|
||||||
plan,
|
|
||||||
price,
|
price,
|
||||||
recurring,
|
recurring,
|
||||||
onSubscriptionUpdate,
|
onSubscriptionUpdate,
|
||||||
};
|
};
|
||||||
const isCancelled = !!subscription?.canceledAt;
|
|
||||||
|
|
||||||
const Action = !loggedIn
|
|
||||||
? AILogin
|
|
||||||
: !subscription
|
|
||||||
? AISubscribe
|
|
||||||
: isCancelled
|
|
||||||
? AIResume
|
|
||||||
: AICancel;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIPlanLayout
|
<AIPlanLayout
|
||||||
@ -70,9 +53,9 @@ export const AIPlan = ({ price, onSubscriptionUpdate }: AIPlanProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.actionBlock}>
|
<div className={styles.actionBlock}>
|
||||||
<Action {...baseActionProps} />
|
<Action {...baseActionProps} className={styles.purchaseButton} />
|
||||||
{subscription?.nextBillAt ? (
|
{billingTip ? (
|
||||||
<PurchasedTip due={timestampToLocalDate(subscription.nextBillAt)} />
|
<div className={styles.agreement}>{billingTip}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -81,9 +64,3 @@ export const AIPlan = ({ price, onSubscriptionUpdate }: AIPlanProps) => {
|
|||||||
</AIPlanLayout>
|
</AIPlanLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PurchasedTip = ({ due }: { due: string }) => (
|
|
||||||
<div className={styles.agreement}>
|
|
||||||
You have purchased AFFiNE AI. The next payment date is {due}.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import type { SubscriptionMutator } from '@affine/core/hooks/use-subscription';
|
import type { SubscriptionMutator } from '@affine/core/hooks/use-subscription';
|
||||||
import type {
|
import type { PricesQuery, SubscriptionRecurring } from '@affine/graphql';
|
||||||
PricesQuery,
|
|
||||||
SubscriptionPlan,
|
|
||||||
SubscriptionRecurring,
|
|
||||||
} from '@affine/graphql';
|
|
||||||
|
|
||||||
export interface BaseActionProps {
|
export interface BaseActionProps {
|
||||||
price: PricesQuery['prices'][number];
|
price: PricesQuery['prices'][number];
|
||||||
recurring: SubscriptionRecurring;
|
recurring: SubscriptionRecurring;
|
||||||
plan: SubscriptionPlan;
|
|
||||||
onSubscriptionUpdate: SubscriptionMutator;
|
onSubscriptionUpdate: SubscriptionMutator;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import type { PricesQuery } from '@affine/graphql';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
|
||||||
|
export const useAffineAIPrice = (price: PricesQuery['prices'][number]) => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
|
assertExists(price.yearlyAmount, 'AFFiNE AI yearly price is missing');
|
||||||
|
|
||||||
|
const priceReadable = `$${(price.yearlyAmount / 100).toFixed(2)}`;
|
||||||
|
const priceFrequency = t['com.affine.payment.billing-setting.year']();
|
||||||
|
|
||||||
|
return { priceReadable, priceFrequency };
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
|
||||||
|
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
|
||||||
|
import { timestampToLocalDate } from '@affine/core/utils';
|
||||||
|
import { SubscriptionPlan } from '@affine/graphql';
|
||||||
|
|
||||||
|
import { AICancel, AILogin, AIResume, AISubscribe } from './actions';
|
||||||
|
|
||||||
|
const plan = SubscriptionPlan.AI;
|
||||||
|
|
||||||
|
export type ActionType = 'login' | 'subscribe' | 'resume' | 'cancel';
|
||||||
|
|
||||||
|
export const useAffineAISubscription = () => {
|
||||||
|
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
||||||
|
|
||||||
|
const [subscription] = useUserSubscription(plan);
|
||||||
|
|
||||||
|
const isCancelled = !!subscription?.canceledAt;
|
||||||
|
const actionType: ActionType = !loggedIn
|
||||||
|
? 'login'
|
||||||
|
: !subscription
|
||||||
|
? 'subscribe'
|
||||||
|
: isCancelled
|
||||||
|
? 'resume'
|
||||||
|
: 'cancel';
|
||||||
|
|
||||||
|
const Action = {
|
||||||
|
login: AILogin,
|
||||||
|
subscribe: AISubscribe,
|
||||||
|
resume: AIResume,
|
||||||
|
cancel: AICancel,
|
||||||
|
}[actionType];
|
||||||
|
|
||||||
|
const billingTip = subscription?.nextBillAt
|
||||||
|
? `You have purchased AFFiNE AI. The next payment date is ${timestampToLocalDate(subscription.nextBillAt)}.`
|
||||||
|
: subscription?.canceledAt && subscription.end
|
||||||
|
? `You have purchased AFFiNE AI. The expiration date is ${timestampToLocalDate(subscription.end)}.`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return { actionType, Action, billingTip };
|
||||||
|
};
|
@ -844,10 +844,11 @@
|
|||||||
"com.affine.payment.billing-setting.cancel-subscription": "Cancel Subscription",
|
"com.affine.payment.billing-setting.cancel-subscription": "Cancel Subscription",
|
||||||
"com.affine.payment.billing-setting.cancel-subscription.description": "Subscription cancelled, your pro account will expire on {{cancelDate}}",
|
"com.affine.payment.billing-setting.cancel-subscription.description": "Subscription cancelled, your pro account will expire on {{cancelDate}}",
|
||||||
"com.affine.payment.billing-setting.change-plan": "Change Plan",
|
"com.affine.payment.billing-setting.change-plan": "Change Plan",
|
||||||
"com.affine.payment.billing-setting.current-plan": "Current Plan",
|
"com.affine.payment.billing-setting.current-plan": "AFFiNE Cloud",
|
||||||
"com.affine.payment.billing-setting.current-plan.description": "You are currently on the <1>{{planName}} plan</1>.",
|
"com.affine.payment.billing-setting.current-plan.description": "You are currently on the <1>{{planName}} plan</1>.",
|
||||||
"com.affine.payment.billing-setting.current-plan.description.monthly": "You are currently on the monthly <1>{{planName}} plan</1>.",
|
"com.affine.payment.billing-setting.current-plan.description.monthly": "You are currently on the monthly <1>{{planName}} plan</1>.",
|
||||||
"com.affine.payment.billing-setting.current-plan.description.yearly": "You are currently on the yearly <1>{{planName}} plan</1>.",
|
"com.affine.payment.billing-setting.current-plan.description.yearly": "You are currently on the yearly <1>{{planName}} plan</1>.",
|
||||||
|
"com.affine.payment.billing-setting.ai-plan": "AFFiNE AI",
|
||||||
"com.affine.payment.billing-setting.expiration-date": "Expiration Date",
|
"com.affine.payment.billing-setting.expiration-date": "Expiration Date",
|
||||||
"com.affine.payment.billing-setting.expiration-date.description": "Your subscription is valid until {{expirationDate}}",
|
"com.affine.payment.billing-setting.expiration-date.description": "Your subscription is valid until {{expirationDate}}",
|
||||||
"com.affine.payment.billing-setting.history": "Billing history",
|
"com.affine.payment.billing-setting.history": "Billing history",
|
||||||
|
Loading…
Reference in New Issue
Block a user