feat(core): add translation key for payment (#4723)

This commit is contained in:
JimmFly
2023-10-26 16:15:12 +08:00
committed by GitHub
parent 1deb6bffd3
commit e0f7ac426c
7 changed files with 236 additions and 97 deletions
@@ -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]);
@@ -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>
);
};
@@ -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',
});
@@ -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',
+31 -2
View File
@@ -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"
}