mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 18:16:15 +08:00
feat(core): add translation key for payment (#4723)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
@@ -11,7 +12,12 @@ export interface StorageProgressProgress {
|
||||
max: number;
|
||||
value: number;
|
||||
onUpgrade: () => void;
|
||||
plan: string;
|
||||
plan: SubscriptionPlan;
|
||||
}
|
||||
|
||||
enum ButtonType {
|
||||
Primary = 'primary',
|
||||
Default = 'default',
|
||||
}
|
||||
|
||||
export const StorageProgress = ({
|
||||
@@ -30,10 +36,10 @@ export const StorageProgress = ({
|
||||
const max = useMemo(() => bytes.format(upperLimit), [upperLimit]);
|
||||
|
||||
const buttonType = useMemo(() => {
|
||||
if (plan === 'Free') {
|
||||
return 'primary';
|
||||
if (plan === SubscriptionPlan.Free) {
|
||||
return ButtonType.Primary;
|
||||
}
|
||||
return 'default';
|
||||
return ButtonType.Default;
|
||||
}, [plan]);
|
||||
|
||||
return (
|
||||
@@ -43,7 +49,7 @@ export const StorageProgress = ({
|
||||
<span>{t['com.affine.storage.used.hint']()}</span>
|
||||
<span>
|
||||
{used}/{max}
|
||||
{` (${plan} Plan)`}
|
||||
{` (${plan} ${t['com.affine.storage.plan']()})`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -59,9 +65,7 @@ export const StorageProgress = ({
|
||||
|
||||
<Tooltip
|
||||
options={{ hidden: percent < 100 }}
|
||||
content={
|
||||
'You have reached the maximum capacity limit for your current account'
|
||||
}
|
||||
content={t['com.affine.storage.maximum-tips']()}
|
||||
>
|
||||
<span tabIndex={0}>
|
||||
<Button
|
||||
@@ -69,7 +73,9 @@ export const StorageProgress = ({
|
||||
onClick={onUpgrade}
|
||||
className={styles.storageButton}
|
||||
>
|
||||
{plan === 'Free' ? 'Upgrade' : 'Change'}
|
||||
{plan === 'Free'
|
||||
? t['com.affine.storage.upgrade']()
|
||||
: t['com.affine.storage.change-plan']()}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import Tooltip from '@toeverything/components/tooltip';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -24,8 +25,10 @@ export const UserPlanButton = () => {
|
||||
[setSettingModalAtom]
|
||||
);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<Tooltip content={'See all plans'} side="top">
|
||||
<Tooltip content={t['com.affine.payment.tag-tooltips']()} side="top">
|
||||
<div className={styles.userPlanButton} onClick={handleClick}>
|
||||
{plan}
|
||||
</div>
|
||||
|
||||
@@ -157,6 +157,7 @@ const StoragePanel = () => {
|
||||
|
||||
const [subscription] = useUserSubscription();
|
||||
const plan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
|
||||
const maxLimit = useMemo(() => {
|
||||
return bytes.parse(plan === SubscriptionPlan.Free ? '10GB' : '100GB');
|
||||
}, [plan]);
|
||||
|
||||
+169
-81
@@ -15,11 +15,13 @@ import {
|
||||
SubscriptionRecurring,
|
||||
SubscriptionStatus,
|
||||
} from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
import { Suspense, useCallback, useMemo } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../../../atoms';
|
||||
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
|
||||
@@ -29,8 +31,25 @@ import {
|
||||
} from '../../../../../hooks/use-subscription';
|
||||
import * as styles from './style.css';
|
||||
|
||||
enum DescriptionI18NKey {
|
||||
Basic = 'com.affine.payment.billing-setting.current-plan.description',
|
||||
Monthly = 'com.affine.payment.billing-setting.current-plan.description.monthly',
|
||||
Yearly = 'com.affine.payment.billing-setting.current-plan.description.yearly',
|
||||
}
|
||||
|
||||
const getMessageKey = (
|
||||
plan: SubscriptionPlan,
|
||||
recurring: SubscriptionRecurring
|
||||
): DescriptionI18NKey => {
|
||||
if (plan !== SubscriptionPlan.Pro) {
|
||||
return DescriptionI18NKey.Basic;
|
||||
}
|
||||
return DescriptionI18NKey[recurring];
|
||||
};
|
||||
|
||||
export const BillingSettings = () => {
|
||||
const status = useCurrentLoginStatus();
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
if (status !== 'authenticated') {
|
||||
return null;
|
||||
@@ -39,18 +58,22 @@ export const BillingSettings = () => {
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title="Billing"
|
||||
subtitle="Manage your billing information and invoices."
|
||||
title={t['com.affine.payment.billing-setting.title']()}
|
||||
subtitle={t['com.affine.payment.billing-setting.subtitle']()}
|
||||
/>
|
||||
{/* TODO: loading fallback */}
|
||||
<Suspense>
|
||||
<SettingWrapper title="Information">
|
||||
<SettingWrapper
|
||||
title={t['com.affine.payment.billing-setting.information']()}
|
||||
>
|
||||
<SubscriptionSettings />
|
||||
</SettingWrapper>
|
||||
</Suspense>
|
||||
{/* TODO: loading fallback */}
|
||||
<Suspense>
|
||||
<SettingWrapper title="Billing history">
|
||||
<SettingWrapper
|
||||
title={t['com.affine.payment.billing-setting.history']()}
|
||||
>
|
||||
<BillingHistory />
|
||||
</SettingWrapper>
|
||||
</Suspense>
|
||||
@@ -74,75 +97,11 @@ const SubscriptionSettings = () => {
|
||||
: price
|
||||
? recurring === SubscriptionRecurring.Monthly
|
||||
? String(price.amount / 100)
|
||||
: (price.yearlyAmount / 100 / 12).toFixed(2)
|
||||
: String(price.yearlyAmount / 100)
|
||||
: '?';
|
||||
|
||||
return (
|
||||
<div className={styles.subscription}>
|
||||
<div className={styles.planCard}>
|
||||
<div className={styles.currentPlan}>
|
||||
<SettingRow
|
||||
spreadCol={false}
|
||||
name="Current Plan"
|
||||
desc={
|
||||
<p>
|
||||
You are current on the{' '}
|
||||
<a>
|
||||
{/* TODO: Action */}
|
||||
{plan} plan
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<PlanAction plan={plan} />
|
||||
</div>
|
||||
<p className={styles.planPrice}>${amount}/month</p>
|
||||
</div>
|
||||
{subscription?.status === SubscriptionStatus.Active && (
|
||||
<>
|
||||
<SettingRow
|
||||
className={styles.paymentMethod}
|
||||
name="Payment Method"
|
||||
desc="Provided by Stripe."
|
||||
>
|
||||
<PaymentMethodUpdater />
|
||||
</SettingRow>
|
||||
{subscription.nextBillAt && (
|
||||
<SettingRow
|
||||
name="Renew Date"
|
||||
desc={`Next billing date: ${new Date(
|
||||
subscription.nextBillAt
|
||||
).toLocaleDateString()}`}
|
||||
/>
|
||||
)}
|
||||
{subscription.canceledAt ? (
|
||||
<SettingRow
|
||||
name="Expiration Date"
|
||||
desc={`Your subscription is valid until ${new Date(
|
||||
subscription.end
|
||||
).toLocaleDateString()}`}
|
||||
>
|
||||
<ResumeSubscription onSubscriptionUpdate={mutateSubscription} />
|
||||
</SettingRow>
|
||||
) : (
|
||||
<SettingRow
|
||||
className="dangerous-setting"
|
||||
name="Cancel Subscription"
|
||||
desc={`Subscription cancelled, your pro account will expire on ${new Date(
|
||||
subscription.end
|
||||
).toLocaleDateString()}`}
|
||||
>
|
||||
<CancelSubscription onSubscriptionUpdate={mutateSubscription} />
|
||||
</SettingRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const PlanAction = ({ plan }: { plan: string }) => {
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const gotoPlansSetting = useCallback(() => {
|
||||
@@ -153,13 +112,120 @@ const PlanAction = ({ plan }: { plan: string }) => {
|
||||
});
|
||||
}, [setOpenSettingModalAtom]);
|
||||
|
||||
const currentPlanDesc = useMemo(() => {
|
||||
const messageKey = getMessageKey(plan, recurring);
|
||||
return (
|
||||
<Trans
|
||||
i18nKey={messageKey}
|
||||
values={{
|
||||
planName: plan,
|
||||
}}
|
||||
components={{
|
||||
1: (
|
||||
<span
|
||||
onClick={gotoPlansSetting}
|
||||
className={styles.currentPlanName}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [plan, recurring, gotoPlansSetting]);
|
||||
|
||||
return (
|
||||
<div className={styles.subscription}>
|
||||
<div className={styles.planCard}>
|
||||
<div className={styles.currentPlan}>
|
||||
<SettingRow
|
||||
spreadCol={false}
|
||||
name={t['com.affine.payment.billing-setting.current-plan']()}
|
||||
desc={currentPlanDesc}
|
||||
/>
|
||||
<PlanAction plan={plan} gotoPlansSetting={gotoPlansSetting} />
|
||||
</div>
|
||||
<p className={styles.planPrice}>
|
||||
${amount}
|
||||
<span className={styles.billingFrequency}>
|
||||
/
|
||||
{recurring === SubscriptionRecurring.Monthly
|
||||
? t['com.affine.payment.billing-setting.month']()
|
||||
: t['com.affine.payment.billing-setting.year']()}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{subscription?.status === SubscriptionStatus.Active && (
|
||||
<>
|
||||
<SettingRow
|
||||
className={styles.paymentMethod}
|
||||
name={t['com.affine.payment.billing-setting.payment-method']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.payment-method.description'
|
||||
]()}
|
||||
>
|
||||
<PaymentMethodUpdater />
|
||||
</SettingRow>
|
||||
{subscription.nextBillAt && (
|
||||
<SettingRow
|
||||
name={t['com.affine.payment.billing-setting.renew-date']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.renew-date.description'
|
||||
]({
|
||||
renewDate: new Date(
|
||||
subscription.nextBillAt
|
||||
).toLocaleDateString(),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{subscription.canceledAt ? (
|
||||
<SettingRow
|
||||
name={t['com.affine.payment.billing-setting.expiration-date']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.expiration-date.description'
|
||||
]({
|
||||
expirationDate: new Date(subscription.end).toLocaleDateString(),
|
||||
})}
|
||||
>
|
||||
<ResumeSubscription onSubscriptionUpdate={mutateSubscription} />
|
||||
</SettingRow>
|
||||
) : (
|
||||
<SettingRow
|
||||
className="dangerous-setting"
|
||||
name={t[
|
||||
'com.affine.payment.billing-setting.cancel-subscription'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.cancel-subscription.description'
|
||||
]({
|
||||
cancelDate: new Date(subscription.end).toLocaleDateString(),
|
||||
})}
|
||||
>
|
||||
<CancelSubscription onSubscriptionUpdate={mutateSubscription} />
|
||||
</SettingRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PlanAction = ({
|
||||
plan,
|
||||
gotoPlansSetting,
|
||||
}: {
|
||||
plan: string;
|
||||
gotoPlansSetting: () => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.planAction}
|
||||
type="primary"
|
||||
onClick={gotoPlansSetting}
|
||||
>
|
||||
{plan === SubscriptionPlan.Free ? 'Upgrade' : 'Change Plan'}
|
||||
{plan === SubscriptionPlan.Free
|
||||
? t['com.affine.payment.billing-setting.upgrade']()
|
||||
: t['com.affine.payment.billing-setting.change-plan']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -169,6 +235,7 @@ const PaymentMethodUpdater = () => {
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: createCustomerPortalMutation,
|
||||
});
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const update = useCallback(() => {
|
||||
trigger(null, {
|
||||
@@ -179,8 +246,13 @@ const PaymentMethodUpdater = () => {
|
||||
}, [trigger]);
|
||||
|
||||
return (
|
||||
<Button onClick={update} loading={isMutating} disabled={isMutating}>
|
||||
Update
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={update}
|
||||
loading={isMutating}
|
||||
disabled={isMutating}
|
||||
>
|
||||
{t['com.affine.payment.billing-setting.upgrade']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -190,6 +262,7 @@ const ResumeSubscription = ({
|
||||
}: {
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: resumeSubscriptionMutation,
|
||||
});
|
||||
@@ -203,8 +276,13 @@ const ResumeSubscription = ({
|
||||
}, [trigger, onSubscriptionUpdate]);
|
||||
|
||||
return (
|
||||
<Button onClick={resume} loading={isMutating} disabled={isMutating}>
|
||||
Resume
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={resume}
|
||||
loading={isMutating}
|
||||
disabled={isMutating}
|
||||
>
|
||||
{t['com.affine.payment.billing-setting.resume-subscription']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -237,6 +315,7 @@ const CancelSubscription = ({
|
||||
};
|
||||
|
||||
const BillingHistory = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { data: invoicesQueryResult } = useQuery({
|
||||
query: invoicesQuery,
|
||||
variables: {
|
||||
@@ -250,7 +329,9 @@ const BillingHistory = () => {
|
||||
return (
|
||||
<div className={styles.billingHistory}>
|
||||
{invoices.length === 0 ? (
|
||||
<p className={styles.noInvoice}>There are no invoices to display.</p>
|
||||
<p className={styles.noInvoice}>
|
||||
{t['com.affine.payment.billing-setting.no-invoice']()}
|
||||
</p>
|
||||
) : (
|
||||
// TODO: pagination
|
||||
invoices.map(invoice => (
|
||||
@@ -266,21 +347,28 @@ const InvoiceLine = ({
|
||||
}: {
|
||||
invoice: NonNullable<InvoicesQuery['currentUser']>['invoices'][0];
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const open = useCallback(() => {
|
||||
if (invoice.link) {
|
||||
window.open(invoice.link, '_blank', 'noopener noreferrer');
|
||||
}
|
||||
}, [invoice.link]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
key={invoice.id}
|
||||
name={new Date(invoice.createdAt).toLocaleDateString()}
|
||||
// TODO: currency to format: usd => $, cny => ¥
|
||||
desc={`${invoice.status === InvoiceStatus.Paid ? 'Paid' : ''} $${
|
||||
invoice.amount / 100
|
||||
}`}
|
||||
desc={`${
|
||||
invoice.status === InvoiceStatus.Paid
|
||||
? t['com.affine.payment.billing-setting.paid']()
|
||||
: ''
|
||||
} $${invoice.amount / 100}`}
|
||||
>
|
||||
<Button onClick={open}>View Invoice</Button>
|
||||
<Button className={styles.button} onClick={open}>
|
||||
{t['com.affine.payment.billing-setting.view-invoice']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
+14
@@ -25,6 +25,10 @@ export const planPrice = style({
|
||||
fontWeight: 600,
|
||||
});
|
||||
|
||||
export const billingFrequency = style({
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
});
|
||||
|
||||
export const paymentMethod = style({
|
||||
marginTop: '24px',
|
||||
});
|
||||
@@ -37,3 +41,13 @@ export const noInvoice = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
});
|
||||
|
||||
export const currentPlanName = style({
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: 500,
|
||||
color: 'var(--affine-text-emphasis-color)',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
export const button = style({
|
||||
padding: '4px 12px',
|
||||
});
|
||||
|
||||
+2
-4
@@ -51,8 +51,7 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
},
|
||||
{
|
||||
key: 'plans',
|
||||
// TODO: i18n
|
||||
title: 'AFFiNE Cloud Plans',
|
||||
title: t['com.affine.payment.title'](),
|
||||
// TODO: icon
|
||||
icon: KeyboardIcon,
|
||||
testId: 'plans-panel-trigger',
|
||||
@@ -75,8 +74,7 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
if (status === 'authenticated') {
|
||||
settings.splice(3, 0, {
|
||||
key: 'billing',
|
||||
// TODO: i18n
|
||||
title: 'Billing',
|
||||
title: t['com.affine.payment.billing-setting.title'](),
|
||||
// TODO: icon
|
||||
icon: KeyboardIcon,
|
||||
testId: 'billing-panel-trigger',
|
||||
|
||||
@@ -333,7 +333,9 @@
|
||||
"com.affine.storage.extend.hint": "The usage has reached its maximum capacity, AFFiNE Cloud is currently in early access phase and is not supported for upgrading, please be patient and wait for our pricing plan. ",
|
||||
"com.affine.storage.extend.link": "To get more information click here.",
|
||||
"com.affine.storage.title": "AFFiNE Cloud Storage",
|
||||
"com.affine.storage.upgrade": "Upgrade to Pro",
|
||||
"com.affine.storage.upgrade": "Upgrade",
|
||||
"com.affine.storage.change-plan": "Change",
|
||||
"com.affine.storage.plan": "Plan",
|
||||
"com.affine.storage.used.hint": "Space used",
|
||||
"com.affine.themeSettings.dark": "Dark",
|
||||
"com.affine.themeSettings.light": "Light",
|
||||
@@ -639,5 +641,32 @@
|
||||
"com.affine.auth.sign-out.confirm-modal.title": "Sign out?",
|
||||
"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.auth.sign-out.confirm-modal.confirm": "Sign Out",
|
||||
"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",
|
||||
"com.affine.payment.billing-setting.history": "Billing history",
|
||||
"com.affine.payment.billing-setting.current-plan": "Current Plan",
|
||||
"com.affine.payment.billing-setting.current-plan.description": "You are current on the <1>{{planName}} plan</1>.",
|
||||
"com.affine.payment.billing-setting.current-plan.description.monthly": "You are current on the monthly <1>{{planName}} plan</1>.",
|
||||
"com.affine.payment.billing-setting.current-plan.description.yearly": "You are current on the yearly <1>{{planName}} plan</1>.",
|
||||
"com.affine.payment.billing-setting.month": "month",
|
||||
"com.affine.payment.billing-setting.year": "year",
|
||||
"com.affine.payment.billing-setting.payment-method": "Payment Method",
|
||||
"com.affine.payment.billing-setting.payment-method.description": "Provided by Stripe.",
|
||||
"com.affine.payment.billing-setting.renew-date": "Renew Date",
|
||||
"com.affine.payment.billing-setting.renew-date.description": "Next billing date: {{renewDate}}",
|
||||
"com.affine.payment.billing-setting.expiration-date": "Expiration Date",
|
||||
"com.affine.payment.billing-setting.expiration-date.description": "Your subscription is valid until {{expirationDate}}",
|
||||
"com.affine.payment.billing-setting.cancel-subscription": "Cancel Subscription",
|
||||
"com.affine.payment.billing-setting.cancel-subscription.description": "Subscription cancelled, your pro account will expire on {{cancelDate}}",
|
||||
"com.affine.payment.billing-setting.upgrade": "Upgrade",
|
||||
"com.affine.payment.billing-setting.change-plan": "Change Plan",
|
||||
"com.affine.payment.billing-setting.resume-subscription": "Resume",
|
||||
"com.affine.payment.billing-setting.no-invoice": "There are no invoices to display.",
|
||||
"com.affine.payment.billing-setting.paid": "Paid",
|
||||
"com.affine.payment.billing-setting.view-invoice": "View Invoice"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user