From edb6e0fd6905aa461ba5309c3700b0a58f8a8922 Mon Sep 17 00:00:00 2001 From: Cats Juice Date: Thu, 26 Oct 2023 22:00:14 +0800 Subject: [PATCH] feat(core): pricing plans actions (#4724) --- .../general-setting/plans/index.tsx | 89 +++- .../general-setting/plans/layout.tsx | 32 +- .../general-setting/plans/modals.tsx | 119 ++++++ .../general-setting/plans/plan-card.tsx | 383 +++++++++++++----- .../general-setting/plans/style.css.ts | 33 +- .../components/affine/setting-modal/index.tsx | 66 +-- .../affine/setting-modal/style.css.ts | 18 +- packages/frontend/i18n/src/resources/en.json | 45 +- 8 files changed, 607 insertions(+), 178 deletions(-) create mode 100644 packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx index 6cb38e898c..4067782bd4 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx @@ -1,10 +1,14 @@ import { RadioButton, RadioButtonGroup } from '@affine/component'; +import { pushNotificationAtom } from '@affine/component/notification-center'; import { pricesQuery, SubscriptionPlan, SubscriptionRecurring, } from '@affine/graphql'; +import { Trans } from '@affine/i18n'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useQuery } from '@affine/workspace/affine/gql'; +import { useSetAtom } from 'jotai'; import { Suspense, useEffect, useRef, useState } from 'react'; import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status'; @@ -14,10 +18,25 @@ import { type FixedPrice, getPlanDetail, PlanCard } from './plan-card'; import { PlansSkeleton } from './skeleton'; import * as styles from './style.css'; +const getRecurringLabel = ({ + recurring, + t, +}: { + recurring: SubscriptionRecurring; + t: ReturnType; +}) => { + return recurring === SubscriptionRecurring.Monthly + ? t['com.affine.payment.recurring-monthly']() + : t['com.affine.payment.recurring-yearly'](); +}; + const Settings = () => { + const t = useAFFiNEI18N(); const [subscription, mutateSubscription] = useUserSubscription(); + const pushNotification = useSetAtom(pushNotificationAtom); + const loggedIn = useCurrentLoginStatus() === 'authenticated'; - const planDetail = getPlanDetail(); + const planDetail = getPlanDetail(t); const scrollWrapper = useRef(null); const { @@ -44,6 +63,9 @@ const Settings = () => { ); const currentPlan = subscription?.plan ?? SubscriptionPlan.Free; + const isCanceled = !!subscription?.canceledAt; + const currentRecurring = + subscription?.recurring ?? SubscriptionRecurring.Monthly; const yearlyDiscount = ( planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined @@ -76,23 +98,39 @@ const Settings = () => { }, [recurring]); const subtitle = loggedIn ? ( -

- You are current on the {currentPlan} plan. If you have any questions, - please contact our {/*TODO: add action*/}customer support. -

+ isCanceled ? ( +

+ {t['com.affine.payment.subtitle-canceled']({ + plan: `${getRecurringLabel({ + recurring: currentRecurring, + t, + })} ${currentPlan}`, + })} +

+ ) : ( +

+ + You are current on the {{ currentPlan }} plan. If you have any + questions, please contact our  + + customer support + + . + +

+ ) ) : ( -

- This is the Pricing plans of AFFiNE Cloud. You can sign up or sign in to - your account first. -

+

{t['com.affine.payment.subtitle-not-signed-in']()}

); - const getRecurringLabel = (recurring: SubscriptionRecurring) => - ({ - [SubscriptionRecurring.Monthly]: 'Monthly', - [SubscriptionRecurring.Yearly]: 'Annually', - })[recurring]; - const tabs = ( { > {Object.values(SubscriptionRecurring).map(recurring => ( - {getRecurringLabel(recurring)} + {getRecurringLabel({ recurring, t })} {recurring === SubscriptionRecurring.Yearly && yearlyDiscount && ( - {yearlyDiscount}% off + {t['com.affine.payment.discount-amount']({ + amount: yearlyDiscount, + })} )} @@ -119,6 +159,21 @@ const Settings = () => { { + pushNotification({ + type: 'success', + title: t['com.affine.payment.updated-notify-title'](), + message: t['com.affine.payment.updated-notify-msg']({ + plan: + detail.plan === SubscriptionPlan.Free + ? SubscriptionPlan.Free + : getRecurringLabel({ + recurring: recurring as SubscriptionRecurring, + t, + }), + }), + }); + }} {...{ detail, subscription, recurring }} /> ); diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.tsx index 424e944cd0..89388f4066 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/layout.tsx @@ -1,4 +1,5 @@ import { SettingHeader } from '@affine/component/setting-components'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { ArrowRightBigIcon } from '@blocksuite/icons'; import type { HtmlHTMLAttributes, ReactNode } from 'react'; @@ -14,32 +15,37 @@ export interface PlanLayoutProps scrollRef?: React.RefObject; } -const SeeAllLink = () => ( - - See all plans - {} - -); +const SeeAllLink = () => { + const t = useAFFiNEI18N(); + + return ( + + {t['com.affine.payment.see-all-plans']()} + {} + + ); +}; export const PlanLayout = ({ subtitle, tabs, scroll, - title = 'Pricing Plans', + title, footer = , scrollRef, }: PlanLayoutProps) => { + const t = useAFFiNEI18N(); return (
{/* TODO: SettingHeader component shouldn't have margin itself */} {tabs} diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx new file mode 100644 index 0000000000..cdd208228e --- /dev/null +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx @@ -0,0 +1,119 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { DialogTrigger } from '@radix-ui/react-dialog'; +import { Button } from '@toeverything/components/button'; +import { + ConfirmModal, + type ConfirmModalProps, + Modal, +} from '@toeverything/components/modal'; +import { type ReactNode, useEffect, useRef } from 'react'; + +import * as styles from './style.css'; + +/** + * + * @param param0 + * @returns + */ +export const ConfirmLoadingModal = ({ + type, + loading, + open, + content, + onOpenChange, + onConfirm, + ...props +}: { + type: 'resume' | 'change'; + loading?: boolean; + content?: ReactNode; +} & ConfirmModalProps) => { + const t = useAFFiNEI18N(); + const confirmed = useRef(false); + + const title = t[`com.affine.payment.modal.${type}.title`](); + const confirmText = t[`com.affine.payment.modal.${type}.confirm`](); + const cancelText = t[`com.affine.payment.modal.${type}.cancel`](); + const contentText = + type !== 'change' ? t[`com.affine.payment.modal.${type}.content`]() : ''; + + useEffect(() => { + if (!loading && open && confirmed.current) { + onOpenChange?.(false); + confirmed.current = false; + } + }, [loading, open, onOpenChange]); + + return ( + { + confirmed.current = true; + onConfirm?.(); + }} + {...props} + > + {content ?? contentText} + + ); +}; + +/** + * Downgrade modal, confirm & cancel button are reversed + * @param param0 + */ +export const DowngradeModal = ({ + open, + onOpenChange, + onCancel, +}: { + loading?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + onCancel?: () => void; +}) => { + const t = useAFFiNEI18N(); + + return ( + +
+

+ {t['com.affine.payment.modal.downgrade.content']()} +

+

+ {t['com.affine.payment.modal.downgrade.caption']()} +

+
+ +
+ + + + +
+
+ ); +}; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx index 45c90ab68b..58b32f61c3 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx @@ -5,19 +5,32 @@ import type { import { cancelSubscriptionMutation, checkoutMutation, + resumeSubscriptionMutation, SubscriptionPlan, SubscriptionRecurring, updateSubscriptionMutation, } from '@affine/graphql'; +import { Trans } from '@affine/i18n'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useMutation } from '@affine/workspace/affine/gql'; import { DoneIcon } from '@blocksuite/icons'; import { Button } from '@toeverything/components/button'; +import { Tooltip } from '@toeverything/components/tooltip'; +import { useSetAtom } from 'jotai'; import { useAtom } from 'jotai'; -import { type PropsWithChildren, useCallback, useEffect, useRef } from 'react'; +import { + type PropsWithChildren, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { openPaymentDisableAtom } from '../../../../../atoms'; +import { authAtom } from '../../../../../atoms/index'; import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status'; import { BulledListIcon } from './icons/bulled-list'; +import { ConfirmLoadingModal, DowngradeModal } from './modals'; import * as styles from './style.css'; export interface FixedPrice { @@ -41,12 +54,13 @@ interface PlanCardProps { subscription?: Subscription | null; recurring: string; onSubscriptionUpdate: SubscriptionMutator; + onNotify: (info: { + detail: FixedPrice | DynamicPrice; + recurring: string; + }) => void; } -export function getPlanDetail() { - // const t = useAFFiNEI18N(); - - // TODO: i18n all things +export function getPlanDetail(t: ReturnType) { return new Map([ [ SubscriptionPlan.Free, @@ -56,12 +70,12 @@ export function getPlanDetail() { price: '0', yearlyPrice: '0', benefits: [ - 'Unlimited local workspace', - 'Unlimited login devices', - 'Unlimited blocks', - 'AFFiNE Cloud Storage 10GB', - 'The maximum file size is 10M', - 'Number of members per Workspace ≤ 3', + t['com.affine.payment.benefit-1'](), + t['com.affine.payment.benefit-2'](), + t['com.affine.payment.benefit-3'](), + t['com.affine.payment.benefit-4']({ capacity: '10GB' }), + t['com.affine.payment.benefit-5']({ capacity: '10M' }), + t['com.affine.payment.benefit-6']({ capacity: '3' }), ], }, ], @@ -73,12 +87,12 @@ export function getPlanDetail() { price: '1', yearlyPrice: '1', benefits: [ - 'Unlimited local workspace', - 'Unlimited login devices', - 'Unlimited blocks', - 'AFFiNE Cloud Storage 100GB', - 'The maximum file size is 500M', - 'Number of members per Workspace ≤ 10', + t['com.affine.payment.benefit-1'](), + t['com.affine.payment.benefit-2'](), + t['com.affine.payment.benefit-3'](), + t['com.affine.payment.benefit-4']({ capacity: '100GB' }), + t['com.affine.payment.benefit-5']({ capacity: '500M' }), + t['com.affine.payment.benefit-6']({ capacity: '10' }), ], }, ], @@ -89,9 +103,9 @@ export function getPlanDetail() { plan: SubscriptionPlan.Team, contact: true, benefits: [ - 'Best team workspace for collaboration and knowledge distilling.', - 'Focusing on what really matters with team project management and automation.', - 'Pay for seats, fits all team size.', + t['com.affine.payment.dynamic-benefit-1'](), + t['com.affine.payment.dynamic-benefit-2'](), + t['com.affine.payment.dynamic-benefit-3'](), ], }, ], @@ -102,20 +116,16 @@ export function getPlanDetail() { plan: SubscriptionPlan.Enterprise, contact: true, benefits: [ - 'Solutions & best practices for dedicated needs.', - 'Embedable & interrogations with IT support.', + t['com.affine.payment.dynamic-benefit-4'](), + t['com.affine.payment.dynamic-benefit-5'](), ], }, ], ]); } -export const PlanCard = ({ - detail, - subscription, - recurring, - onSubscriptionUpdate, -}: PlanCardProps) => { +export const PlanCard = (props: PlanCardProps) => { + const { detail, subscription, recurring } = props; const loggedIn = useCurrentLoginStatus() === 'authenticated'; const currentPlan = subscription?.plan ?? SubscriptionPlan.Free; const currentRecurring = subscription?.recurring; @@ -160,49 +170,7 @@ export const PlanCard = ({ )}

- { - // branches: - // if contact => 'Contact Sales' - // if not signed in: - // if free => 'Sign up free' - // else => 'Buy Pro' - // else - // if isCurrent => 'Current Plan' - // else if free => 'Downgrade' - // else if currentRecurring !== recurring => 'Change to {recurring} Billing' - // else => 'Upgrade' - // TODO: should replace with components with proper actions - detail.type === 'dynamic' ? ( - - ) : loggedIn ? ( - detail.plan === currentPlan && - (currentRecurring === recurring || - (!currentRecurring && detail.plan === SubscriptionPlan.Free)) ? ( - - ) : detail.plan === SubscriptionPlan.Free ? ( - - ) : currentRecurring !== recurring && - currentPlan === detail.plan ? ( - - ) : ( - - ) - ) : ( - - {detail.plan === SubscriptionPlan.Free - ? 'Sign up free' - : 'Buy Pro'} - - ) - } +
{detail.benefits.map((content, i) => ( @@ -226,15 +194,111 @@ export const PlanCard = ({ ); }; +const ActionButton = ({ + detail, + subscription, + recurring, + onSubscriptionUpdate, + onNotify, +}: PlanCardProps) => { + const t = useAFFiNEI18N(); + const loggedIn = useCurrentLoginStatus() === 'authenticated'; + const currentPlan = subscription?.plan ?? SubscriptionPlan.Free; + const currentRecurring = subscription?.recurring; + + const mutateAndNotify = useCallback( + (sub: Parameters[0]) => { + onSubscriptionUpdate?.(sub); + onNotify?.({ detail, recurring }); + }, + [onSubscriptionUpdate, onNotify, detail, recurring] + ); + + // branches: + // if contact => 'Contact Sales' + // if not signed in: + // if free => 'Sign up free' + // else => 'Buy Pro' + // else + // if isCurrent + // if canceled => 'Resume' + // else => 'Current Plan' + // if isCurrent => 'Current Plan' + // else if free => 'Downgrade' + // else if currentRecurring !== recurring => 'Change to {recurring} Billing' + // else => 'Upgrade' + + // contact + if (detail.type === 'dynamic') { + return ; + } + + // not signed in + if (!loggedIn) { + return ( + + {detail.plan === SubscriptionPlan.Free + ? t['com.affine.payment.sign-up-free']() + : t['com.affine.payment.buy-pro']()} + + ); + } + + const isCanceled = !!subscription?.canceledAt; + const isFree = detail.plan === SubscriptionPlan.Free; + const isCurrent = + detail.plan === currentPlan && + (isFree ? true : currentRecurring === recurring); + + // is current + if (isCurrent) { + return isCanceled ? ( + + ) : ( + + ); + } + + if (isFree) { + return ( + + ); + } + + return currentPlan === detail.plan ? ( + + ) : ( + + ); +}; + const CurrentPlan = () => { - return ; + const t = useAFFiNEI18N(); + return ( + + ); }; const Downgrade = ({ + disabled, onSubscriptionUpdate, }: { + disabled?: boolean; onSubscriptionUpdate: SubscriptionMutator; }) => { + const t = useAFFiNEI18N(); + const [open, setOpen] = useState(false); const { isMutating, trigger } = useMutation({ mutation: cancelSubscriptionMutation, }); @@ -247,25 +311,43 @@ const Downgrade = ({ }); }, [trigger, onSubscriptionUpdate]); + const tooltipContent = disabled + ? t['com.affine.payment.downgraded-tooltip']() + : null; + return ( - + <> + +
+ +
+
+ + ); }; const ContactSales = () => { + const t = useAFFiNEI18N(); return ( - // TODO: add action - + + + ); }; @@ -276,6 +358,7 @@ const Upgrade = ({ recurring: SubscriptionRecurring; onSubscriptionUpdate: SubscriptionMutator; }) => { + const t = useAFFiNEI18N(); const { isMutating, trigger } = useMutation({ mutation: checkoutMutation, }); @@ -330,27 +413,35 @@ const Upgrade = ({ }, [onClose]); return ( - + <> + + ); }; const ChangeRecurring = ({ - from: _from /* TODO: from can be useful when showing confirmation modal */, + from, to, + disabled, + due, onSubscriptionUpdate, }: { from: SubscriptionRecurring; to: SubscriptionRecurring; + disabled?: boolean; + due: string; onSubscriptionUpdate: SubscriptionMutator; }) => { + const t = useAFFiNEI18N(); + const [open, setOpen] = useState(false); const { isMutating, trigger } = useMutation({ mutation: updateSubscriptionMutation, }); @@ -366,24 +457,102 @@ const ChangeRecurring = ({ ); }, [trigger, onSubscriptionUpdate, to]); + const changeCurringContent = ( + + You are changing your {from}{' '} + subscription to {to}{' '} + subscription. This change will take effect in the next billing cycle, with + an effective date of {due}. + + ); + return ( - + <> + + + + ); }; -const SignupAction = ({ children }: PropsWithChildren) => { - // TODO: add login action +const SignUpAction = ({ children }: PropsWithChildren) => { + const setOpen = useSetAtom(authAtom); + + const onClickSignIn = useCallback(async () => { + setOpen(state => ({ + ...state, + openModal: true, + })); + }, [setOpen]); + return ( - ); }; + +const ResumeAction = ({ + onSubscriptionUpdate, +}: { + onSubscriptionUpdate: SubscriptionMutator; +}) => { + const t = useAFFiNEI18N(); + const [open, setOpen] = useState(false); + const [hovered, setHovered] = useState(false); + const { isMutating, trigger } = useMutation({ + mutation: resumeSubscriptionMutation, + }); + + const resume = useCallback(() => { + trigger(null, { + onSuccess: data => { + onSubscriptionUpdate(data.resumeSubscription); + }, + }); + }, [trigger, onSubscriptionUpdate]); + + return ( + <> + + + + + ); +}; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts index 4189024ff9..a1c5ac5abb 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts @@ -14,7 +14,7 @@ export const radioButtonDiscount = style({ }); export const planCardsWrapper = style({ - paddingRight: 'calc(var(--setting-modal-gap-x))', + paddingRight: 'calc(var(--setting-modal-gap-x) + 30px)', display: 'flex', gap: '16px', width: 'fit-content', @@ -116,3 +116,34 @@ export const planBenefitText = style({ flexDirection: 'column', alignItems: 'center', }); + +export const downgradeContentWrapper = style({ + padding: '12px 0 20px 0px', + display: 'flex', + flexDirection: 'column', + gap: '12px', +}); + +export const downgradeContent = style({ + fontSize: '15px', + lineHeight: '24px', + fontWeight: 400, + color: 'var(--affine-text-primary-color)', +}); + +export const downgradeCaption = style({ + fontSize: '14px', + lineHeight: '22px', + color: 'var(--affine-text-secondary-color)', +}); + +export const downgradeFooter = style({ + display: 'flex', + justifyContent: 'flex-end', + gap: '20px', + paddingTop: '20px', +}); + +export const textEmphasis = style({ + color: 'var(--affine-text-emphasis-color)', +}); diff --git a/packages/frontend/core/src/components/affine/setting-modal/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/index.tsx index dbac104bd7..7ed213a44e 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/index.tsx @@ -39,6 +39,7 @@ export const SettingModal = ({ const generalSettingList = useGeneralSettingList(); const modalContentRef = useRef(null); + const modalContentWrapperRef = useRef(null); useEffect(() => { if (!modalProps.open) return; @@ -46,19 +47,18 @@ export const SettingModal = ({ const onResize = debounce(() => { cancelAnimationFrame(animationFrameId); animationFrameId = requestAnimationFrame(() => { - if (!modalContentRef.current) return; + if (!modalContentRef.current || !modalContentWrapperRef.current) return; + const wrapperWidth = modalContentWrapperRef.current.offsetWidth; const contentWidth = modalContentRef.current.offsetWidth; - const computedStyle = window.getComputedStyle(modalContentRef.current); - const marginX = parseInt(computedStyle.marginLeft, 10); - const paddingX = parseInt(computedStyle.paddingLeft, 10); + modalContentRef.current?.style.setProperty( '--setting-modal-width', - `${contentWidth + marginX * 2}px` + `${wrapperWidth}px` ); modalContentRef.current?.style.setProperty( '--setting-modal-gap-x', - `${marginX + paddingX}px` + `${(wrapperWidth - contentWidth) / 2}px` ); }); }, 200); @@ -121,33 +121,35 @@ export const SettingModal = ({
-
- {activeTab === 'workspace' && workspaceId ? ( - }> - - - ) : null} - {generalSettingList.find(v => v.key === activeTab) ? ( - - ) : null} - {activeTab === 'account' && loginStatus === 'authenticated' ? ( - - ) : null} -
-
- - - - - {t['com.affine.settings.suggestion']()} - +
+
+ {activeTab === 'workspace' && workspaceId ? ( + }> + + + ) : null} + {generalSettingList.find(v => v.key === activeTab) ? ( + + ) : null} + {activeTab === 'account' && loginStatus === 'authenticated' ? ( + + ) : null} +
+
diff --git a/packages/frontend/core/src/components/affine/setting-modal/style.css.ts b/packages/frontend/core/src/components/affine/setting-modal/style.css.ts index 9ba1ab1c48..786f42111e 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/style.css.ts +++ b/packages/frontend/core/src/components/affine/setting-modal/style.css.ts @@ -3,21 +3,25 @@ import { style } from '@vanilla-extract/css'; export const wrapper = style({ flexGrow: '1', height: '100%', - maxWidth: '560px', - margin: '0 auto', - padding: '40px 15px 20px 15px', - // children + // margin: '0 auto', + padding: '40px 15px 20px 15px', + overflowX: 'hidden', + overflowY: 'auto', + display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - alignItems: 'center', + justifyContent: 'center', '::-webkit-scrollbar': { display: 'none', }, }); +export const centerContainer = style({ + width: '100%', + maxWidth: '560px', +}); + export const content = style({ width: '100%', marginBottom: '24px', diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 4ae0bfd809..d8b0c50c4d 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -642,9 +642,52 @@ "com.affine.auth.sign-out.confirm-modal.description": "After signing out, the Cloud Workspaces associated with this account will be removed from the current device, and signing in again will add them back.", "com.affine.auth.sign-out.confirm-modal.cancel": "Cancel", "com.affine.auth.sign-out.confirm-modal.confirm": "Sign Out", + "com.affine.payment.recurring-yearly": "Annually", + "com.affine.payment.recurring-monthly": "Monthly", + "com.affine.payment.title": "Pricing Plans", + "com.affine.payment.subtitle-not-signed-in": "This is the Pricing plans of AFFiNE Cloud. You can sign up or sign in to your account first.", + "com.affine.payment.subtitle-active": "You are current on the {{currentPlan}} plan. If you have any questions, please contact our <3>customer support.", + "com.affine.payment.subtitle-canceled": "You are currently on the {{plan}} plan. After the current billing period ends, your account will automatically switch to the Free plan.", + "com.affine.payment.discount-amount": "{{amount}}% off", + "com.affine.payment.sign-up-free": "Sign up free", + "com.affine.payment.buy-pro": "Buy Pro", + "com.affine.payment.current-plan": "Current Plan", + "com.affine.payment.downgrade": "Downgrade", + "com.affine.payment.upgrade": "Upgrade", + "com.affine.payment.downgraded-tooltip": "You have successfully downgraded. After the current billing period ends, your account will automatically switch to the Free plan.", + "com.affine.payment.contact-sales": "Contact Sales", + "com.affine.payment.change-to": "Change to {{to}} Billing", + "com.affine.payment.resume": "Resume", + "com.affine.payment.resume-renewal": "Resume Auto-renewal", + "com.affine.payment.benefit-1": "Unlimited local workspace", + "com.affine.payment.benefit-2": "Unlimited login devices", + "com.affine.payment.benefit-3": "Unlimited blocks", + "com.affine.payment.benefit-4": "AFFiNE Cloud Storage {{capacity}}", + "com.affine.payment.benefit-5": "The maximum file size is {{capacity}}", + "com.affine.payment.benefit-6": "Number of members per Workspace ≤ {{capacity}}", + "com.affine.payment.dynamic-benefit-1": "Best team workspace for collaboration and knowledge distilling.", + "com.affine.payment.dynamic-benefit-2": "Focusing on what really matters with team project management and automation.", + "com.affine.payment.dynamic-benefit-3": "Pay for seats, fits all team size.", + "com.affine.payment.dynamic-benefit-4": "Solutions & best practices for dedicated needs.", + "com.affine.payment.dynamic-benefit-5": "Embedable & interrogations with IT support.", + "com.affine.payment.see-all-plans": "See all plans", + "com.affine.payment.modal.resume.title": "Resume Auto-Renewal?", + "com.affine.payment.modal.resume.content": "Are you sure you want to resume the subscription for your pro account? 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.modal.resume.cancel": "Cancel", + "com.affine.payment.modal.resume.confirm": "Confirm", + "com.affine.payment.modal.downgrade.title": "Are you sure?", + "com.affine.payment.modal.downgrade.content": "We're sorry to see you go, but we're always working to improve, and your feedback is welcome. We hope to see you return in the future.", + "com.affine.payment.modal.downgrade.caption": "You can still use AFFiNE Cloud Pro until the end of this billing period :)", + "com.affine.payment.modal.downgrade.cancel": "Cancel Subscription", + "com.affine.payment.modal.downgrade.confirm": "Keep AFFiNE Cloud Pro", + "com.affine.payment.modal.change.title": "Change your subscription", + "com.affine.payment.modal.change.content": "You are changing your <0>from subscription to <1>to subscription. This change will take effect in the next billing cycle, with an effective date of <2>due.", + "com.affine.payment.modal.change.cancel": "Cancel", + "com.affine.payment.modal.change.confirm": "Change", + "com.affine.payment.updated-notify-title": "Subscription updated", + "com.affine.payment.updated-notify-msg": "You have changed your plan to {{plan}} billing.", "com.affine.storage.maximum-tips": "You have reached the maximum capacity limit for your current account", "com.affine.payment.tag-tooltips": "See all plans", - "com.affine.payment.title": "Pricing Plans", "com.affine.payment.billing-setting.title": "Billing", "com.affine.payment.billing-setting.subtitle": "Manage your billing information and invoices.", "com.affine.payment.billing-setting.information": "Information",