@carlonicora/nextjs-jsonapi 4.1.2 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AssistantMessageInterface-CubxuBIJ.d.ts → AssistantMessageInterface-DHJJO_Z_.d.ts} +1 -1
- package/dist/{AssistantMessageInterface-sHjI2zPN.d.mts → AssistantMessageInterface-S7oyERTT.d.mts} +1 -1
- package/dist/{BlockNoteEditor-VOYKZOP5.mjs → BlockNoteEditor-HXAHDJA7.mjs} +34 -27
- package/dist/BlockNoteEditor-HXAHDJA7.mjs.map +1 -0
- package/dist/{BlockNoteEditor-BENYTTCZ.js → BlockNoteEditor-LLCZDFLZ.js} +50 -43
- package/dist/BlockNoteEditor-LLCZDFLZ.js.map +1 -0
- package/dist/{BlockNoteViewer-USLR4Y7C.js → BlockNoteViewer-77CCTC2U.js} +3 -3
- package/dist/{BlockNoteViewer-USLR4Y7C.js.map → BlockNoteViewer-77CCTC2U.js.map} +1 -1
- package/dist/{BlockNoteViewer-Y2ZM4ESA.mjs → BlockNoteViewer-MDPO6BXN.mjs} +2 -2
- package/dist/{auth.interface-Df4vXbNz.d.mts → auth.interface-CeFCz9NU.d.mts} +1 -1
- package/dist/{auth.interface-BzI4onI_.d.ts → auth.interface-D1R7VVky.d.ts} +1 -1
- package/dist/billing/index.d.mts +40 -6
- package/dist/billing/index.d.ts +40 -6
- package/dist/billing/index.js +806 -774
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +494 -462
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-6ISF5KNQ.mjs → chunk-2VXDZRYY.mjs} +1103 -816
- package/dist/chunk-2VXDZRYY.mjs.map +1 -0
- package/dist/{chunk-4BZJ4LAB.js → chunk-4ORLPEK7.js} +136 -104
- package/dist/chunk-4ORLPEK7.js.map +1 -0
- package/dist/{chunk-ZZ6E33FO.mjs → chunk-6DRSSW5O.mjs} +36 -4
- package/dist/chunk-6DRSSW5O.mjs.map +1 -0
- package/dist/{chunk-KS6WZP2E.js → chunk-6SMJRAE3.js} +4 -4
- package/dist/{chunk-KS6WZP2E.js.map → chunk-6SMJRAE3.js.map} +1 -1
- package/dist/{chunk-WQ77CUUZ.js → chunk-GPXYQMOZ.js} +7 -7
- package/dist/{chunk-WQ77CUUZ.js.map → chunk-GPXYQMOZ.js.map} +1 -1
- package/dist/{chunk-46S3KPLU.mjs → chunk-ILFFC6BG.mjs} +2 -2
- package/dist/{chunk-HZVVRIJQ.mjs → chunk-IUS2RHCX.mjs} +2 -2
- package/dist/{chunk-F7RNBGHS.js → chunk-KJMMBUOA.js} +3 -3
- package/dist/{chunk-F7RNBGHS.js.map → chunk-KJMMBUOA.js.map} +1 -1
- package/dist/{chunk-ZO3QQ5CQ.mjs → chunk-MQDR6MEE.mjs} +2 -2
- package/dist/{chunk-ORU7KLCY.js → chunk-VDVIUDY5.js} +1445 -1158
- package/dist/chunk-VDVIUDY5.js.map +1 -0
- package/dist/client/index.d.mts +20 -6
- package/dist/client/index.d.ts +20 -6
- package/dist/client/index.js +6 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +5 -3
- package/dist/components/index.d.mts +59 -21
- package/dist/components/index.d.ts +59 -21
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/{config-CEAq4Ozy.d.mts → config-BP2vyddw.d.mts} +5 -0
- package/dist/{config-DydDHCfb.d.ts → config-BhW2QrRD.d.ts} +5 -0
- package/dist/contexts/index.d.mts +4 -4
- package/dist/contexts/index.d.ts +4 -4
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +17 -9
- package/dist/core/index.d.ts +17 -9
- package/dist/core/index.js +4 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +3 -1
- package/dist/features/analytics/index.d.mts +1 -1
- package/dist/features/analytics/index.d.ts +1 -1
- package/dist/features/analytics/index.js +122 -122
- package/dist/features/analytics/index.mjs +4 -4
- package/dist/features/analytics/server.js +4 -4
- package/dist/features/analytics/server.mjs +2 -2
- package/dist/features/help/index.d.mts +1 -1
- package/dist/features/help/index.d.ts +1 -1
- package/dist/features/help/index.js +42 -40
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +8 -6
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/tokenusage/index.js +125 -125
- package/dist/features/tokenusage/index.mjs +4 -4
- package/dist/index.d.mts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4 -2
- package/dist/{notification.interface-P_pNfzZQ.d.mts → notification.interface-BB6GPpZY.d.mts} +1 -1
- package/dist/{notification.interface-BNCvnVCW.d.ts → notification.interface-CmAGrijJ.d.ts} +1 -1
- package/dist/{s3.service-CbUa6a-8.d.ts → s3.service-C-4YpEMu.d.ts} +7 -3
- package/dist/{s3.service-DAp8FbuG.d.mts → s3.service-dVj-nxMl.d.mts} +7 -3
- package/dist/server/index.d.mts +4 -4
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/dist/{useSocket-BHVIyigI.d.ts → useSocket-B0H9pmmH.d.ts} +1 -1
- package/dist/{useSocket-B9gTbuCj.d.mts → useSocket-B5RQb6v2.d.mts} +1 -1
- package/dist/{user.interface-CrmwJLYW.d.ts → user.interface-C0ZdsBZj.d.ts} +2 -0
- package/dist/{user.interface-CSaGWFbg.d.mts → user.interface-CIWGlbqM.d.mts} +2 -0
- package/package.json +1 -1
- package/src/components/containers/RoundPageContainer.tsx +3 -1
- package/src/components/containers/RoundPageContainerTitle.tsx +5 -2
- package/src/components/editors/BlockNoteEditor.tsx +31 -15
- package/src/components/editors/BlockNoteEditorAiActions.tsx +11 -7
- package/src/components/editors/__tests__/BlockNoteEditorAiActions.test.ts +10 -3
- package/src/components/errors/ErrorDetails.tsx +5 -2
- package/src/components/forms/CommonAssociationForm.tsx +1 -1
- package/src/components/forms/EntityMultiSelector.tsx +6 -2
- package/src/components/forms/EntitySelector.tsx +4 -1
- package/src/components/forms/FileUploader.tsx +3 -2
- package/src/components/forms/MultiFileUploader.tsx +3 -1
- package/src/components/navigations/Breadcrumb.tsx +3 -2
- package/src/components/navigations/Header.tsx +3 -1
- package/src/components/navigations/MobileNavigationBar.tsx +3 -1
- package/src/components/navigations/__tests__/Header.spec.tsx +2 -2
- package/src/components/tables/cells/cell.id.tsx +13 -2
- package/src/core/abstracts/AbstractService.ts +3 -3
- package/src/core/abstracts/ClientAbstractService.ts +3 -2
- package/src/features/assistant-action/components/ApprovalActionCard.tsx +74 -5
- package/src/features/assistant-action/components/__tests__/ApprovalActionCard.spec.tsx +81 -0
- package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +4 -1
- package/src/features/assistant-message/components/parts/tabs/UsersTab.tsx +2 -1
- package/src/features/auth/components/buttons/GoogleSignInButton.tsx +4 -1
- package/src/features/auth/components/details/LandingComponent.tsx +1 -1
- package/src/features/auth/components/forms/ForgotPassword.tsx +1 -1
- package/src/features/auth/components/forms/Login.tsx +1 -1
- package/src/features/auth/components/forms/Register.tsx +1 -1
- package/src/features/auth/components/forms/ResetPassword.tsx +2 -2
- package/src/features/auth/components/two-factor/BackupCodesDialog.tsx +1 -1
- package/src/features/auth/components/two-factor/PasskeyButton.tsx +1 -1
- package/src/features/billing/components/cards/BillingUsageSummaryCard.tsx +21 -11
- package/src/features/billing/components/cards/CustomerInfoCard.tsx +16 -13
- package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +33 -21
- package/src/features/billing/components/cards/PaymentMethodSummaryCard.tsx +20 -10
- package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +35 -22
- package/src/features/billing/components/containers/BillingDashboardContainer.tsx +24 -24
- package/src/features/billing/components/utils/__tests__/billing-formatters.spec.ts +63 -0
- package/src/features/billing/components/utils/currency.ts +67 -10
- package/src/features/billing/components/utils/date.ts +9 -4
- package/src/features/billing/components/utils/format-options.ts +20 -0
- package/src/features/billing/components/utils/index.ts +3 -1
- package/src/features/billing/components/utils/useBillingFormatOptions.ts +11 -0
- package/src/features/billing/components/widgets/BillingAlertBanner.tsx +8 -9
- package/src/features/billing/contexts/PriceContext.tsx +18 -6
- package/src/features/billing/i18n-keys.ts +212 -0
- package/src/features/billing/stripe-customer/components/containers/PaymentMethodsContainer.tsx +8 -8
- package/src/features/billing/stripe-customer/components/details/PaymentMethodCard.tsx +11 -9
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +4 -4
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +9 -7
- package/src/features/billing/stripe-invoice/components/containers/InvoicesContainer.tsx +10 -8
- package/src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx +34 -29
- package/src/features/billing/stripe-invoice/components/lists/InvoicesList.tsx +12 -9
- package/src/features/billing/stripe-invoice/components/widgets/InvoiceStatusBadge.tsx +9 -6
- package/src/features/billing/stripe-price/components/details/PriceDetails.tsx +4 -3
- package/src/features/billing/stripe-price/components/forms/PriceArchiver.tsx +3 -1
- package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +3 -1
- package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +6 -5
- package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +12 -7
- package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.service.ts +3 -1
- package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +10 -6
- package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +44 -29
- package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx +14 -11
- package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +20 -17
- package/src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx +4 -2
- package/src/features/billing/stripe-subscription/components/widgets/PricingCard.tsx +16 -7
- package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +3 -1
- package/src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx +3 -1
- package/src/features/billing/stripe-subscription/components/widgets/ProrationPreview.tsx +12 -7
- package/src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx +11 -8
- package/src/features/billing/stripe-subscription/components/widgets/SubscriptionStatusBadge.tsx +13 -10
- package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +8 -6
- package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +7 -4
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +4 -2
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +3 -1
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +37 -28
- package/src/features/billing/stripe-subscription/hooks/useConfirmSubscriptionPayment.ts +9 -7
- package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +20 -9
- package/src/features/billing/stripe-usage/components/containers/UsageContainer.tsx +6 -6
- package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +24 -9
- package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +16 -12
- package/src/features/company/components/details/TokenStatusIndicator.tsx +7 -5
- package/src/features/company/components/forms/CompanyEditor.tsx +2 -2
- package/src/features/help/components/HelpArticleBody.tsx +1 -1
- package/src/features/help/components/HelpHeader.tsx +1 -1
- package/src/features/help/components/HelpSideNav.tsx +1 -1
- package/src/features/help/i18n-keys.ts +2 -0
- package/src/features/notification/components/common/NotificationErrorBoundary.tsx +17 -12
- package/src/features/notification/components/containers/NotificationsListContainer.tsx +4 -2
- package/src/features/notification/components/notifications/Notification.tsx +6 -3
- package/src/features/notification/contexts/NotificationContext.tsx +9 -4
- package/src/features/notification/contexts/__tests__/NotificationContext.spec.tsx +13 -0
- package/src/features/notification/hooks/useNotificationTableStructure.tsx +4 -2
- package/src/features/oauth/components/OAuthClientCard.tsx +13 -7
- package/src/features/oauth/components/OAuthClientDetail.tsx +34 -30
- package/src/features/oauth/components/OAuthClientForm.tsx +24 -24
- package/src/features/oauth/components/OAuthClientList.tsx +10 -6
- package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +10 -8
- package/src/features/oauth/components/OAuthRedirectUriInput.tsx +9 -13
- package/src/features/oauth/components/OAuthScopeSelector.tsx +15 -6
- package/src/features/oauth/components/__tests__/OAuthClientForm.spec.tsx +11 -9
- package/src/features/oauth/components/consent/OAuthScopeList.tsx +4 -2
- package/src/features/oauth/hooks/__tests__/useOAuthScopeText.spec.ts +30 -0
- package/src/features/oauth/hooks/index.ts +1 -0
- package/src/features/oauth/hooks/useOAuthClient.ts +11 -5
- package/src/features/oauth/hooks/useOAuthClients.ts +9 -3
- package/src/features/oauth/hooks/useOAuthConsent.ts +11 -5
- package/src/features/oauth/hooks/useOAuthScopeText.ts +33 -0
- package/src/features/rbac/components/RbacByRoleContainer.tsx +7 -6
- package/src/features/rbac/components/RbacContainer.tsx +7 -6
- package/src/features/rbac/contexts/RbacContext.tsx +3 -3
- package/src/features/user/components/containers/UserContainer.tsx +3 -1
- package/src/features/user/data/__tests__/user.test.ts +66 -0
- package/src/features/user/data/user.interface.ts +2 -0
- package/src/features/user/data/user.service.ts +9 -0
- package/src/features/user/data/user.ts +7 -0
- package/src/features/waitlist/components/forms/WaitlistForm.tsx +1 -1
- package/src/features/waitlist/components/lists/WaitlistList.tsx +1 -1
- package/src/i18n/__tests__/config.test.ts +21 -1
- package/src/i18n/config.ts +13 -0
- package/src/i18n/index.ts +1 -0
- package/src/shadcnui/custom/multi-select.tsx +47 -14
- package/src/shadcnui/custom/multiple-selector.tsx +14 -2
- package/src/shadcnui/ui/__tests__/use-ui-text.test.tsx +76 -0
- package/src/shadcnui/ui/breadcrumb.tsx +9 -2
- package/src/shadcnui/ui/carousel.tsx +15 -4
- package/src/shadcnui/ui/dialog.tsx +15 -2
- package/src/shadcnui/ui/sheet.tsx +6 -1
- package/src/shadcnui/ui/sidebar.tsx +33 -7
- package/src/shadcnui/ui/spinner.tsx +6 -1
- package/src/shadcnui/ui/use-ui-text.ts +49 -0
- package/src/utils/__tests__/date-formatter.test.ts +24 -1
- package/src/utils/date-formatter.ts +16 -0
- package/src/utils/index.ts +1 -1
- package/dist/BlockNoteEditor-BENYTTCZ.js.map +0 -1
- package/dist/BlockNoteEditor-VOYKZOP5.mjs.map +0 -1
- package/dist/chunk-4BZJ4LAB.js.map +0 -1
- package/dist/chunk-6ISF5KNQ.mjs.map +0 -1
- package/dist/chunk-ORU7KLCY.js.map +0 -1
- package/dist/chunk-ZZ6E33FO.mjs.map +0 -1
- /package/dist/{BlockNoteViewer-Y2ZM4ESA.mjs.map → BlockNoteViewer-MDPO6BXN.mjs.map} +0 -0
- /package/dist/{chunk-46S3KPLU.mjs.map → chunk-ILFFC6BG.mjs.map} +0 -0
- /package/dist/{chunk-HZVVRIJQ.mjs.map → chunk-IUS2RHCX.mjs.map} +0 -0
- /package/dist/{chunk-ZO3QQ5CQ.mjs.map → chunk-MQDR6MEE.mjs.map} +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { ChevronRight, CreditCard } from "lucide-react";
|
|
4
5
|
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "../../../../shadcnui";
|
|
5
6
|
import { StripeSubscriptionInterface, SubscriptionStatus } from "../../stripe-subscription";
|
|
7
|
+
import { formatDate, useBillingFormatOptions } from "../utils";
|
|
6
8
|
|
|
7
9
|
type SubscriptionSummaryCardProps = {
|
|
8
10
|
subscriptions: StripeSubscriptionInterface[];
|
|
@@ -11,6 +13,9 @@ type SubscriptionSummaryCardProps = {
|
|
|
11
13
|
onManageClick: () => void;
|
|
12
14
|
};
|
|
13
15
|
|
|
16
|
+
/** Statuses with a `billing.ui.subscription_status.*` label; anything else Stripe adds is shown raw. */
|
|
17
|
+
const SUBSCRIPTION_STATUSES: string[] = Object.values(SubscriptionStatus);
|
|
18
|
+
|
|
14
19
|
function getStatusBadgeVariant(status: SubscriptionStatus): "default" | "secondary" | "destructive" | "outline" {
|
|
15
20
|
switch (status) {
|
|
16
21
|
case SubscriptionStatus.ACTIVE:
|
|
@@ -26,22 +31,14 @@ function getStatusBadgeVariant(status: SubscriptionStatus): "default" | "seconda
|
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
function
|
|
30
|
-
return new Date(date).toLocaleDateString(undefined, {
|
|
31
|
-
year: "numeric",
|
|
32
|
-
month: "short",
|
|
33
|
-
day: "numeric",
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function formatPlanName(subscription: StripeSubscriptionInterface): string {
|
|
34
|
+
function formatPlanName(subscription: StripeSubscriptionInterface, fallback: string): string {
|
|
38
35
|
const productName = subscription.price?.product?.name || "";
|
|
39
36
|
const nickname = subscription.price?.nickname || "";
|
|
40
37
|
|
|
41
38
|
if (productName && nickname) {
|
|
42
39
|
return `${productName} - ${nickname}`;
|
|
43
40
|
}
|
|
44
|
-
return productName || nickname ||
|
|
41
|
+
return productName || nickname || fallback;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
export function SubscriptionSummaryCard({
|
|
@@ -50,11 +47,13 @@ export function SubscriptionSummaryCard({
|
|
|
50
47
|
error,
|
|
51
48
|
onManageClick,
|
|
52
49
|
}: SubscriptionSummaryCardProps) {
|
|
50
|
+
const t = useTranslations();
|
|
51
|
+
const fmt = useBillingFormatOptions();
|
|
53
52
|
if (loading) {
|
|
54
53
|
return (
|
|
55
54
|
<Card>
|
|
56
55
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
57
|
-
<CardTitle>
|
|
56
|
+
<CardTitle>{t("billing.ui.subscriptions.title")}</CardTitle>
|
|
58
57
|
<CreditCard className="h-4 w-4 text-muted-foreground" />
|
|
59
58
|
</CardHeader>
|
|
60
59
|
<CardContent>
|
|
@@ -70,7 +69,7 @@ export function SubscriptionSummaryCard({
|
|
|
70
69
|
return (
|
|
71
70
|
<Card>
|
|
72
71
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
73
|
-
<CardTitle>
|
|
72
|
+
<CardTitle>{t("billing.ui.subscriptions.title")}</CardTitle>
|
|
74
73
|
<CreditCard className="h-4 w-4 text-muted-foreground" />
|
|
75
74
|
</CardHeader>
|
|
76
75
|
<CardContent>
|
|
@@ -88,14 +87,14 @@ export function SubscriptionSummaryCard({
|
|
|
88
87
|
return (
|
|
89
88
|
<Card className="cursor-pointer hover:bg-accent/50 transition-colors" onClick={onManageClick}>
|
|
90
89
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
91
|
-
<CardTitle>
|
|
90
|
+
<CardTitle>{t("billing.ui.subscriptions.title")}</CardTitle>
|
|
92
91
|
<CreditCard className="h-4 w-4 text-muted-foreground" />
|
|
93
92
|
</CardHeader>
|
|
94
93
|
<CardContent>
|
|
95
94
|
{subscriptions.length === 0 ? (
|
|
96
95
|
<div className="space-y-2">
|
|
97
|
-
<p className="text-xl font-bold text-muted-foreground">
|
|
98
|
-
<p className="text-xs text-muted-foreground">
|
|
96
|
+
<p className="text-xl font-bold text-muted-foreground">{t("billing.ui.cards.subscriptions.empty_title")}</p>
|
|
97
|
+
<p className="text-xs text-muted-foreground">{t("billing.ui.cards.subscriptions.empty_description")}</p>
|
|
99
98
|
<Button
|
|
100
99
|
variant="outline"
|
|
101
100
|
size="sm"
|
|
@@ -105,14 +104,16 @@ export function SubscriptionSummaryCard({
|
|
|
105
104
|
onManageClick();
|
|
106
105
|
}}
|
|
107
106
|
>
|
|
108
|
-
|
|
107
|
+
{t("billing.ui.cards.subscriptions.view_plans")}
|
|
109
108
|
<ChevronRight className="h-4 w-4 ms-1 rtl:rotate-180" />
|
|
110
109
|
</Button>
|
|
111
110
|
</div>
|
|
112
111
|
) : primarySubscription ? (
|
|
113
112
|
<div className="space-y-2">
|
|
114
113
|
<div className="flex items-center gap-2">
|
|
115
|
-
<p className="text-xl font-bold">
|
|
114
|
+
<p className="text-xl font-bold">
|
|
115
|
+
{formatPlanName(primarySubscription, t("billing.ui.cards.subscriptions.fallback_name"))}
|
|
116
|
+
</p>
|
|
116
117
|
<Badge
|
|
117
118
|
variant={
|
|
118
119
|
primarySubscription.cancelAtPeriodEnd
|
|
@@ -120,18 +121,30 @@ export function SubscriptionSummaryCard({
|
|
|
120
121
|
: getStatusBadgeVariant(primarySubscription.status)
|
|
121
122
|
}
|
|
122
123
|
>
|
|
123
|
-
{primarySubscription.cancelAtPeriodEnd
|
|
124
|
+
{primarySubscription.cancelAtPeriodEnd
|
|
125
|
+
? t("billing.ui.cards.subscriptions.canceling")
|
|
126
|
+
: SUBSCRIPTION_STATUSES.includes(primarySubscription.status)
|
|
127
|
+
? t(`billing.ui.subscription_status.${primarySubscription.status}`)
|
|
128
|
+
: primarySubscription.status}
|
|
124
129
|
</Badge>
|
|
125
130
|
</div>
|
|
126
131
|
<p className="text-xs text-muted-foreground">
|
|
127
132
|
{!primarySubscription.price?.isTrial
|
|
128
|
-
?
|
|
133
|
+
? t("billing.ui.cards.subscriptions.ends_on", {
|
|
134
|
+
date: formatDate(primarySubscription.currentPeriodEnd, fmt),
|
|
135
|
+
})
|
|
129
136
|
: primarySubscription.cancelAtPeriodEnd
|
|
130
|
-
?
|
|
131
|
-
|
|
137
|
+
? t("billing.ui.cards.subscriptions.cancels_on", {
|
|
138
|
+
date: formatDate(primarySubscription.currentPeriodEnd, fmt),
|
|
139
|
+
})
|
|
140
|
+
: t("billing.ui.cards.subscriptions.renews_on", {
|
|
141
|
+
date: formatDate(primarySubscription.currentPeriodEnd, fmt),
|
|
142
|
+
})}
|
|
132
143
|
</p>
|
|
133
144
|
{activeSubscriptions.length > 1 && (
|
|
134
|
-
<p className="text-xs text-muted-foreground"
|
|
145
|
+
<p className="text-xs text-muted-foreground">
|
|
146
|
+
{t("billing.ui.cards.subscriptions.more", { count: activeSubscriptions.length - 1 })}
|
|
147
|
+
</p>
|
|
135
148
|
)}
|
|
136
149
|
</div>
|
|
137
150
|
) : null}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { CreditCard, Loader2, Wallet } from "lucide-react";
|
|
4
5
|
import { useSearchParams } from "next/navigation";
|
|
5
6
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
@@ -51,6 +52,7 @@ type ErrorState = {
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
export function BillingDashboardContainer() {
|
|
55
|
+
const t = useTranslations();
|
|
54
56
|
const [data, setData] = useState<DataState>({
|
|
55
57
|
customer: null,
|
|
56
58
|
subscriptions: [],
|
|
@@ -68,6 +70,7 @@ export function BillingDashboardContainer() {
|
|
|
68
70
|
usage: true,
|
|
69
71
|
});
|
|
70
72
|
|
|
73
|
+
// Error state holds message keys; they are translated at render time.
|
|
71
74
|
const [errors, setErrors] = useState<ErrorState>({
|
|
72
75
|
customer: null,
|
|
73
76
|
subscriptions: null,
|
|
@@ -118,7 +121,7 @@ export function BillingDashboardContainer() {
|
|
|
118
121
|
if ((error as { status?: number })?.status === 404) {
|
|
119
122
|
setNoCustomerExists(true);
|
|
120
123
|
} else {
|
|
121
|
-
setErrors((prev) => ({ ...prev, customer: "
|
|
124
|
+
setErrors((prev) => ({ ...prev, customer: "billing.ui.dashboard.errors.load_customer" }));
|
|
122
125
|
}
|
|
123
126
|
} finally {
|
|
124
127
|
setLoading((prev) => ({ ...prev, customer: false }));
|
|
@@ -146,7 +149,7 @@ export function BillingDashboardContainer() {
|
|
|
146
149
|
return subscriptions;
|
|
147
150
|
} catch (error) {
|
|
148
151
|
console.error("[BillingDashboard] Failed to load subscriptions:", error);
|
|
149
|
-
setErrors((prev) => ({ ...prev, subscriptions: "
|
|
152
|
+
setErrors((prev) => ({ ...prev, subscriptions: "billing.ui.dashboard.errors.load_subscriptions" }));
|
|
150
153
|
return [];
|
|
151
154
|
} finally {
|
|
152
155
|
setLoading((prev) => ({ ...prev, subscriptions: false }));
|
|
@@ -161,7 +164,7 @@ export function BillingDashboardContainer() {
|
|
|
161
164
|
setErrors((prev) => ({ ...prev, paymentMethods: null }));
|
|
162
165
|
} catch (error) {
|
|
163
166
|
console.error("[BillingDashboard] Failed to load payment methods:", error);
|
|
164
|
-
setErrors((prev) => ({ ...prev, paymentMethods: "
|
|
167
|
+
setErrors((prev) => ({ ...prev, paymentMethods: "billing.ui.dashboard.errors.load_payment_methods" }));
|
|
165
168
|
} finally {
|
|
166
169
|
setLoading((prev) => ({ ...prev, paymentMethods: false }));
|
|
167
170
|
}
|
|
@@ -175,7 +178,7 @@ export function BillingDashboardContainer() {
|
|
|
175
178
|
setErrors((prev) => ({ ...prev, invoices: null }));
|
|
176
179
|
} catch (error) {
|
|
177
180
|
console.error("[BillingDashboard] Failed to load invoices:", error);
|
|
178
|
-
setErrors((prev) => ({ ...prev, invoices: "
|
|
181
|
+
setErrors((prev) => ({ ...prev, invoices: "billing.ui.dashboard.errors.load_invoices" }));
|
|
179
182
|
} finally {
|
|
180
183
|
setLoading((prev) => ({ ...prev, invoices: false }));
|
|
181
184
|
}
|
|
@@ -206,7 +209,7 @@ export function BillingDashboardContainer() {
|
|
|
206
209
|
await fetchAllData();
|
|
207
210
|
} catch (error) {
|
|
208
211
|
console.error("[BillingDashboard] Failed to create customer:", error);
|
|
209
|
-
setErrors((prev) => ({ ...prev, customer: "
|
|
212
|
+
setErrors((prev) => ({ ...prev, customer: "billing.ui.dashboard.errors.setup" }));
|
|
210
213
|
} finally {
|
|
211
214
|
setCreatingCustomer(false);
|
|
212
215
|
}
|
|
@@ -242,7 +245,7 @@ export function BillingDashboardContainer() {
|
|
|
242
245
|
setErrors((prev) => ({ ...prev, usage: null }));
|
|
243
246
|
} catch (error) {
|
|
244
247
|
console.error("[BillingDashboard] Failed to load usage data:", error);
|
|
245
|
-
setErrors((prev) => ({ ...prev, usage: "
|
|
248
|
+
setErrors((prev) => ({ ...prev, usage: "billing.ui.dashboard.errors.load_usage" }));
|
|
246
249
|
} finally {
|
|
247
250
|
setLoading((prev) => ({ ...prev, usage: false }));
|
|
248
251
|
}
|
|
@@ -311,13 +314,13 @@ export function BillingDashboardContainer() {
|
|
|
311
314
|
const getModalTitle = (type: ModalType): string => {
|
|
312
315
|
switch (type) {
|
|
313
316
|
case "subscriptions":
|
|
314
|
-
return "
|
|
317
|
+
return t("billing.ui.dashboard.modal.subscriptions");
|
|
315
318
|
case "payment-methods":
|
|
316
|
-
return "
|
|
319
|
+
return t("billing.ui.payment_methods.title");
|
|
317
320
|
case "invoices":
|
|
318
|
-
return "
|
|
321
|
+
return t("billing.ui.dashboard.modal.invoices");
|
|
319
322
|
case "usage":
|
|
320
|
-
return "
|
|
323
|
+
return t("billing.ui.usage.title");
|
|
321
324
|
default:
|
|
322
325
|
return "";
|
|
323
326
|
}
|
|
@@ -331,7 +334,7 @@ export function BillingDashboardContainer() {
|
|
|
331
334
|
{/* Header */}
|
|
332
335
|
<div className="flex items-center gap-x-3">
|
|
333
336
|
<Wallet className="h-8 w-8" />
|
|
334
|
-
<h1 className="text-primary text-3xl font-semibold">
|
|
337
|
+
<h1 className="text-primary text-3xl font-semibold">{t("billing.title")}</h1>
|
|
335
338
|
</div>
|
|
336
339
|
|
|
337
340
|
{/* Initial Loading State */}
|
|
@@ -350,27 +353,24 @@ export function BillingDashboardContainer() {
|
|
|
350
353
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
|
|
351
354
|
<CreditCard className="h-8 w-8 text-primary" />
|
|
352
355
|
</div>
|
|
353
|
-
<CardTitle>
|
|
354
|
-
<CardDescription>
|
|
355
|
-
Your company doesn't have a billing account yet. Set one up to manage subscriptions, payment methods,
|
|
356
|
-
and view invoices.
|
|
357
|
-
</CardDescription>
|
|
356
|
+
<CardTitle>{t("billing.ui.dashboard.setup_title")}</CardTitle>
|
|
357
|
+
<CardDescription>{t("billing.ui.dashboard.setup_description")}</CardDescription>
|
|
358
358
|
</CardHeader>
|
|
359
359
|
<CardContent className="flex justify-center pb-8">
|
|
360
360
|
<Button onClick={handleCreateCustomer} disabled={creatingCustomer} size="lg">
|
|
361
361
|
{creatingCustomer ? (
|
|
362
362
|
<>
|
|
363
363
|
<Loader2 className="me-2 h-4 w-4 animate-spin" />
|
|
364
|
-
|
|
364
|
+
{t("billing.ui.dashboard.setting_up")}
|
|
365
365
|
</>
|
|
366
366
|
) : (
|
|
367
|
-
"
|
|
367
|
+
t("billing.ui.dashboard.setup_button")
|
|
368
368
|
)}
|
|
369
369
|
</Button>
|
|
370
370
|
</CardContent>
|
|
371
371
|
{errors.customer && (
|
|
372
372
|
<CardContent className="pt-0">
|
|
373
|
-
<p className="text-center text-destructive text-xs/relaxed">{errors.customer}</p>
|
|
373
|
+
<p className="text-center text-destructive text-xs/relaxed">{t(errors.customer)}</p>
|
|
374
374
|
</CardContent>
|
|
375
375
|
)}
|
|
376
376
|
</Card>
|
|
@@ -394,7 +394,7 @@ export function BillingDashboardContainer() {
|
|
|
394
394
|
<SubscriptionSummaryCard
|
|
395
395
|
subscriptions={data.subscriptions}
|
|
396
396
|
loading={loading.subscriptions}
|
|
397
|
-
error={errors.subscriptions
|
|
397
|
+
error={errors.subscriptions ? t(errors.subscriptions) : undefined}
|
|
398
398
|
onManageClick={() => {
|
|
399
399
|
if (data.subscriptions.length === 0) {
|
|
400
400
|
// No subscriptions - open wizard directly
|
|
@@ -410,20 +410,20 @@ export function BillingDashboardContainer() {
|
|
|
410
410
|
paymentMethods={data.paymentMethods}
|
|
411
411
|
defaultPaymentMethodId={data.customer?.defaultPaymentMethodId}
|
|
412
412
|
loading={loading.paymentMethods}
|
|
413
|
-
error={errors.paymentMethods
|
|
413
|
+
error={errors.paymentMethods ? t(errors.paymentMethods) : undefined}
|
|
414
414
|
onManageClick={() => setActiveModal("payment-methods")}
|
|
415
415
|
/>
|
|
416
416
|
|
|
417
417
|
<CustomerInfoCard
|
|
418
418
|
customer={data.customer}
|
|
419
419
|
loading={loading.customer}
|
|
420
|
-
error={errors.customer
|
|
420
|
+
error={errors.customer ? t(errors.customer) : undefined}
|
|
421
421
|
/>
|
|
422
422
|
|
|
423
423
|
<InvoicesSummaryCard
|
|
424
424
|
invoices={data.invoices}
|
|
425
425
|
loading={loading.invoices}
|
|
426
|
-
error={errors.invoices
|
|
426
|
+
error={errors.invoices ? t(errors.invoices) : undefined}
|
|
427
427
|
onViewAllClick={() => setActiveModal("invoices")}
|
|
428
428
|
/>
|
|
429
429
|
|
|
@@ -433,7 +433,7 @@ export function BillingDashboardContainer() {
|
|
|
433
433
|
meters={data.meters}
|
|
434
434
|
summaries={data.meterSummaries}
|
|
435
435
|
loading={loading.usage}
|
|
436
|
-
error={errors.usage
|
|
436
|
+
error={errors.usage ? t(errors.usage) : undefined}
|
|
437
437
|
onViewDetailsClick={() => setActiveModal("usage")}
|
|
438
438
|
/>
|
|
439
439
|
)}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
|
|
3
|
+
import { formatCurrency, formatFixed, formatInterval } from "../currency";
|
|
4
|
+
import { formatDate } from "../date";
|
|
5
|
+
|
|
6
|
+
const price = (recurring?: { interval: string; intervalCount: number }, priceType = "recurring") =>
|
|
7
|
+
({ priceType, recurring }) as unknown as StripePriceInterface;
|
|
8
|
+
|
|
9
|
+
/** Echoes the key and values so a test can see exactly what was asked for. */
|
|
10
|
+
const t = (key: string, values?: Record<string, any>) => (values ? `${key}|${JSON.stringify(values)}` : key);
|
|
11
|
+
|
|
12
|
+
describe("formatInterval", () => {
|
|
13
|
+
it("keeps the English output when no translator is passed", () => {
|
|
14
|
+
expect(formatInterval(price(undefined, "one_time"))).toBe("one-time");
|
|
15
|
+
expect(formatInterval(price({ interval: "month", intervalCount: 1 }))).toBe("/month");
|
|
16
|
+
expect(formatInterval(price({ interval: "week", intervalCount: 2 }))).toBe("/2 weeks");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("asks the translator for every word it emits", () => {
|
|
20
|
+
expect(formatInterval(price(undefined, "one_time"), { t })).toBe("billing.ui.interval.one_time");
|
|
21
|
+
expect(formatInterval(price({ interval: "day", intervalCount: 1 }), { t })).toBe("billing.ui.interval.per_day");
|
|
22
|
+
expect(formatInterval(price({ interval: "year", intervalCount: 1 }), { t })).toBe("billing.ui.interval.per_year");
|
|
23
|
+
expect(formatInterval(price({ interval: "month", intervalCount: 3 }), { t })).toBe(
|
|
24
|
+
'billing.ui.interval.per_months|{"count":3}',
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("formatCurrency", () => {
|
|
30
|
+
it("keeps the en-US output and the $0.00 literal without a locale", () => {
|
|
31
|
+
expect(formatCurrency(999, "usd")).toBe("$9.99");
|
|
32
|
+
expect(formatCurrency(undefined, "usd")).toBe("$0.00");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("formats with the given locale", () => {
|
|
36
|
+
expect(formatCurrency(999, "eur", { locale: "it" })).toMatch(/^9,99\s€$/);
|
|
37
|
+
expect(formatCurrency(undefined, "eur", { locale: "it" })).toMatch(/^0,00\s€$/);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("formatDate", () => {
|
|
42
|
+
const date = new Date(2025, 0, 15, 12);
|
|
43
|
+
|
|
44
|
+
it("keeps the English output without options", () => {
|
|
45
|
+
expect(formatDate(date)).toBe("Jan 15, 2025");
|
|
46
|
+
expect(formatDate(undefined)).toBe("N/A");
|
|
47
|
+
expect(formatDate(new Date("not a date"))).toBe("Invalid Date");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("uses the locale and the translator when given", () => {
|
|
51
|
+
expect(formatDate(date, { locale: "it", t })).toBe("15 gen 2025");
|
|
52
|
+
expect(formatDate(undefined, { locale: "it", t })).toBe("billing.ui.not_available");
|
|
53
|
+
expect(formatDate(new Date("not a date"), { locale: "it", t })).toBe("billing.ui.invalid_date");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("formatFixed", () => {
|
|
58
|
+
it("matches toFixed in English and uses the locale's decimal separator otherwise", () => {
|
|
59
|
+
expect(formatFixed(12.345, "en", 1)).toBe((12.345).toFixed(1));
|
|
60
|
+
expect(formatFixed(1234.5, "en", 1)).toBe("1234.5");
|
|
61
|
+
expect(formatFixed(12.5, "it", 1)).toBe("12,5");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { StripePriceInterface } from "../../stripe-price/data/stripe-price.interface";
|
|
2
|
+
import { BillingFormatOptions, DEFAULT_BILLING_LOCALE } from "./format-options";
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
* @param price - Stripe price object
|
|
6
|
-
* @returns Formatted interval string (e.g., "/month", "/year", "/2 weeks", "one-time")
|
|
7
|
-
*/
|
|
8
|
-
export function formatInterval(price: StripePriceInterface): string {
|
|
4
|
+
/** English output kept for callers that pass no translator. */
|
|
5
|
+
function formatIntervalEnglish(price: StripePriceInterface): string {
|
|
9
6
|
if (price.priceType === "one_time" || !price.recurring) {
|
|
10
7
|
return "one-time";
|
|
11
8
|
}
|
|
@@ -22,20 +19,68 @@ export function formatInterval(price: StripePriceInterface): string {
|
|
|
22
19
|
return `/${intervalCount} ${pluralInterval}`;
|
|
23
20
|
}
|
|
24
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Format price recurring interval for display
|
|
24
|
+
* @param price - Stripe price object
|
|
25
|
+
* @param options - `t` translates the interval words; without it the output is English
|
|
26
|
+
* @returns Formatted interval string (e.g., "/month", "/year", "/2 weeks", "one-time")
|
|
27
|
+
*/
|
|
28
|
+
export function formatInterval(price: StripePriceInterface, options: BillingFormatOptions = {}): string {
|
|
29
|
+
const { t } = options;
|
|
30
|
+
if (!t) return formatIntervalEnglish(price);
|
|
31
|
+
|
|
32
|
+
if (price.priceType === "one_time" || !price.recurring) {
|
|
33
|
+
return t("billing.ui.interval.one_time");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { interval, intervalCount } = price.recurring;
|
|
37
|
+
|
|
38
|
+
if (intervalCount === 1) {
|
|
39
|
+
switch (interval) {
|
|
40
|
+
case "day":
|
|
41
|
+
return t("billing.ui.interval.per_day");
|
|
42
|
+
case "week":
|
|
43
|
+
return t("billing.ui.interval.per_week");
|
|
44
|
+
case "month":
|
|
45
|
+
return t("billing.ui.interval.per_month");
|
|
46
|
+
default:
|
|
47
|
+
return t("billing.ui.interval.per_year");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
switch (interval) {
|
|
52
|
+
case "day":
|
|
53
|
+
return t("billing.ui.interval.per_days", { count: intervalCount });
|
|
54
|
+
case "week":
|
|
55
|
+
return t("billing.ui.interval.per_weeks", { count: intervalCount });
|
|
56
|
+
case "month":
|
|
57
|
+
return t("billing.ui.interval.per_months", { count: intervalCount });
|
|
58
|
+
default:
|
|
59
|
+
return t("billing.ui.interval.per_years", { count: intervalCount });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
25
63
|
/**
|
|
26
64
|
* Format currency amount from cents to localized currency string
|
|
27
65
|
* @param amount - Amount in cents
|
|
28
66
|
* @param currency - Currency code (e.g., "USD", "EUR", "GBP")
|
|
67
|
+
* @param options - `locale` drives number formatting; defaults to "en-US"
|
|
29
68
|
* @returns Formatted currency string (e.g., "$9.99", "€9,99")
|
|
30
69
|
*/
|
|
31
|
-
export function formatCurrency(
|
|
32
|
-
|
|
70
|
+
export function formatCurrency(
|
|
71
|
+
amount: number | undefined,
|
|
72
|
+
currency: string,
|
|
73
|
+
options: BillingFormatOptions = {},
|
|
74
|
+
): string {
|
|
75
|
+
const { locale } = options;
|
|
76
|
+
// No locale: the historical literal. With one, zero goes through Intl like any amount.
|
|
77
|
+
if (amount === undefined && !locale) return "$0.00";
|
|
33
78
|
|
|
34
79
|
// Convert cents to dollars
|
|
35
|
-
const dollars = amount / 100;
|
|
80
|
+
const dollars = (amount ?? 0) / 100;
|
|
36
81
|
|
|
37
82
|
try {
|
|
38
|
-
return new Intl.NumberFormat(
|
|
83
|
+
return new Intl.NumberFormat(locale ?? DEFAULT_BILLING_LOCALE, {
|
|
39
84
|
style: "currency",
|
|
40
85
|
currency: currency.toUpperCase(),
|
|
41
86
|
minimumFractionDigits: 2,
|
|
@@ -47,3 +92,15 @@ export function formatCurrency(amount: number | undefined, currency: string): st
|
|
|
47
92
|
return `$${dollars.toFixed(2)}`;
|
|
48
93
|
}
|
|
49
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `value.toFixed(digits)` with the locale's decimal separator ("12.5" in English,
|
|
98
|
+
* "12,5" in Italian). No grouping, so English output matches `toFixed` exactly.
|
|
99
|
+
*/
|
|
100
|
+
export function formatFixed(value: number, locale: string | undefined, digits: number): string {
|
|
101
|
+
return new Intl.NumberFormat(locale ?? DEFAULT_BILLING_LOCALE, {
|
|
102
|
+
minimumFractionDigits: digits,
|
|
103
|
+
maximumFractionDigits: digits,
|
|
104
|
+
useGrouping: false,
|
|
105
|
+
}).format(value);
|
|
106
|
+
}
|
|
@@ -1,21 +1,26 @@
|
|
|
1
|
+
import { BillingFormatOptions, DEFAULT_BILLING_LOCALE } from "./format-options";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Format a date to a localized date string
|
|
3
5
|
* @param date - Date object or ISO string
|
|
6
|
+
* @param options - `locale` drives the date format (default "en-US"); `t` translates the
|
|
7
|
+
* placeholder shown for a missing or invalid date (default English)
|
|
4
8
|
* @returns Formatted date string (e.g., "Jan 15, 2025")
|
|
5
9
|
*/
|
|
6
|
-
export function formatDate(date: Date | string | undefined): string {
|
|
7
|
-
|
|
10
|
+
export function formatDate(date: Date | string | undefined, options: BillingFormatOptions = {}): string {
|
|
11
|
+
const { locale, t } = options;
|
|
12
|
+
if (!date) return t ? t("billing.ui.not_available") : "N/A";
|
|
8
13
|
|
|
9
14
|
const dateObj = typeof date === "string" ? new Date(date) : date;
|
|
10
15
|
|
|
11
16
|
try {
|
|
12
|
-
return new Intl.DateTimeFormat(
|
|
17
|
+
return new Intl.DateTimeFormat(locale ?? DEFAULT_BILLING_LOCALE, {
|
|
13
18
|
month: "short",
|
|
14
19
|
day: "numeric",
|
|
15
20
|
year: "numeric",
|
|
16
21
|
}).format(dateObj);
|
|
17
22
|
} catch (error) {
|
|
18
23
|
console.error("Error formatting date:", error);
|
|
19
|
-
return "Invalid Date";
|
|
24
|
+
return t ? t("billing.ui.invalid_date") : "Invalid Date";
|
|
20
25
|
}
|
|
21
26
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A message translator, as returned by `useTranslations()`. */
|
|
2
|
+
export type BillingTranslator = (key: string, values?: Record<string, any>) => string;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What the billing formatters need from the caller's React context. The
|
|
6
|
+
* formatters are plain functions and cannot call hooks, so a component reads
|
|
7
|
+
* `useI18nLocale()` and `useTranslations()` and passes both here.
|
|
8
|
+
*
|
|
9
|
+
* Both are optional so existing callers keep their output: without a `locale`
|
|
10
|
+
* the formatters use "en-US", without `t` they return the English literals they
|
|
11
|
+
* always returned.
|
|
12
|
+
*/
|
|
13
|
+
export type BillingFormatOptions = {
|
|
14
|
+
/** BCP 47 locale for `Intl` (the app's active locale, from `useI18nLocale()`). */
|
|
15
|
+
locale?: string;
|
|
16
|
+
/** Translator for the words the formatters emit. */
|
|
17
|
+
t?: BillingTranslator;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_BILLING_LOCALE = "en-US";
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export { formatCurrency, formatInterval } from "./currency";
|
|
1
|
+
export { formatCurrency, formatFixed, formatInterval } from "./currency";
|
|
2
2
|
export { formatDate } from "./date";
|
|
3
|
+
export type { BillingFormatOptions, BillingTranslator } from "./format-options";
|
|
4
|
+
export { useBillingFormatOptions } from "./useBillingFormatOptions";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useTranslations } from "next-intl";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { useI18nLocale } from "../../../../i18n";
|
|
4
|
+
import { BillingFormatOptions } from "./format-options";
|
|
5
|
+
|
|
6
|
+
/** The active locale and translator, ready to pass to the billing formatters. */
|
|
7
|
+
export function useBillingFormatOptions(): Required<BillingFormatOptions> {
|
|
8
|
+
const t = useTranslations();
|
|
9
|
+
const locale = useI18nLocale();
|
|
10
|
+
return useMemo(() => ({ locale, t }), [locale, t]);
|
|
11
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { AlertCircle } from "lucide-react";
|
|
4
5
|
import { Button } from "../../../../shadcnui";
|
|
5
6
|
import { StripeSubscriptionInterface, SubscriptionStatus } from "../../stripe-subscription";
|
|
@@ -11,20 +12,19 @@ type BillingAlertBannerProps = {
|
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export function BillingAlertBanner({ subscription, onUpdatePayment, onAddPayment }: BillingAlertBannerProps) {
|
|
15
|
+
const t = useTranslations();
|
|
14
16
|
// Payment failed alert (past_due)
|
|
15
17
|
if (subscription.status === SubscriptionStatus.PAST_DUE) {
|
|
16
18
|
return (
|
|
17
19
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-start gap-x-3">
|
|
18
20
|
<AlertCircle className="text-destructive mt-0.5 h-5 w-5" />
|
|
19
21
|
<div className="flex-1">
|
|
20
|
-
<h3 className="text-destructive text-sm font-medium">
|
|
21
|
-
<p className="text-destructive mt-1 text-sm">
|
|
22
|
-
Your last payment failed. Please update your payment method to avoid service interruption.
|
|
23
|
-
</p>
|
|
22
|
+
<h3 className="text-destructive text-sm font-medium">{t("billing.ui.alert.payment_failed_title")}</h3>
|
|
23
|
+
<p className="text-destructive mt-1 text-sm">{t("billing.ui.alert.payment_failed_description")}</p>
|
|
24
24
|
</div>
|
|
25
25
|
{onUpdatePayment && (
|
|
26
26
|
<Button variant="outline" size="sm" onClick={onUpdatePayment} className="text-destructive border-red-300">
|
|
27
|
-
|
|
27
|
+
{t("billing.ui.alert.update_payment_method")}
|
|
28
28
|
</Button>
|
|
29
29
|
)}
|
|
30
30
|
</div>
|
|
@@ -42,15 +42,14 @@ export function BillingAlertBanner({ subscription, onUpdatePayment, onAddPayment
|
|
|
42
42
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 flex items-start gap-x-3">
|
|
43
43
|
<AlertCircle className="text-warning mt-0.5 h-5 w-5" />
|
|
44
44
|
<div className="flex-1">
|
|
45
|
-
<h3 className="text-warning text-sm font-medium">
|
|
45
|
+
<h3 className="text-warning text-sm font-medium">{t("billing.ui.alert.trial_ending_title")}</h3>
|
|
46
46
|
<p className="text-warning mt-1 text-sm">
|
|
47
|
-
|
|
48
|
-
continue your subscription.
|
|
47
|
+
{t("billing.ui.alert.trial_ending_description", { count: daysRemaining })}
|
|
49
48
|
</p>
|
|
50
49
|
</div>
|
|
51
50
|
{onAddPayment && (
|
|
52
51
|
<Button variant="outline" size="sm" onClick={onAddPayment} className="text-warning border-yellow-300">
|
|
53
|
-
|
|
52
|
+
{t("billing.ui.payment_methods.add")}
|
|
54
53
|
</Button>
|
|
55
54
|
)}
|
|
56
55
|
</div>
|