0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-10 21:58:23 -05:00

feat(console): add payment overdue reminder (#4203)

This commit is contained in:
Xiao Yijun 2023-07-23 13:19:51 +08:00 committed by GitHub
parent 7b97571f1c
commit 437be82a27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 208 additions and 38 deletions

View file

@ -11,17 +11,17 @@ import * as styles from './index.module.scss';
type Props = {
cost: number;
isManagePaymentVisible?: boolean;
};
function NextBillInfo({ cost }: Props) {
function BillInfo({ cost, isManagePaymentVisible }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
<div className={styles.container}>
<div className={styles.billInfo}>
<div className={styles.price}>
<span>{`$${cost.toLocaleString()}`}</span>
{}
<span>{`$${(cost / 100).toLocaleString()}`}</span>
{cost > 0 && (
<ToggleTip content={<DynamicT forKey="subscription.next_bill_tip" />}>
<IconButton size="small">
@ -46,7 +46,7 @@ function NextBillInfo({ cost }: Props) {
</Trans>
</div>
</div>
{cost > 0 && (
{isManagePaymentVisible && (
<Button
title="subscription.manage_payment"
onClick={() => {
@ -58,4 +58,4 @@ function NextBillInfo({ cost }: Props) {
);
}
export default NextBillInfo;
export default BillInfo;

View file

@ -0,0 +1,7 @@
.linkButton {
text-decoration: none;
}
.strong {
font-weight: 500;
}

View file

@ -0,0 +1,93 @@
import { conditional } from '@silverhand/essentials';
import { useContext, useEffect, useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { contactEmailLink } from '@/consts';
import { TenantsContext } from '@/contexts/TenantsProvider';
import Button from '@/ds-components/Button';
import FormField from '@/ds-components/FormField';
import InlineNotification from '@/ds-components/InlineNotification';
import ModalLayout from '@/ds-components/ModalLayout';
import useInvoices from '@/hooks/use-invoices';
import * as modalStyles from '@/scss/modal.module.scss';
import { getLatestUnpaidInvoice } from '@/utils/subscription';
import BillInfo from '../BillInfo';
import * as styles from './index.module.scss';
function PaymentOverdueModal() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { currentTenant, currentTenantId } = useContext(TenantsContext);
const { data: invoices, error } = useInvoices(currentTenantId);
const isLoadingInvoices = !invoices && !error;
const latestUnpaidInvoice = useMemo(() => {
return conditional(invoices && getLatestUnpaidInvoice(invoices));
}, [invoices]);
const [hasClosed, setHasClosed] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const handleCloseModal = () => {
setHasClosed(true);
setIsOpen(false);
};
useEffect(() => {
if (isLoadingInvoices || hasClosed) {
return;
}
if (latestUnpaidInvoice) {
setIsOpen(true);
}
}, [hasClosed, isLoadingInvoices, latestUnpaidInvoice]);
if (isLoadingInvoices || !latestUnpaidInvoice || hasClosed) {
return null;
}
return (
<ReactModal
shouldCloseOnEsc
isOpen={isOpen}
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
onRequestClose={handleCloseModal}
>
<ModalLayout
title="upsell.payment_overdue_modal.title"
footer={
<>
<a href={contactEmailLink} target="_blank" className={styles.linkButton} rel="noopener">
<Button title="upsell.contact_us" />
</a>
<Button
type="primary"
title="upsell.payment_overdue_modal.update_payment"
onClick={() => {
// Todo: @xiaoyijun Update payment
}}
/>
</>
}
onClose={handleCloseModal}
>
{currentTenant && (
<InlineNotification severity="error">
<Trans components={{ span: <span className={styles.strong} /> }}>
{t('upsell.payment_overdue_modal.notification', { name: currentTenant.name })}
</Trans>
</InlineNotification>
)}
<FormField title="upsell.payment_overdue_modal.unpaid_bills">
<BillInfo cost={latestUnpaidInvoice.amountDue} />
</FormField>
</ModalLayout>
</ReactModal>
);
}
export default PaymentOverdueModal;

View file

@ -4,6 +4,7 @@ import { Navigate, Outlet, useParams } from 'react-router-dom';
import AppLoading from '@/components/AppLoading';
import MauExceededModal from '@/components/MauExceededModal';
import PaymentOverdueModal from '@/components/PaymentOverdueModal';
import { isCloud, isProduction } from '@/consts/env';
import useConfigs from '@/hooks/use-configs';
import useScroll from '@/hooks/use-scroll';
@ -38,7 +39,12 @@ export default function AppContent() {
</div>
{isCloud && <Broadcast />}
{/* Todo: @xiaoyijun remove this condition on subscription features ready. */}
{!isProduction && isCloud && <MauExceededModal />}
{!isProduction && isCloud && (
<>
<MauExceededModal />
<PaymentOverdueModal />
</>
)}
</>
);
}

View file

@ -0,0 +1,44 @@
import { conditional } from '@silverhand/essentials';
import { useContext, useMemo } from 'react';
import { TenantsContext } from '@/contexts/TenantsProvider';
import DynamicT from '@/ds-components/DynamicT';
import InlineNotification from '@/ds-components/InlineNotification';
import useInvoices from '@/hooks/use-invoices';
import { getLatestUnpaidInvoice } from '@/utils/subscription';
type Props = {
className?: string;
};
function PaymentOverdueNotification({ className }: Props) {
const { currentTenantId } = useContext(TenantsContext);
const { data: invoices, error } = useInvoices(currentTenantId);
const isLoadingInvoices = !invoices && !error;
const latestUnpaidInvoice = useMemo(
() => conditional(invoices && getLatestUnpaidInvoice(invoices)),
[invoices]
);
if (isLoadingInvoices || !latestUnpaidInvoice) {
return null;
}
return (
<InlineNotification
severity="error"
action="subscription.update_payment"
className={className}
onClick={() => {
// Todo @xiaoyijun manage payment
}}
>
<DynamicT
forKey="subscription.payment_error"
interpolation={{ price: latestUnpaidInvoice.amountDue / 100 }}
/>
</InlineNotification>
);
}
export default PaymentOverdueNotification;

View file

@ -1,4 +1,5 @@
import { type SubscriptionUsage, type Subscription } from '@/cloud/types/router';
import BillInfo from '@/components/BillInfo';
import FormCard from '@/components/FormCard';
import PlanDescription from '@/components/PlanDescription';
import PlanName from '@/components/PlanName';
@ -7,7 +8,7 @@ import FormField from '@/ds-components/FormField';
import { type SubscriptionPlan } from '@/types/subscriptions';
import MauLimitExceedNotification from './MauLimitExceededNotification';
import NextBillInfo from './NextBillInfo';
import PaymentOverdueNotification from './PaymentOverdueNotification';
import * as styles from './index.module.scss';
type Props = {
@ -37,13 +38,17 @@ function CurrentPlan({ subscription, subscriptionPlan, subscriptionUsage }: Prop
/>
</FormField>
<FormField title="subscription.next_bill">
<NextBillInfo cost={subscriptionUsage.cost} />
<BillInfo
cost={subscriptionUsage.cost}
isManagePaymentVisible={Boolean(subscriptionUsage.cost)}
/>
</FormField>
<MauLimitExceedNotification
activeUsers={subscriptionUsage.activeUsers}
currentPlan={subscriptionPlan}
className={styles.notification}
/>
<PaymentOverdueNotification className={styles.notification} />
</FormCard>
);
}

View file

@ -22,6 +22,7 @@ const subscription = {
overfill_quota_warning:
'Sie haben Ihr Quotenlimit erreicht. Um Probleme zu vermeiden, upgraden Sie den Plan.',
upgrade_pro: 'Pro upgraden',
update_payment: 'Zahlung aktualisieren',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Es wurde ein Zahlungsproblem festgestellt. Der Betrag von ${{price, number}} für den vorherigen Zyklus kann nicht verarbeitet werden. Aktualisieren Sie die Zahlung, um eine Aussetzung des Logto-Dienstes zu vermeiden.',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: 'Zahlungsrückstand für Rechnung',
notification:
'Hoppla! Die Zahlung für die Rechnung des Tenants {{name}} ist im letzten Abrechnungszeitraum fehlgeschlagen. Bitte zahlen Sie die Rechnung umgehend, um die Aussetzung des Logto-Dienstes zu vermeiden.',
unpaid_bills_last_cycle: 'Unbezahlte Rechnungen im letzten Abrechnungszeitraum',
'Hoppla! Die Zahlung für die Rechnung des Mieters <span>{{name}}</span> ist fehlgeschlagen. Bitte zahlen Sie die Rechnung umgehend, um eine Sperrung des Logto-Dienstes zu vermeiden.',
unpaid_bills: 'Ausstehende Rechnungen',
update_payment: 'Zahlung aktualisieren',
},
};

View file

@ -22,6 +22,7 @@ const subscription = {
overfill_quota_warning:
'You have reached your quota limit. To prevent any issues, upgrade the plan.',
upgrade_pro: 'Upgrade Pro',
update_payment: 'Update payment',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Payment issue detected. Unable to process ${{price, number}} for previous cycle. Update payment to avoid Logto service suspension.',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: 'Bill payment overdue',
notification:
'Oops! Payment for tenant {{name}} bill failed last cycle. Please pay the bill promptly to avoid suspension of Logto service.',
unpaid_bills_last_cycle: 'Unpaid bills last cycle',
'Oops! Payment for tenant <span>{{name}}</span> bill failed. Please pay the bill promptly to avoid suspension of Logto service.',
unpaid_bills: 'Unpaid bills',
update_payment: 'Update Payment',
},
};

View file

@ -23,6 +23,7 @@ const subscription = {
overfill_quota_warning:
'Ha alcanzado el límite de su cuota. Para evitar problemas, actualice el plan.',
upgrade_pro: 'Actualizar a Pro',
update_payment: 'Actualizar pago',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Se ha detectado un problema de pago. No se puede procesar ${ {price, number}} para el ciclo anterior. Actualice el pago para evitar la suspensión del servicio Logto.',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: 'Factura con pagos atrasados',
notification:
Vaya! El pago de la factura del tenant {{name}} falló en el último ciclo. Por favor, paga la factura a tiempo para evitar la suspensión del servicio de Logto.',
unpaid_bills_last_cycle: 'Facturas impagadas del último ciclo',
Ups! El pago de la factura del inquilino <span>{{name}}</span> ha fallado. Por favor, pague la factura a tiempo para evitar la suspensión del servicio de Logto.',
unpaid_bills: 'Facturas impagas',
update_payment: 'Actualizar pago',
},
};

View file

@ -24,6 +24,7 @@ const subscription = {
overfill_quota_warning:
'Vous avez atteint votre limite de quota. Pour éviter tout problème, passez à un plan supérieur.',
upgrade_pro: 'Passer au Plan Professionnel',
update_payment: 'Mettre à jour le paiement',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Problème de paiement détecté. Impossible de traiter ${{price, number}} pour le cycle précédent. Mettez à jour le paiement pour éviter la suspension du service Logto.',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: 'Paiement de facture en retard',
notification:
'Oups ! Le paiement de la facture du locataire {{name}} a échoué lors du dernier cycle. Veuillez payer la facture rapidement pour éviter la suspension du service Logto.',
unpaid_bills_last_cycle: 'Factures impayées du dernier cycle',
'Oups ! Le paiement de la facture du locataire <span>{{name}}</span> a échoué. Veuillez payer la facture rapidement pour éviter la suspension du service Logto.',
unpaid_bills: 'Factures impayées',
update_payment: 'Mettre à jour le paiement',
},
};

View file

@ -23,6 +23,7 @@ const subscription = {
overfill_quota_warning:
"Hai raggiunto il limite del tuo contingente. Per evitare eventuali problemi, esegui l'upgrade del piano.",
upgrade_pro: "Esegui l'upgrade a Pro",
update_payment: 'Aggiorna pagamento',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Rilevato un problema di pagamento. Impossibile elaborare ${{price, number}} per il ciclo precedente. Aggiorna il pagamento per evitare la sospensione del servizio Logto.',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: 'Pagamento della fattura in ritardo',
notification:
"Oops! Il pagamento della fattura del tenant {{name}} è fallito l'ultimo ciclo. Effettua il pagamento della fattura tempestivamente per evitare la sospensione del servizio Logto.",
unpaid_bills_last_cycle: "Fatture non pagate l'ultimo ciclo",
"Oops! Il pagamento della fattura dell'affittuario <span>{{name}}</span> è fallito. Si prega di pagare tempestivamente la fattura per evitare la sospensione del servizio Logto.",
unpaid_bills: 'Fatture non pagate',
update_payment: 'Aggiorna pagamento',
},
};

View file

@ -23,6 +23,7 @@ const subscription = {
overfill_quota_warning:
'クォータ制限に到達しました。問題を防ぐために、プランをアップグレードしてください。',
upgrade_pro: 'プロプランにアップグレード',
update_payment: '支払いを更新する',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'支払いに問題が発生しました。前回のサイクルで ${{price, number}} を処理できませんでした。Logtoのサービス停止を回避するために支払いを更新してください。',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: '請求書の支払いが期限切れです',
notification:
'おっと!テナント{{name}}の請求書の支払いが前回のサイクルで失敗しました。サービスの停止を回避するために、迅速に請求書をお支払いください。',
unpaid_bills_last_cycle: '前回のサイクルの未払い請求書',
'おっと!テナント<span>{{name}}</span>の請求書の支払いが失敗しました。Logtoサービスの停止を避けるために、すみやかに請求書をお支払いください。',
unpaid_bills: '未払いの請求書',
update_payment: '支払いを更新',
},
};

View file

@ -22,6 +22,7 @@ const subscription = {
overfill_quota_warning:
'할당량 한도에 도달했습니다. 문제를 방지하기 위해 요금제를 업그레이드하세요.',
upgrade_pro: '프로 업그레이드',
update_payment: '결제 정보 업데이트',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'결제 문제가 발생했습니다. 이전 주기에 ${{price, number}}을(를) 처리할 수 없습니다. Logto 서비스 중단을 피하기 위해 결제를 업데이트하세요.',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: '청구서 지불 연체',
notification:
'이런! 지난 사이클에 테넌트 {{name}}의 청구서 지불이 실패했습니다. 로그토 서비스 중단을 피하기 위해 청구서를 신속히 지불하세요.',
unpaid_bills_last_cycle: '지난 사이클 미납 청구서',
'이런! 테넌트 <span>{{name}}</span>의 청구서 결제에 실패했습니다. Logto 서비스 중단을 피하기 위해 즉시 청구서를 지불하십시오.',
unpaid_bills: '미납 청구서',
update_payment: '지불 업데이트',
},
};

View file

@ -23,6 +23,7 @@ const subscription = {
overfill_quota_warning:
'Osiągnąłeś limit swojej puli. Aby uniknąć problemów, zaktualizuj swój plan.',
upgrade_pro: 'Uaktualnij do Pro',
update_payment: 'Zaktualizuj płatność',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Wykryto problem z płatnością. Nie można przetworzyć ${{price, number}} za poprzedni cykl. Zaktualizuj płatność, aby uniknąć zawieszenia usługi Logto.',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: 'Opłata za fakturę zaległa',
notification:
'Ups! Opłata za fakturę dla najemcy {{name}} nie została opłacona w ostatnim cyklu. Proszę natychmiast uiścić opłatę, aby uniknąć zawieszenia usługi Logto.',
unpaid_bills_last_cycle: 'Niezapłacone faktury z ostatniego cyklu',
'Ups! Płatność za fakturę najemcy <span>{{name}}</span> nie powiodła się. Proszę zapłacić fakturę w odpowiednim terminie, aby uniknąć zawieszenia usługi Logto.',
unpaid_bills: 'Nieuregulowane faktury',
update_payment: 'Zaktualizuj płatność',
},
};

View file

@ -23,6 +23,7 @@ const subscription = {
overfill_quota_warning:
'Você atingiu o limite de cota. Para evitar problemas, faça upgrade do plano.',
upgrade_pro: 'Upgrade Pro',
update_payment: 'Atualizar pagamento',
payment_error:
'Detectado um problema de pagamento. Não é possível processar $ {{price, number}} para o ciclo anterior. Atualize o pagamento para evitar a suspensão do serviço Logto.',
downgrade: 'Downgrade',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: 'Pagamento da fatura em atraso',
notification:
'Ops! O pagamento da fatura do inquilino {{name}} falhou no último ciclo. Por favor, efetue o pagamento da fatura a tempo para evitar a suspensão do serviço do Logto.',
unpaid_bills_last_cycle: 'Faturas em atraso no último ciclo',
'Oops! O pagamento da fatura do inquilino <span>{{name}}</span> falhou. Por favor, pague a fatura prontamente para evitar a suspensão do serviço Logto.',
unpaid_bills: 'Faturas não pagas',
update_payment: 'Atualizar pagamento',
},
};

View file

@ -23,6 +23,7 @@ const subscription = {
overfill_quota_warning:
'Você atingiu o limite da sua cota. Para evitar problemas, faça upgrade do plano.',
upgrade_pro: 'Atualizar para Pro',
update_payment: 'Atualizar pagamento',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Problema de pagamento detectado. Não é possível processar ${{price, number}} para o ciclo anterior. Atualize o pagamento para evitar a suspensão do serviço Logto.',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: 'Pagamento da fatura em atraso',
notification:
'Ops! O pagamento da fatura do inquilino {{name}} falhou no último ciclo. Por favor, efetue o pagamento da fatura a tempo para evitar a suspensão do serviço do Logto.',
unpaid_bills_last_cycle: 'Faturas em atraso no último ciclo',
'Oops! O pagamento da fatura do inquilino <span>{{name}}</span> falhou. Por favor, pague a fatura prontamente para evitar a suspensão do serviço Logto.',
unpaid_bills: 'Faturas não pagas',
update_payment: 'Atualizar pagamento',
},
};

View file

@ -22,6 +22,7 @@ const subscription = {
overfill_quota_warning:
'Вы достигли лимита вашего квоты. Чтобы избежать возможных проблем, повысьте план.',
upgrade_pro: 'Повысить уровень до Pro',
update_payment: 'Обновить платеж',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Обнаружена ошибка платежа. Невозможно обработать сумму ${{price, number}} за предыдущий цикл. Обновите платежную информацию, чтобы избежать блокировки сервиса Logto.',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: 'Просрочен платеж за счет',
notification:
'Упс! Платеж за арендатора {{name}} не был произведен в прошлом цикле. Пожалуйста, незамедлительно оплатите счет, чтобы избежать приостановки сервиса Logto.',
unpaid_bills_last_cycle: 'Просроченные счета за последний цикл',
'Упс! Оплата счета арендатора <span>{{name}}</span> не удалась. Пожалуйста, оплатите счет немедленно, чтобы избежать приостановки сервиса Logto.',
unpaid_bills: 'Неоплаченные счета',
update_payment: 'Обновить платеж',
},
};

View file

@ -23,6 +23,7 @@ const subscription = {
overfill_quota_warning:
'Kota sınırınıza ulaştınız. Herhangi bir sorunu önlemek için planı yükseltin.',
upgrade_pro: "Pro'ya yükselt",
update_payment: 'Ödemeyi Güncelle',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Ödeme hatası tespit edildi. Önceki döngü için ${{price, number}} işlenemedi. Logto hizmeti askıya alınmasını önlemek için ödemeleri güncelleyin.',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: 'Fatura ödemesi gecikti',
notification:
'Ups! Kiracı {{name}} faturasının ödemesi son döngüde başarısız oldu. Logto hizmetinin askıya alınmasını önlemek için faturayı hemen ödeyin.',
unpaid_bills_last_cycle: 'Son döngüde ödenmemiş faturalar',
'Oops! Kiracı <span>{{name}}</span> faturasının ödemesi başarısız oldu. Logto hizmetinin askıya alınmaması için faturayı zamanında ödeyin.',
unpaid_bills: denmemiş faturalar',
update_payment: 'Ödemeyi Güncelle',
},
};

View file

@ -21,6 +21,7 @@ const subscription = {
manage_payment: '管理付款',
overfill_quota_warning: '您已达到配额限制。为防止任何问题,请升级计划。',
upgrade_pro: '升级专业计划',
update_payment: '更新付款信息',
payment_error:
'检测到付款问题。无法处理前一周期的{{price, number}}美元。更新付款以避免 Logto 服务中断。',
downgrade: '降级',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: '账单逾期未付',
notification:
'糟糕!上一周期租户 {{name}} 的账单支付失败。请立即支付账单,以避免 Logto 服务的暂停。',
unpaid_bills_last_cycle: '上一周期未支付账单',
'糟糕!租户<span>{{name}}</span>的账单支付失败。请尽快支付账单以避免Logto服务中止。',
unpaid_bills: '未付账单',
update_payment: '更新支付',
},
};

View file

@ -21,6 +21,7 @@ const subscription = {
manage_payment: '管理付款',
overfill_quota_warning: '您已達到配額限制。請升級計劃以防止任何問題。',
upgrade_pro: '升級到專業版',
update_payment: '更新付款信息',
payment_error:
'檢測到付款問題。無法處理前一個週期的$ {{price, number}}。更新付款以避免Logto服務暫停。',
downgrade: '降級',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: '賬單逾期未付',
notification:
'糟糕!上一周期租戶 {{name}} 的賬單支付失敗。請立即支付賬單,以避免 Logto 服務的暫停。',
unpaid_bills_last_cycle: '上一周期未支付賬單',
'糟糕!租戶<span>{{name}}</span>的賬單支付失敗。請儘快支付賬單以避免Logto服務中止。',
unpaid_bills: '未付賬單',
update_payment: '更新支付',
},
};

View file

@ -21,6 +21,7 @@ const subscription = {
manage_payment: '管理付款',
overfill_quota_warning: '您已達到配額限制。為防止出現問題,請升級計畫。',
upgrade_pro: '升級專業',
update_payment: '更新付款信息',
payment_error:
'偵測到付款問題。無法處理以前週期的 {{price, number}}$ 。更新付款以避免 Logto 服務中止。',
downgrade: '降級',

View file

@ -81,8 +81,8 @@ const upsell = {
payment_overdue_modal: {
title: '賬單逾期未付',
notification:
'糟糕!上一周期租戶 {{name}} 的賬單支付失敗。請立即支付賬單,以避免 Logto 服務的暫停。',
unpaid_bills_last_cycle: '上一周期未支付賬單',
'糟糕!租戶<span>{{name}}</span>的賬單支付失敗。請儘快支付賬單以避免Logto服務中止。',
unpaid_bills: '未付賬單',
update_payment: '更新支付',
},
};