mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 11:06:25 +08:00
feat(core): i18n for pricing plans (#6499)
feat(core): adjust pricing plans style feat(core): i18n for pricing plans
This commit is contained in:
+10
-6
@@ -2,6 +2,7 @@ import { Button, type ButtonProps, useConfirmModal } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useMutation } from '@affine/core/hooks/use-mutation';
|
||||
import { cancelSubscriptionMutation, SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -12,6 +13,7 @@ export const AICancel = ({
|
||||
onSubscriptionUpdate,
|
||||
...btnProps
|
||||
}: AICancelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const { trigger, isMutating } = useMutation({
|
||||
mutation: cancelSubscriptionMutation,
|
||||
@@ -20,15 +22,17 @@ export const AICancel = ({
|
||||
|
||||
const cancel = useAsyncCallback(async () => {
|
||||
openConfirmModal({
|
||||
title: 'Cancel Subscription',
|
||||
title: t['com.affine.payment.ai.action.cancel.confirm.title'](),
|
||||
description:
|
||||
'If you end your subscription now, you can still use AFFiNE AI until the end of this billing period.',
|
||||
t['com.affine.payment.ai.action.cancel.confirm.description'](),
|
||||
reverseFooter: true,
|
||||
confirmButtonOptions: {
|
||||
children: 'Cancel Subscription',
|
||||
children:
|
||||
t['com.affine.payment.ai.action.cancel.confirm.confirm-text'](),
|
||||
type: 'default',
|
||||
},
|
||||
cancelText: 'Keep AFFiNE AI',
|
||||
cancelText:
|
||||
t['com.affine.payment.ai.action.cancel.confirm.cancel-text'](),
|
||||
cancelButtonOptions: {
|
||||
type: 'primary',
|
||||
},
|
||||
@@ -45,11 +49,11 @@ export const AICancel = ({
|
||||
);
|
||||
},
|
||||
});
|
||||
}, [openConfirmModal, trigger, idempotencyKey, onSubscriptionUpdate]);
|
||||
}, [openConfirmModal, t, trigger, idempotencyKey, onSubscriptionUpdate]);
|
||||
|
||||
return (
|
||||
<Button onClick={cancel} loading={isMutating} type="primary" {...btnProps}>
|
||||
Cancel subscription
|
||||
{t['com.affine.payment.ai.action.cancel.button-label']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-1
@@ -1,11 +1,13 @@
|
||||
import { Button, type ButtonProps } from '@affine/component';
|
||||
import { authAtom } from '@affine/core/atoms';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { BaseActionProps } from '../types';
|
||||
|
||||
export const AILogin = (btnProps: BaseActionProps & ButtonProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
|
||||
const onClickSignIn = useCallback(() => {
|
||||
@@ -17,7 +19,7 @@ export const AILogin = (btnProps: BaseActionProps & ButtonProps) => {
|
||||
|
||||
return (
|
||||
<Button onClick={onClickSignIn} type="primary" {...btnProps}>
|
||||
Login
|
||||
{t['com.affine.payment.ai.action.login.button-label']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
+16
-7
@@ -7,6 +7,7 @@ import {
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useMutation } from '@affine/core/hooks/use-mutation';
|
||||
import { resumeSubscriptionMutation, SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { nanoid } from 'nanoid';
|
||||
@@ -20,6 +21,7 @@ export const AIResume = ({
|
||||
onSubscriptionUpdate,
|
||||
...btnProps
|
||||
}: AIResumeProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
|
||||
const { isMutating, trigger } = useMutation({
|
||||
@@ -29,13 +31,16 @@ export const AIResume = ({
|
||||
|
||||
const resume = useAsyncCallback(async () => {
|
||||
openConfirmModal({
|
||||
title: 'Resume Auto-Renewal?',
|
||||
title: t['com.affine.payment.ai.action.resume.confirm.title'](),
|
||||
description:
|
||||
'Are you sure you want to resume the subscription for AFFiNE AI? This means your payment method will be charged automatically at the end of each billing cycle, starting from the next billing cycle.',
|
||||
t['com.affine.payment.ai.action.resume.confirm.description'](),
|
||||
confirmButtonOptions: {
|
||||
children: 'Confirm',
|
||||
children:
|
||||
t['com.affine.payment.ai.action.resume.confirm.confirm-text'](),
|
||||
type: 'primary',
|
||||
},
|
||||
cancelText:
|
||||
t['com.affine.payment.ai.action.resume.confirm.cancel-text'](),
|
||||
onConfirm: async () => {
|
||||
await trigger(
|
||||
{ idempotencyKey, plan: SubscriptionPlan.AI },
|
||||
@@ -50,19 +55,23 @@ export const AIResume = ({
|
||||
color={cssVar('processingColor')}
|
||||
/>
|
||||
),
|
||||
title: 'Subscription Updated',
|
||||
message: 'You will be charged in the next billing cycle.',
|
||||
title:
|
||||
t[
|
||||
'com.affine.payment.ai.action.resume.confirm.notify.title'
|
||||
](),
|
||||
message:
|
||||
t['com.affine.payment.ai.action.resume.confirm.notify.msg'](),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}, [openConfirmModal, trigger, idempotencyKey, onSubscriptionUpdate]);
|
||||
}, [openConfirmModal, t, trigger, idempotencyKey, onSubscriptionUpdate]);
|
||||
|
||||
return (
|
||||
<Button loading={isMutating} onClick={resume} type="primary" {...btnProps}>
|
||||
Resume
|
||||
{t['com.affine.payment.ai.action.resume.button-label']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
+12
@@ -41,6 +41,10 @@ export const actionBlock = style({
|
||||
alignItems: 'start',
|
||||
marginBottom: 24,
|
||||
});
|
||||
export const actionButtons = style({
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
});
|
||||
export const purchaseButton = style({
|
||||
minWidth: 160,
|
||||
height: 37,
|
||||
@@ -50,6 +54,14 @@ export const purchaseButton = style({
|
||||
lineHeight: '14px',
|
||||
letterSpacing: '-1%',
|
||||
});
|
||||
export const learnAIButton = style([
|
||||
purchaseButton,
|
||||
{
|
||||
color: cssVar('textEmphasisColor'),
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
},
|
||||
]);
|
||||
export const agreement = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 400,
|
||||
|
||||
+21
-6
@@ -1,3 +1,4 @@
|
||||
import { Button } from '@affine/component';
|
||||
import {
|
||||
type SubscriptionMutator,
|
||||
useUserSubscription,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
import { AIPlanLayout } from '../layout';
|
||||
import * as styles from './ai-plan.css';
|
||||
@@ -19,6 +21,7 @@ interface AIPlanProps {
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
}
|
||||
export const AIPlan = ({ price, onSubscriptionUpdate }: AIPlanProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const recurring = SubscriptionRecurring.Yearly;
|
||||
|
||||
const { Action, billingTip } = useAffineAISubscription();
|
||||
@@ -35,25 +38,37 @@ export const AIPlan = ({ price, onSubscriptionUpdate }: AIPlanProps) => {
|
||||
|
||||
return (
|
||||
<AIPlanLayout
|
||||
title={t['com.affine.payment.ai.pricing-plan.title']()}
|
||||
caption={
|
||||
subscription
|
||||
? 'You have purchased AFFiNE AI'
|
||||
: 'You are current on the Basic plan.'
|
||||
? t['com.affine.payment.ai.pricing-plan.caption-purchased']()
|
||||
: t['com.affine.payment.ai.pricing-plan.caption-free']()
|
||||
}
|
||||
>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.titleBlock}>
|
||||
<section className={styles.titleCaption1}>
|
||||
Turn all your ideas into reality
|
||||
{t['com.affine.payment.ai.pricing-plan.title-caption-1']()}
|
||||
</section>
|
||||
<section className={styles.title}>
|
||||
{t['com.affine.payment.ai.pricing-plan.title']()}
|
||||
</section>
|
||||
<section className={styles.title}>AFFiNE AI</section>
|
||||
<section className={styles.titleCaption2}>
|
||||
A true multimodal AI copilot.
|
||||
{t['com.affine.payment.ai.pricing-plan.title-caption-2']()}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className={styles.actionBlock}>
|
||||
<Action {...baseActionProps} className={styles.purchaseButton} />
|
||||
<div className={styles.actionButtons}>
|
||||
<Action {...baseActionProps} className={styles.purchaseButton} />
|
||||
{subscription ? null : (
|
||||
<a href="https://ai.affine.pro" target="_blank" rel="noreferrer">
|
||||
<Button className={styles.learnAIButton}>
|
||||
{t['com.affine.payment.ai.pricing-plan.learn']()}
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{billingTip ? (
|
||||
<div className={styles.agreement}>{billingTip}</div>
|
||||
) : null}
|
||||
|
||||
+21
-17
@@ -1,38 +1,42 @@
|
||||
import { ShapeIcon } from '@blocksuite/icons';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CheckBoxCheckLinearIcon, PenIcon, TextIcon } from '@blocksuite/icons';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as styles from './ai-plan.css';
|
||||
|
||||
const benefits = [
|
||||
const benefitsGetter = (t: ReturnType<typeof useAFFiNEI18N>) => [
|
||||
{
|
||||
name: 'Write with you',
|
||||
icon: <ShapeIcon />,
|
||||
name: t['com.affine.payment.ai.benefit.g1'](),
|
||||
icon: <TextIcon />,
|
||||
items: [
|
||||
'Create quality content from sentences to articles on topics you need',
|
||||
'Rewrite like the professionals',
|
||||
'Change the tones / fix spelling & grammar',
|
||||
t['com.affine.payment.ai.benefit.g1-1'](),
|
||||
t['com.affine.payment.ai.benefit.g1-2'](),
|
||||
t['com.affine.payment.ai.benefit.g1-3'](),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Draw with you',
|
||||
icon: <ShapeIcon />,
|
||||
name: t['com.affine.payment.ai.benefit.g2'](),
|
||||
icon: <PenIcon />,
|
||||
items: [
|
||||
'Visualize your mind, magically',
|
||||
'Turn your outline into beautiful, engaging presentations',
|
||||
'Summarize your content into structured mind-map',
|
||||
t['com.affine.payment.ai.benefit.g2-1'](),
|
||||
t['com.affine.payment.ai.benefit.g2-2'](),
|
||||
t['com.affine.payment.ai.benefit.g2-3'](),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Plan with you',
|
||||
icon: <ShapeIcon />,
|
||||
name: t['com.affine.payment.ai.benefit.g3'](),
|
||||
icon: <CheckBoxCheckLinearIcon />,
|
||||
items: [
|
||||
'Memorize and tidy up your knowledge',
|
||||
'Auto-sorting and auto-tagging',
|
||||
'Open source & Privacy ensured',
|
||||
t['com.affine.payment.ai.benefit.g3-1'](),
|
||||
t['com.affine.payment.ai.benefit.g3-2'](),
|
||||
t['com.affine.payment.ai.benefit.g3-3'](),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const AIBenefits = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const benefits = useMemo(() => benefitsGetter(t), [t]);
|
||||
// TODO: responsive
|
||||
return (
|
||||
<div className={styles.benefits}>
|
||||
|
||||
+8
-2
@@ -2,6 +2,7 @@ import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-log
|
||||
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
|
||||
import { timestampToLocalDate } from '@affine/core/utils';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
import { AICancel, AILogin, AIResume, AISubscribe } from './actions';
|
||||
|
||||
@@ -10,6 +11,7 @@ const plan = SubscriptionPlan.AI;
|
||||
export type ActionType = 'login' | 'subscribe' | 'resume' | 'cancel';
|
||||
|
||||
export const useAffineAISubscription = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
||||
|
||||
const [subscription] = useUserSubscription(plan);
|
||||
@@ -31,9 +33,13 @@ export const useAffineAISubscription = () => {
|
||||
}[actionType];
|
||||
|
||||
const billingTip = subscription?.nextBillAt
|
||||
? `You have purchased AFFiNE AI. The next payment date is ${timestampToLocalDate(subscription.nextBillAt)}.`
|
||||
? t['com.affine.payment.ai.billing-tip.next-bill-at']({
|
||||
due: timestampToLocalDate(subscription.nextBillAt),
|
||||
})
|
||||
: subscription?.canceledAt && subscription.end
|
||||
? `You have purchased AFFiNE AI. The expiration date is ${timestampToLocalDate(subscription.end)}.`
|
||||
? t['com.affine.payment.ai.billing-tip.end-at']({
|
||||
end: timestampToLocalDate(subscription.end),
|
||||
})
|
||||
: null;
|
||||
|
||||
return { actionType, Action, billingTip };
|
||||
|
||||
+64
-49
@@ -1,9 +1,14 @@
|
||||
// TODO: we don't handle i18n for now
|
||||
// it's better to manage all equity at server side
|
||||
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { AfFiNeIcon } from '@blocksuite/icons';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { planTitleTitleCaption } from './style.css';
|
||||
|
||||
type T = ReturnType<typeof useAFFiNEI18N>;
|
||||
|
||||
export type Benefits = Record<
|
||||
string,
|
||||
Array<{
|
||||
@@ -11,6 +16,7 @@ export type Benefits = Record<
|
||||
title: ReactNode;
|
||||
}>
|
||||
>;
|
||||
type BenefitsGetter = (t: T) => Benefits;
|
||||
interface BasePrice {
|
||||
plan: SubscriptionPlan;
|
||||
name: string;
|
||||
@@ -37,49 +43,49 @@ export interface DynamicPrice extends BasePrice {
|
||||
) => ReactNode;
|
||||
}
|
||||
|
||||
const freeBenefits: Benefits = {
|
||||
'Include in FOSS': [
|
||||
{ title: 'Unlimited Local Workspaces.' },
|
||||
{ title: 'Unlimited use and Customization.' },
|
||||
{ title: 'Unlimited Doc and Edgeless editing.' },
|
||||
],
|
||||
'Include in Basic': [
|
||||
{ title: '10 GB of Cloud Storage.' },
|
||||
{ title: '10 MB of Maximum file size.' },
|
||||
{ title: 'Up to 3 members per Workspace.' },
|
||||
{ title: '7-days Cloud Time Machine file version history.' },
|
||||
{ title: 'Up to 3 login devices.' },
|
||||
],
|
||||
};
|
||||
const freeBenefits: BenefitsGetter = t => ({
|
||||
[t['com.affine.payment.cloud.free.benefit.g1']()]: ([1, 2, 3] as const).map(
|
||||
i => ({
|
||||
title: t[`com.affine.payment.cloud.free.benefit.g1-${i}`](),
|
||||
})
|
||||
),
|
||||
[t['com.affine.payment.cloud.free.benefit.g2']()]: (
|
||||
[1, 2, 3, 4, 5] as const
|
||||
).map(i => ({
|
||||
title: t[`com.affine.payment.cloud.free.benefit.g2-${i}`](),
|
||||
})),
|
||||
});
|
||||
|
||||
const proBenefits: Benefits = {
|
||||
'Include in Pro': [
|
||||
{ title: 'Everything in AFFiNE FOSS & Basic.', icon: <AfFiNeIcon /> },
|
||||
{ title: '100 GB of Cloud Storage.' },
|
||||
{ title: '100 MB of Maximum file size.' },
|
||||
{ title: 'Up to 10 members per Workspace.' },
|
||||
{ title: '30-days Cloud Time Machine file version history.' },
|
||||
{ title: 'Add comments on Doc and Edgeless.' },
|
||||
{ title: 'Community Support.' },
|
||||
{ title: 'Real-time Syncing & Collaboration for more people.' },
|
||||
const proBenefits: BenefitsGetter = t => ({
|
||||
[t['com.affine.payment.cloud.pro.benefit.g1']()]: [
|
||||
{
|
||||
title: t['com.affine.payment.cloud.pro.benefit.g1-1'](),
|
||||
icon: <AfFiNeIcon />,
|
||||
},
|
||||
...([2, 3, 4, 5, 6, 7, 8] as const).map(i => ({
|
||||
title: t[`com.affine.payment.cloud.pro.benefit.g1-${i}`](),
|
||||
})),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const teamBenefits: Benefits = {
|
||||
'Both in Team & Enterprise': [
|
||||
{ title: 'Everything in AFFiNE Pro.', icon: <AfFiNeIcon /> },
|
||||
{ title: 'Advanced Permission control, Page history and Review mode.' },
|
||||
{ title: 'Pay for seats, fits all team size.' },
|
||||
{ title: 'Email & Slack Support.' },
|
||||
const teamBenefits: BenefitsGetter = t => ({
|
||||
[t['com.affine.payment.cloud.team.benefit.g1']()]: [
|
||||
{
|
||||
title: t['com.affine.payment.cloud.team.benefit.g1-1'](),
|
||||
icon: <AfFiNeIcon />,
|
||||
},
|
||||
...([2, 3, 4] as const).map(i => ({
|
||||
title: t[`com.affine.payment.cloud.team.benefit.g1-${i}`](),
|
||||
})),
|
||||
],
|
||||
'Enterprise only': [
|
||||
{ title: 'SSO Authorization.' },
|
||||
{ title: 'Solutions & Best Practices for Dedicated needs.' },
|
||||
{ title: 'Embed-able & Integrations with IT support.' },
|
||||
[t['com.affine.payment.cloud.team.benefit.g2']()]: [
|
||||
{ title: t['com.affine.payment.cloud.team.benefit.g2-1']() },
|
||||
{ title: t['com.affine.payment.cloud.team.benefit.g2-2']() },
|
||||
{ title: t['com.affine.payment.cloud.team.benefit.g2-3']() },
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
export function getPlanDetail() {
|
||||
export function getPlanDetail(t: T) {
|
||||
return new Map<SubscriptionPlan, FixedPrice | DynamicPrice>([
|
||||
[
|
||||
SubscriptionPlan.Free,
|
||||
@@ -88,10 +94,10 @@ export function getPlanDetail() {
|
||||
plan: SubscriptionPlan.Free,
|
||||
price: '0',
|
||||
yearlyPrice: '0',
|
||||
name: 'FOSS + Basic',
|
||||
description: 'Open-Source under MIT license.',
|
||||
titleRenderer: () => 'Free forever',
|
||||
benefits: freeBenefits,
|
||||
name: t['com.affine.payment.cloud.free.name'](),
|
||||
description: t['com.affine.payment.cloud.free.description'](),
|
||||
titleRenderer: () => t['com.affine.payment.cloud.free.title'](),
|
||||
benefits: freeBenefits(t),
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -101,16 +107,25 @@ export function getPlanDetail() {
|
||||
plan: SubscriptionPlan.Pro,
|
||||
price: '1',
|
||||
yearlyPrice: '1',
|
||||
name: 'Pro',
|
||||
description: 'For family and small teams.',
|
||||
name: t['com.affine.payment.cloud.pro.name'](),
|
||||
description: t['com.affine.payment.cloud.pro.description'](),
|
||||
titleRenderer: (recurring, detail) => {
|
||||
const price =
|
||||
recurring === SubscriptionRecurring.Yearly
|
||||
? detail.yearlyPrice
|
||||
: detail.price;
|
||||
return `$${price} per month`;
|
||||
return (
|
||||
<>
|
||||
{t['com.affine.payment.cloud.pro.title.price-monthly']({ price })}
|
||||
{recurring === SubscriptionRecurring.Yearly ? (
|
||||
<span className={planTitleTitleCaption}>
|
||||
{t['com.affine.payment.cloud.pro.title.billed-yearly']()}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
},
|
||||
benefits: proBenefits,
|
||||
benefits: proBenefits(t),
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -119,10 +134,10 @@ export function getPlanDetail() {
|
||||
type: 'dynamic',
|
||||
plan: SubscriptionPlan.Team,
|
||||
contact: true,
|
||||
name: 'Team / Enterprise',
|
||||
description: 'Best for scalable teams.',
|
||||
titleRenderer: () => 'Contact Sales',
|
||||
benefits: teamBenefits,
|
||||
name: t['com.affine.payment.cloud.team.name'](),
|
||||
description: t['com.affine.payment.cloud.team.description'](),
|
||||
titleRenderer: () => t['com.affine.payment.cloud.team.title'](),
|
||||
benefits: teamBenefits(t),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
+15
-7
@@ -8,7 +8,7 @@ import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||
import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { FallbackProps } from 'react-error-boundary';
|
||||
|
||||
import { SWRErrorBoundary } from '../../../../../components/pure/swr-error-bundary';
|
||||
@@ -39,7 +39,7 @@ const Settings = () => {
|
||||
const [subscription, mutateSubscription] = useUserSubscription();
|
||||
|
||||
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
||||
const planDetail = getPlanDetail();
|
||||
const planDetail = useMemo(() => getPlanDetail(t), [t]);
|
||||
const scrollWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
@@ -138,15 +138,23 @@ const Settings = () => {
|
||||
<div className={styles.recurringToggleWrapper}>
|
||||
<div>
|
||||
{recurring === SubscriptionRecurring.Yearly ? (
|
||||
<div className={styles.recurringToggleRecurring}>Yearly</div>
|
||||
<div className={styles.recurringToggleRecurring}>
|
||||
{t['com.affine.payment.cloud.pricing-plan.toggle-yearly']()}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.recurringToggleRecurring}>
|
||||
<span>Billed Yearly</span>
|
||||
<span>
|
||||
{t[
|
||||
'com.affine.payment.cloud.pricing-plan.toggle-billed-yearly'
|
||||
]()}
|
||||
</span>
|
||||
</div>
|
||||
{yearlyDiscount ? (
|
||||
<div className={styles.recurringToggleDiscount}>
|
||||
Saving {yearlyDiscount}%
|
||||
{t['com.affine.payment.cloud.pricing-plan.toggle-discount']({
|
||||
discount: yearlyDiscount,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
@@ -201,8 +209,8 @@ const Settings = () => {
|
||||
|
||||
const cloudSelect = (
|
||||
<div className={styles.cloudSelect}>
|
||||
<b>Hosted by AFFiNE.Pro</b>
|
||||
<span>We host, no technical setup required.</span>
|
||||
<b>{t['com.affine.payment.cloud.pricing-plan.select.title']()}</b>
|
||||
<span>{t['com.affine.payment.cloud.pricing-plan.select.caption']()}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
+7
@@ -153,6 +153,13 @@ export const planTitleTitle = style({
|
||||
fontSize: cssVar('fontBase'),
|
||||
lineHeight: '20px',
|
||||
});
|
||||
export const planTitleTitleCaption = style({
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
marginLeft: 4,
|
||||
});
|
||||
export const planPriceWrapper = style({
|
||||
minHeight: '28px',
|
||||
lineHeight: 1,
|
||||
|
||||
@@ -82,6 +82,7 @@ export enum ServerDeploymentType {
|
||||
}
|
||||
|
||||
export enum ServerFeature {
|
||||
Copilot = 'Copilot',
|
||||
OAuth = 'OAuth',
|
||||
Payment = 'Payment',
|
||||
}
|
||||
|
||||
@@ -834,6 +834,82 @@
|
||||
"com.affine.pageMode.all": "all",
|
||||
"com.affine.pageMode.edgeless": "Edgeless",
|
||||
"com.affine.pageMode.page": "Page",
|
||||
"com.affine.payment.cloud.pricing-plan.select.title": "Hosted by AFFiNE.Pro",
|
||||
"com.affine.payment.cloud.pricing-plan.select.caption": "We host, no technical setup required.",
|
||||
"com.affine.payment.cloud.pricing-plan.toggle-yearly": "Yearly",
|
||||
"com.affine.payment.cloud.pricing-plan.toggle-billed-yearly": "Billed Yearly",
|
||||
"com.affine.payment.cloud.pricing-plan.toggle-discount": "Saving {{discount}}%",
|
||||
"com.affine.payment.cloud.free.name": "FOSS + Basic",
|
||||
"com.affine.payment.cloud.free.description": "Open-Source under MIT license.",
|
||||
"com.affine.payment.cloud.free.title": "Free forever",
|
||||
"com.affine.payment.cloud.free.benefit.g1": "Include in FOSS",
|
||||
"com.affine.payment.cloud.free.benefit.g1-1": "Unlimited Local Workspaces",
|
||||
"com.affine.payment.cloud.free.benefit.g1-2": "Unlimited use and Customization",
|
||||
"com.affine.payment.cloud.free.benefit.g1-3": "Unlimited Doc and Edgeless editing",
|
||||
"com.affine.payment.cloud.free.benefit.g2": "Include in Basic",
|
||||
"com.affine.payment.cloud.free.benefit.g2-1": "10 GB of Cloud Storage.",
|
||||
"com.affine.payment.cloud.free.benefit.g2-2": "10 MB of Maximum file size.",
|
||||
"com.affine.payment.cloud.free.benefit.g2-3": "Up to 3 members per Workspace.",
|
||||
"com.affine.payment.cloud.free.benefit.g2-4": "7-days Cloud Time Machine file version history.",
|
||||
"com.affine.payment.cloud.free.benefit.g2-5": "Up to 3 login devices.",
|
||||
"com.affine.payment.cloud.pro.name": "Pro",
|
||||
"com.affine.payment.cloud.pro.description": "For family and small teams.",
|
||||
"com.affine.payment.cloud.pro.title.price-monthly": "{{price}} per month",
|
||||
"com.affine.payment.cloud.pro.title.billed-yearly": "billed yearly",
|
||||
"com.affine.payment.cloud.pro.benefit.g1": "Include in Pro",
|
||||
"com.affine.payment.cloud.pro.benefit.g1-1": "Everything in AFFiNE FOSS & Basic.",
|
||||
"com.affine.payment.cloud.pro.benefit.g1-2": "100 GB of Cloud Storage.",
|
||||
"com.affine.payment.cloud.pro.benefit.g1-3": "100 MB of Maximum file size.",
|
||||
"com.affine.payment.cloud.pro.benefit.g1-4": "Up to 10 members per Workspace.",
|
||||
"com.affine.payment.cloud.pro.benefit.g1-5": "30-days Cloud Time Machine file version history.",
|
||||
"com.affine.payment.cloud.pro.benefit.g1-6": "Add comments on Doc and Edgeless.",
|
||||
"com.affine.payment.cloud.pro.benefit.g1-7": "Community Support.",
|
||||
"com.affine.payment.cloud.pro.benefit.g1-8": "Real-time Syncing & Collaboration for more people.",
|
||||
"com.affine.payment.cloud.team.name": "Team / Enterprise",
|
||||
"com.affine.payment.cloud.team.description": "Best for scalable teams.",
|
||||
"com.affine.payment.cloud.team.title": "Contact Sales",
|
||||
"com.affine.payment.cloud.team.benefit.g1": "Both in Team & Enterprise",
|
||||
"com.affine.payment.cloud.team.benefit.g1-1": "Everything in AFFiNE Pro.",
|
||||
"com.affine.payment.cloud.team.benefit.g1-2": "Advanced Permission control, Page history and Review mode.",
|
||||
"com.affine.payment.cloud.team.benefit.g1-3": "Pay for seats, fits all team size.",
|
||||
"com.affine.payment.cloud.team.benefit.g1-4": "Email & Slack Support.",
|
||||
"com.affine.payment.cloud.team.benefit.g2": "Enterprise only",
|
||||
"com.affine.payment.cloud.team.benefit.g2-1": "SSO Authorization.",
|
||||
"com.affine.payment.cloud.team.benefit.g2-2": "Solutions & Best Practices for Dedicated needs.",
|
||||
"com.affine.payment.cloud.team.benefit.g2-3": "Embed-able & Integrations with IT support.",
|
||||
"com.affine.payment.ai.pricing-plan.title-caption-1": "Turn all your ideas into reality",
|
||||
"com.affine.payment.ai.pricing-plan.title-caption-2": "A true multimodal AI copilot.",
|
||||
"com.affine.payment.ai.pricing-plan.title": "AFFiNE AI",
|
||||
"com.affine.payment.ai.pricing-plan.caption-purchased": "You have purchased AFFiNE AI",
|
||||
"com.affine.payment.ai.pricing-plan.caption-free": "You are current on the Basic plan.",
|
||||
"com.affine.payment.ai.pricing-plan.learn": "Learn About AFFiNE AI",
|
||||
"com.affine.payment.ai.billing-tip.next-bill-at": "You have purchased AFFiNE AI. The next payment date is {{due}}.",
|
||||
"com.affine.payment.ai.billing-tip.end-at": "You have purchased AFFiNE AI. The expiration date is {{end}}.",
|
||||
"com.affine.payment.ai.action.cancel.confirm.title": "Cancel Subscription",
|
||||
"com.affine.payment.ai.action.cancel.confirm.description": "If you end your subscription now, you can still use AFFiNE AI until the end of this billing period.",
|
||||
"com.affine.payment.ai.action.cancel.confirm.confirm-text": "Cancel Subscription",
|
||||
"com.affine.payment.ai.action.cancel.confirm.cancel-text": "Keep AFFiNE AI",
|
||||
"com.affine.payment.ai.action.cancel.button-label": "Cancel Subscription",
|
||||
"com.affine.payment.ai.action.login.button-label": "Login",
|
||||
"com.affine.payment.ai.action.resume.confirm.title": "Resume Auto-Renewal?",
|
||||
"com.affine.payment.ai.action.resume.confirm.description": "Are you sure you want to resume the subscription for AFFiNE AI? This means your payment method will be charged automatically at the end of each billing cycle, starting from the next billing cycle.",
|
||||
"com.affine.payment.ai.action.resume.confirm.confirm-text": "Confirm",
|
||||
"com.affine.payment.ai.action.resume.confirm.cancel-text": "Cancel",
|
||||
"com.affine.payment.ai.action.resume.confirm.notify.title": "Subscription Updated",
|
||||
"com.affine.payment.ai.action.resume.confirm.notify.msg": "You will be charged in the next billing cycle.",
|
||||
"com.affine.payment.ai.action.resume.button-label": "Resume",
|
||||
"com.affine.payment.ai.benefit.g1": "Write with you",
|
||||
"com.affine.payment.ai.benefit.g1-1": "Create quality content from sentences to articles on topics you need",
|
||||
"com.affine.payment.ai.benefit.g1-2": "Rewrite like the professionals",
|
||||
"com.affine.payment.ai.benefit.g1-3": "Change the tones / fix spelling & grammar",
|
||||
"com.affine.payment.ai.benefit.g2": "Draw with you",
|
||||
"com.affine.payment.ai.benefit.g2-1": "Visualize your mind, magically",
|
||||
"com.affine.payment.ai.benefit.g2-2": "Turn your outline into beautiful, engaging presentations",
|
||||
"com.affine.payment.ai.benefit.g2-3": "Summarize your content into structured mind-map",
|
||||
"com.affine.payment.ai.benefit.g3": "Plan with you",
|
||||
"com.affine.payment.ai.benefit.g3-1": "Memorize and tidy up your knowledge",
|
||||
"com.affine.payment.ai.benefit.g3-2": "Auto-sorting and auto-tagging",
|
||||
"com.affine.payment.ai.benefit.g3-3": "Open source & Privacy ensured",
|
||||
"com.affine.payment.benefit-1": "Unlimited local workspaces",
|
||||
"com.affine.payment.benefit-2": "Unlimited login devices",
|
||||
"com.affine.payment.benefit-3": "Unlimited blocks",
|
||||
|
||||
Reference in New Issue
Block a user