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