@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,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { Download, ExternalLink, RefreshCw } from "lucide-react";
|
|
4
5
|
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../../../../shadcnui";
|
|
5
|
-
import { formatCurrency, formatDate } from "../../../components/utils";
|
|
6
|
+
import { formatCurrency, formatDate, useBillingFormatOptions } from "../../../components/utils";
|
|
6
7
|
import { InvoiceStatus, StripeInvoiceInterface } from "../../data/stripe-invoice.interface";
|
|
7
8
|
import { InvoiceStatusBadge } from "../widgets/InvoiceStatusBadge";
|
|
8
9
|
|
|
@@ -19,6 +20,8 @@ export function InvoiceDetails({
|
|
|
19
20
|
onOpenChange,
|
|
20
21
|
onInvoiceChange: _onInvoiceChange,
|
|
21
22
|
}: InvoiceDetailsProps) {
|
|
23
|
+
const t = useTranslations();
|
|
24
|
+
const fmt = useBillingFormatOptions();
|
|
22
25
|
const handleDownloadPDF = () => {
|
|
23
26
|
if (invoice.stripePdfUrl) {
|
|
24
27
|
window.open(invoice.stripePdfUrl, "_blank");
|
|
@@ -42,67 +45,67 @@ export function InvoiceDetails({
|
|
|
42
45
|
return invoice.stripeInvoiceId.slice(-8);
|
|
43
46
|
};
|
|
44
47
|
|
|
45
|
-
const productName = invoice.subscription?.price?.product?.name || "
|
|
48
|
+
const productName = invoice.subscription?.price?.product?.name || t("billing.ui.cards.subscriptions.fallback_name");
|
|
46
49
|
|
|
47
50
|
return (
|
|
48
51
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
49
52
|
<DialogContent className="max-w-2xl">
|
|
50
53
|
<DialogHeader>
|
|
51
|
-
<DialogTitle>
|
|
52
|
-
<DialogDescription>{formatDate(invoice.periodStart)}</DialogDescription>
|
|
54
|
+
<DialogTitle>{t("billing.ui.invoice.title", { number: getInvoiceNumber() })}</DialogTitle>
|
|
55
|
+
<DialogDescription>{formatDate(invoice.periodStart, fmt)}</DialogDescription>
|
|
53
56
|
</DialogHeader>
|
|
54
57
|
|
|
55
58
|
<div className="space-y-6">
|
|
56
59
|
{/* Status */}
|
|
57
60
|
<div className="flex items-center gap-x-3">
|
|
58
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
61
|
+
<span className="text-sm font-medium text-muted-foreground">{t("billing.ui.labels.status")}</span>
|
|
59
62
|
<InvoiceStatusBadge status={invoice.status} />
|
|
60
63
|
</div>
|
|
61
64
|
|
|
62
65
|
{/* Invoice Info Grid */}
|
|
63
66
|
<div className="grid grid-cols-2 gap-4">
|
|
64
67
|
<div>
|
|
65
|
-
<span className="text-muted-foreground text-xs">
|
|
68
|
+
<span className="text-muted-foreground text-xs">{t("billing.ui.invoice.billing_period")}</span>
|
|
66
69
|
<p className="text-sm">
|
|
67
|
-
{formatDate(invoice.periodStart)} - {formatDate(invoice.periodEnd)}
|
|
70
|
+
{formatDate(invoice.periodStart, fmt)} - {formatDate(invoice.periodEnd, fmt)}
|
|
68
71
|
</p>
|
|
69
72
|
</div>
|
|
70
73
|
|
|
71
74
|
{invoice.dueDate && (
|
|
72
75
|
<div>
|
|
73
|
-
<span className="text-muted-foreground text-xs">
|
|
74
|
-
<p className="text-sm">{formatDate(invoice.dueDate)}</p>
|
|
76
|
+
<span className="text-muted-foreground text-xs">{t("billing.ui.invoice.due_date")}</span>
|
|
77
|
+
<p className="text-sm">{formatDate(invoice.dueDate, fmt)}</p>
|
|
75
78
|
</div>
|
|
76
79
|
)}
|
|
77
80
|
|
|
78
81
|
{invoice.paidAt && (
|
|
79
82
|
<div>
|
|
80
|
-
<span className="text-muted-foreground text-xs">
|
|
81
|
-
<p className="text-sm">{formatDate(invoice.paidAt)}</p>
|
|
83
|
+
<span className="text-muted-foreground text-xs">{t("billing.ui.invoice.paid_date")}</span>
|
|
84
|
+
<p className="text-sm">{formatDate(invoice.paidAt, fmt)}</p>
|
|
82
85
|
</div>
|
|
83
86
|
)}
|
|
84
87
|
|
|
85
88
|
<div>
|
|
86
|
-
<span className="text-muted-foreground text-xs">
|
|
89
|
+
<span className="text-muted-foreground text-xs">{t("billing.ui.invoice.attempt_count")}</span>
|
|
87
90
|
<p className="text-sm">{invoice.attemptCount}</p>
|
|
88
91
|
</div>
|
|
89
92
|
</div>
|
|
90
93
|
|
|
91
94
|
{/* Line Items */}
|
|
92
95
|
<div>
|
|
93
|
-
<h4 className="text-sm font-medium text-muted-foreground mb-2">
|
|
96
|
+
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t("billing.ui.invoice.line_items")}</h4>
|
|
94
97
|
<div className="border rounded-lg overflow-clip">
|
|
95
98
|
<table className="w-full">
|
|
96
99
|
<thead className="bg-muted">
|
|
97
100
|
<tr>
|
|
98
|
-
<th className="text-start p-3 text-sm font-medium">
|
|
99
|
-
<th className="text-end p-3 text-sm font-medium">
|
|
101
|
+
<th className="text-start p-3 text-sm font-medium">{t("billing.ui.columns.description")}</th>
|
|
102
|
+
<th className="text-end p-3 text-sm font-medium">{t("billing.ui.columns.amount")}</th>
|
|
100
103
|
</tr>
|
|
101
104
|
</thead>
|
|
102
105
|
<tbody>
|
|
103
106
|
<tr className="border-t">
|
|
104
107
|
<td className="p-3">{productName}</td>
|
|
105
|
-
<td className="p-3 text-end">{formatCurrency(invoice.subtotal, invoice.currency)}</td>
|
|
108
|
+
<td className="p-3 text-end">{formatCurrency(invoice.subtotal, invoice.currency, fmt)}</td>
|
|
106
109
|
</tr>
|
|
107
110
|
</tbody>
|
|
108
111
|
</table>
|
|
@@ -112,34 +115,36 @@ export function InvoiceDetails({
|
|
|
112
115
|
{/* Amount Breakdown */}
|
|
113
116
|
<div className="space-y-2 border-t pt-4">
|
|
114
117
|
<div className="flex justify-between">
|
|
115
|
-
<span className="text-muted-foreground text-xs">
|
|
116
|
-
<span className="text-sm">{formatCurrency(invoice.subtotal, invoice.currency)}</span>
|
|
118
|
+
<span className="text-muted-foreground text-xs">{t("billing.ui.invoice.subtotal")}</span>
|
|
119
|
+
<span className="text-sm">{formatCurrency(invoice.subtotal, invoice.currency, fmt)}</span>
|
|
117
120
|
</div>
|
|
118
121
|
|
|
119
122
|
{invoice.tax !== undefined && invoice.tax > 0 && (
|
|
120
123
|
<div className="flex justify-between">
|
|
121
|
-
<span className="text-muted-foreground text-xs">
|
|
122
|
-
<span className="text-sm">{formatCurrency(invoice.tax, invoice.currency)}</span>
|
|
124
|
+
<span className="text-muted-foreground text-xs">{t("billing.ui.invoice.tax")}</span>
|
|
125
|
+
<span className="text-sm">{formatCurrency(invoice.tax, invoice.currency, fmt)}</span>
|
|
123
126
|
</div>
|
|
124
127
|
)}
|
|
125
128
|
|
|
126
129
|
<div className="flex justify-between text-lg font-semibold border-t pt-2">
|
|
127
|
-
<span>
|
|
128
|
-
<span>{formatCurrency(invoice.total, invoice.currency)}</span>
|
|
130
|
+
<span>{t("billing.ui.invoice.total")}</span>
|
|
131
|
+
<span>{formatCurrency(invoice.total, invoice.currency, fmt)}</span>
|
|
129
132
|
</div>
|
|
130
133
|
|
|
131
134
|
{invoice.amountPaid > 0 && (
|
|
132
135
|
<div className="flex justify-between">
|
|
133
|
-
<span className="text-muted-foreground text-xs">
|
|
134
|
-
<span className="text-sm text-success">
|
|
136
|
+
<span className="text-muted-foreground text-xs">{t("billing.ui.invoice.amount_paid")}</span>
|
|
137
|
+
<span className="text-sm text-success">
|
|
138
|
+
{formatCurrency(invoice.amountPaid, invoice.currency, fmt)}
|
|
139
|
+
</span>
|
|
135
140
|
</div>
|
|
136
141
|
)}
|
|
137
142
|
|
|
138
143
|
{invoice.amountRemaining > 0 && (
|
|
139
144
|
<div className="flex justify-between">
|
|
140
|
-
<span className="text-muted-foreground text-xs">
|
|
145
|
+
<span className="text-muted-foreground text-xs">{t("billing.ui.invoice.amount_due")}</span>
|
|
141
146
|
<span className="text-sm text-destructive">
|
|
142
|
-
{formatCurrency(invoice.amountRemaining, invoice.currency)}
|
|
147
|
+
{formatCurrency(invoice.amountRemaining, invoice.currency, fmt)}
|
|
143
148
|
</span>
|
|
144
149
|
</div>
|
|
145
150
|
)}
|
|
@@ -150,21 +155,21 @@ export function InvoiceDetails({
|
|
|
150
155
|
{invoice.stripePdfUrl && (
|
|
151
156
|
<Button variant="outline" onClick={handleDownloadPDF}>
|
|
152
157
|
<Download className="me-2 h-4 w-4" />
|
|
153
|
-
|
|
158
|
+
{t("billing.ui.invoice.download_pdf")}
|
|
154
159
|
</Button>
|
|
155
160
|
)}
|
|
156
161
|
|
|
157
162
|
{invoice.status === InvoiceStatus.OPEN && invoice.attempted && (
|
|
158
163
|
<Button variant="default" onClick={handleRetryPayment}>
|
|
159
164
|
<RefreshCw className="me-2 h-4 w-4" />
|
|
160
|
-
|
|
165
|
+
{t("billing.ui.invoice.retry_payment")}
|
|
161
166
|
</Button>
|
|
162
167
|
)}
|
|
163
168
|
|
|
164
169
|
{invoice.stripeHostedInvoiceUrl && (
|
|
165
170
|
<Button variant="outline" onClick={handleViewInStripe}>
|
|
166
171
|
<ExternalLink className="me-2 h-4 w-4" />
|
|
167
|
-
|
|
172
|
+
{t("billing.ui.invoice.view_in_stripe")}
|
|
168
173
|
</Button>
|
|
169
174
|
)}
|
|
170
175
|
</div>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { useState } from "react";
|
|
4
5
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui";
|
|
5
|
-
import { formatCurrency, formatDate } from "../../../components/utils";
|
|
6
|
+
import { formatCurrency, formatDate, useBillingFormatOptions } from "../../../components/utils";
|
|
6
7
|
import { StripeInvoiceInterface } from "../../data/stripe-invoice.interface";
|
|
7
8
|
import { InvoiceDetails } from "../details/InvoiceDetails";
|
|
8
9
|
import { InvoiceStatusBadge } from "../widgets/InvoiceStatusBadge";
|
|
@@ -13,6 +14,8 @@ type InvoicesListProps = {
|
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
export function InvoicesList({ invoices, onInvoicesChange }: InvoicesListProps) {
|
|
17
|
+
const t = useTranslations();
|
|
18
|
+
const fmt = useBillingFormatOptions();
|
|
16
19
|
const [selectedInvoice, setSelectedInvoice] = useState<StripeInvoiceInterface | null>(null);
|
|
17
20
|
|
|
18
21
|
const handleRowClick = (invoice: StripeInvoiceInterface) => {
|
|
@@ -33,19 +36,19 @@ export function InvoicesList({ invoices, onInvoicesChange }: InvoicesListProps)
|
|
|
33
36
|
<Table>
|
|
34
37
|
<TableHeader className="bg-muted">
|
|
35
38
|
<TableRow>
|
|
36
|
-
<TableHead>
|
|
37
|
-
<TableHead>
|
|
38
|
-
<TableHead>
|
|
39
|
-
<TableHead className="text-end">
|
|
40
|
-
<TableHead>
|
|
39
|
+
<TableHead>{t("billing.ui.columns.invoice_number")}</TableHead>
|
|
40
|
+
<TableHead>{t("billing.ui.columns.date")}</TableHead>
|
|
41
|
+
<TableHead>{t("billing.ui.columns.status")}</TableHead>
|
|
42
|
+
<TableHead className="text-end">{t("billing.ui.columns.amount")}</TableHead>
|
|
43
|
+
<TableHead>{t("billing.ui.columns.period")}</TableHead>
|
|
41
44
|
</TableRow>
|
|
42
45
|
</TableHeader>
|
|
43
46
|
<TableBody>
|
|
44
47
|
{invoices.map((invoice) => {
|
|
45
48
|
const invoiceNumber = getInvoiceNumber(invoice);
|
|
46
|
-
const date = formatDate(invoice.periodStart);
|
|
47
|
-
const amount = formatCurrency(invoice.total, invoice.currency);
|
|
48
|
-
const period = `${formatDate(invoice.periodStart)} - ${formatDate(invoice.periodEnd)}`;
|
|
49
|
+
const date = formatDate(invoice.periodStart, fmt);
|
|
50
|
+
const amount = formatCurrency(invoice.total, invoice.currency, fmt);
|
|
51
|
+
const period = `${formatDate(invoice.periodStart, fmt)} - ${formatDate(invoice.periodEnd, fmt)}`;
|
|
49
52
|
|
|
50
53
|
return (
|
|
51
54
|
<TableRow
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { Badge, type BadgeProps } from "../../../../../shadcnui";
|
|
4
5
|
import { InvoiceStatus } from "../../data/stripe-invoice.interface";
|
|
5
6
|
|
|
@@ -8,35 +9,37 @@ type InvoiceStatusBadgeProps = {
|
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
type StatusConfig = {
|
|
12
|
+
/** Message key, translated at render time. */
|
|
11
13
|
label: string;
|
|
12
14
|
variant: BadgeProps["variant"];
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
const statusConfig: Record<InvoiceStatus, StatusConfig> = {
|
|
16
18
|
[InvoiceStatus.DRAFT]: {
|
|
17
|
-
label: "
|
|
19
|
+
label: "billing.ui.invoice_status.draft",
|
|
18
20
|
variant: "softGray",
|
|
19
21
|
},
|
|
20
22
|
[InvoiceStatus.OPEN]: {
|
|
21
|
-
label: "
|
|
23
|
+
label: "billing.ui.invoice_status.open",
|
|
22
24
|
variant: "softBlue",
|
|
23
25
|
},
|
|
24
26
|
[InvoiceStatus.PAID]: {
|
|
25
|
-
label: "
|
|
27
|
+
label: "billing.ui.invoice_status.paid",
|
|
26
28
|
variant: "softGreen",
|
|
27
29
|
},
|
|
28
30
|
[InvoiceStatus.VOID]: {
|
|
29
|
-
label: "
|
|
31
|
+
label: "billing.ui.invoice_status.void",
|
|
30
32
|
variant: "softGray",
|
|
31
33
|
},
|
|
32
34
|
[InvoiceStatus.UNCOLLECTIBLE]: {
|
|
33
|
-
label: "
|
|
35
|
+
label: "billing.ui.invoice_status.uncollectible",
|
|
34
36
|
variant: "softRed",
|
|
35
37
|
},
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
export function InvoiceStatusBadge({ status }: InvoiceStatusBadgeProps) {
|
|
41
|
+
const t = useTranslations();
|
|
39
42
|
const config = statusConfig[status] || statusConfig[InvoiceStatus.DRAFT];
|
|
40
43
|
|
|
41
|
-
return <Badge variant={config.variant}>{config.label}</Badge>;
|
|
44
|
+
return <Badge variant={config.variant}>{t(config.label)}</Badge>;
|
|
42
45
|
}
|
|
@@ -5,11 +5,12 @@ import { DetailField, SectionHeader } from "../../../../../components";
|
|
|
5
5
|
import { Modules } from "../../../../../core";
|
|
6
6
|
import { usePageUrlGenerator } from "../../../../../hooks";
|
|
7
7
|
import { Badge, Link } from "../../../../../shadcnui";
|
|
8
|
-
import { formatCurrency, formatInterval } from "../../../components/utils
|
|
8
|
+
import { formatCurrency, formatInterval, useBillingFormatOptions } from "../../../components/utils";
|
|
9
9
|
import { usePriceContext } from "../../../contexts/PriceContext";
|
|
10
10
|
|
|
11
11
|
export function PriceDetails() {
|
|
12
12
|
const t = useTranslations();
|
|
13
|
+
const fmt = useBillingFormatOptions();
|
|
13
14
|
const generateUrl = usePageUrlGenerator();
|
|
14
15
|
const { price } = usePriceContext();
|
|
15
16
|
|
|
@@ -49,14 +50,14 @@ export function PriceDetails() {
|
|
|
49
50
|
/>
|
|
50
51
|
<DetailField
|
|
51
52
|
label={t("billing.admin.prices.fields.amount")}
|
|
52
|
-
value={<span className="tabular-nums">{formatCurrency(price.unitAmount, price.currency)}</span>}
|
|
53
|
+
value={<span className="tabular-nums">{formatCurrency(price.unitAmount, price.currency, fmt)}</span>}
|
|
53
54
|
/>
|
|
54
55
|
<DetailField label={t("billing.admin.prices.fields.currency")} value={price.currency.toUpperCase()} />
|
|
55
56
|
<DetailField
|
|
56
57
|
label={t("billing.admin.prices.fields.type")}
|
|
57
58
|
value={isRecurring ? t("billing.admin.prices.type.recurring") : t("billing.admin.prices.type.oneTime")}
|
|
58
59
|
/>
|
|
59
|
-
<DetailField label={t("billing.admin.prices.fields.billing")} value={formatInterval(price)} />
|
|
60
|
+
<DetailField label={t("billing.admin.prices.fields.billing")} value={formatInterval(price, fmt)} />
|
|
60
61
|
{isRecurring && (
|
|
61
62
|
<DetailField
|
|
62
63
|
label={t("billing.admin.prices.fields.intervalCount")}
|
|
@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
|
|
|
5
5
|
import { useState } from "react";
|
|
6
6
|
import { errorToast } from "../../../../../components/errors";
|
|
7
7
|
import { Button, ConfirmDialog } from "../../../../../shadcnui";
|
|
8
|
+
import { useBillingFormatOptions } from "../../../components/utils";
|
|
8
9
|
import { priceLabel, usePriceContext } from "../../../contexts/PriceContext";
|
|
9
10
|
import { StripePriceInterface } from "../../data/stripe-price.interface";
|
|
10
11
|
|
|
@@ -20,6 +21,7 @@ type PriceArchiverProps = {
|
|
|
20
21
|
*/
|
|
21
22
|
export function PriceArchiver({ price }: PriceArchiverProps) {
|
|
22
23
|
const t = useTranslations();
|
|
24
|
+
const fmt = useBillingFormatOptions();
|
|
23
25
|
const { archivePrice, restorePrice } = usePriceContext();
|
|
24
26
|
const [open, setOpen] = useState<boolean>(false);
|
|
25
27
|
|
|
@@ -36,7 +38,7 @@ export function PriceArchiver({ price }: PriceArchiverProps) {
|
|
|
36
38
|
open={open}
|
|
37
39
|
onOpenChange={setOpen}
|
|
38
40
|
title={t(`billing.admin.prices.${scope}.title`)}
|
|
39
|
-
description={t(`billing.admin.prices.${scope}.description`, { label: priceLabel(price) })}
|
|
41
|
+
description={t(`billing.admin.prices.${scope}.description`, { label: priceLabel(price, fmt) })}
|
|
40
42
|
confirmLabel={t(`billing.admin.prices.${scope}.confirm`)}
|
|
41
43
|
cancelLabel={t("ui.buttons.cancel")}
|
|
42
44
|
destructive={!isArchived}
|
|
@@ -13,6 +13,7 @@ import { useI18nRouter } from "../../../../../i18n";
|
|
|
13
13
|
import { Alert, AlertDescription, AlertTitle, Button, Checkbox, Input } from "../../../../../shadcnui";
|
|
14
14
|
import { showToast } from "../../../../../utils/toast";
|
|
15
15
|
import { FeatureInterface, FeatureService } from "../../../../feature";
|
|
16
|
+
import { useBillingFormatOptions } from "../../../components/utils";
|
|
16
17
|
import { priceLabel, usePriceContext } from "../../../contexts/PriceContext";
|
|
17
18
|
import { StripePriceInput, StripePriceInterface } from "../../data/stripe-price.interface";
|
|
18
19
|
import { StripePriceSeed } from "../../data/stripe-price.transfer";
|
|
@@ -54,6 +55,7 @@ function PriceEditorInternal({
|
|
|
54
55
|
onDialogOpenChange,
|
|
55
56
|
}: PriceEditorProps) {
|
|
56
57
|
const t = useTranslations();
|
|
58
|
+
const fmt = useBillingFormatOptions();
|
|
57
59
|
const router = useI18nRouter();
|
|
58
60
|
const { createPrice, updatePrice, productId: contextProductId } = usePriceContext();
|
|
59
61
|
const [allFeatures, setAllFeatures] = useState<FeatureInterface[]>([]);
|
|
@@ -266,7 +268,7 @@ function PriceEditorInternal({
|
|
|
266
268
|
<EditorSheet
|
|
267
269
|
form={form}
|
|
268
270
|
entityType={t("billing.admin.prices.entity")}
|
|
269
|
-
entityName={price ? priceLabel(price) : undefined}
|
|
271
|
+
entityName={price ? priceLabel(price, fmt) : undefined}
|
|
270
272
|
isEdit={isEdit}
|
|
271
273
|
module={Modules.StripePrice}
|
|
272
274
|
size="lg"
|
|
@@ -7,7 +7,7 @@ import { useMemo } from "react";
|
|
|
7
7
|
import { Modules } from "../../../../core";
|
|
8
8
|
import { registerTableGenerator, TableContent, usePageUrlGenerator, UseTableStructureHook } from "../../../../hooks";
|
|
9
9
|
import { Badge, Button, Link } from "../../../../shadcnui";
|
|
10
|
-
import { formatCurrency, formatInterval } from "../../components/utils
|
|
10
|
+
import { formatCurrency, formatInterval, useBillingFormatOptions } from "../../components/utils";
|
|
11
11
|
import { StripePriceFields } from "../data/stripe-price.fields";
|
|
12
12
|
import { StripePriceInterface } from "../data/stripe-price.interface";
|
|
13
13
|
|
|
@@ -31,6 +31,7 @@ export const useStripePriceTableStructure: UseTableStructureHook<StripePriceInte
|
|
|
31
31
|
params,
|
|
32
32
|
) => {
|
|
33
33
|
const t = useTranslations();
|
|
34
|
+
const fmt = useBillingFormatOptions();
|
|
34
35
|
const generateUrl = usePageUrlGenerator();
|
|
35
36
|
|
|
36
37
|
const onClone = params.context?.onClone as StripePriceRowActions["onClone"];
|
|
@@ -56,7 +57,7 @@ export const useStripePriceTableStructure: UseTableStructureHook<StripePriceInte
|
|
|
56
57
|
const price: StripePriceInterface = row.original.jsonApiData;
|
|
57
58
|
return (
|
|
58
59
|
<Link href={generateUrl({ page: Modules.StripePrice, id: price.id })}>
|
|
59
|
-
{price.nickname ?? formatCurrency(price.unitAmount, price.currency)}
|
|
60
|
+
{price.nickname ?? formatCurrency(price.unitAmount, price.currency, fmt)}
|
|
60
61
|
</Link>
|
|
61
62
|
);
|
|
62
63
|
},
|
|
@@ -72,7 +73,7 @@ export const useStripePriceTableStructure: UseTableStructureHook<StripePriceInte
|
|
|
72
73
|
// Typography role 15 (numeric): tabular-nums, end-aligned. NEVER font-mono.
|
|
73
74
|
return (
|
|
74
75
|
<span className="block text-xs tabular-nums text-end">
|
|
75
|
-
{formatCurrency(price.unitAmount, price.currency)}
|
|
76
|
+
{formatCurrency(price.unitAmount, price.currency, fmt)}
|
|
76
77
|
</span>
|
|
77
78
|
);
|
|
78
79
|
},
|
|
@@ -86,7 +87,7 @@ export const useStripePriceTableStructure: UseTableStructureHook<StripePriceInte
|
|
|
86
87
|
cell: ({ row }: { row: TableContent<StripePriceInterface> }) => {
|
|
87
88
|
const price: StripePriceInterface = row.original.jsonApiData;
|
|
88
89
|
// Typography role 8 (UI chrome): single-line, plain text-xs.
|
|
89
|
-
return <span className="text-xs">{formatInterval(price)}</span>;
|
|
90
|
+
return <span className="text-xs">{formatInterval(price, fmt)}</span>;
|
|
90
91
|
},
|
|
91
92
|
enableSorting: false,
|
|
92
93
|
enableHiding: false,
|
|
@@ -175,7 +176,7 @@ export const useStripePriceTableStructure: UseTableStructureHook<StripePriceInte
|
|
|
175
176
|
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
176
177
|
TableContent<StripePriceInterface>
|
|
177
178
|
>[];
|
|
178
|
-
}, [params.fields, fieldColumnMap, t, generateUrl, onClone, onExport]);
|
|
179
|
+
}, [params.fields, fieldColumnMap, t, fmt, generateUrl, onClone, onExport]);
|
|
179
180
|
|
|
180
181
|
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
181
182
|
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { useState } from "react";
|
|
4
5
|
import { Check, X, Loader2 } from "lucide-react";
|
|
5
6
|
import { Button, Input } from "../../../../shadcnui";
|
|
7
|
+
import { useI18nLocale } from "../../../../i18n";
|
|
8
|
+
import { formatFixed } from "../../components/utils";
|
|
6
9
|
import { PromotionCodeValidationResult } from "../data/stripe-promotion-code.interface";
|
|
7
10
|
|
|
8
11
|
type PromoCodeInputProps = {
|
|
@@ -22,6 +25,8 @@ export function PromoCodeInput({
|
|
|
22
25
|
onRemove,
|
|
23
26
|
disabled = false,
|
|
24
27
|
}: PromoCodeInputProps) {
|
|
28
|
+
const t = useTranslations();
|
|
29
|
+
const locale = useI18nLocale();
|
|
25
30
|
const [code, setCode] = useState("");
|
|
26
31
|
|
|
27
32
|
const handleApply = () => {
|
|
@@ -40,23 +45,23 @@ export function PromoCodeInput({
|
|
|
40
45
|
// Format discount for display
|
|
41
46
|
const formatDiscount = (result: PromotionCodeValidationResult): string => {
|
|
42
47
|
if (result.discountType === "percent_off") {
|
|
43
|
-
return
|
|
48
|
+
return t("billing.ui.wizard.review.percent_off", { value: String(result.discountValue) });
|
|
44
49
|
}
|
|
45
50
|
// amount_off is in cents, convert to dollars
|
|
46
51
|
const amount = (result.discountValue || 0) / 100;
|
|
47
52
|
const currency = result.currency?.toUpperCase() || "USD";
|
|
48
|
-
return `${currency} ${amount
|
|
53
|
+
return t("billing.ui.wizard.review.amount_off", { amount: `${currency} ${formatFixed(amount, locale, 2)}` });
|
|
49
54
|
};
|
|
50
55
|
|
|
51
56
|
// Format duration for display
|
|
52
57
|
const formatDuration = (result: PromotionCodeValidationResult): string => {
|
|
53
58
|
switch (result.duration) {
|
|
54
59
|
case "forever":
|
|
55
|
-
return "
|
|
60
|
+
return t("billing.ui.promo_code.duration_forever");
|
|
56
61
|
case "once":
|
|
57
|
-
return "
|
|
62
|
+
return t("billing.ui.promo_code.duration_once");
|
|
58
63
|
case "repeating":
|
|
59
|
-
return
|
|
64
|
+
return t("billing.ui.promo_code.duration_repeating", { months: String(result.durationInMonths) });
|
|
60
65
|
default:
|
|
61
66
|
return "";
|
|
62
67
|
}
|
|
@@ -91,7 +96,7 @@ export function PromoCodeInput({
|
|
|
91
96
|
<div className="space-y-2">
|
|
92
97
|
<div className="flex gap-2">
|
|
93
98
|
<Input
|
|
94
|
-
placeholder="
|
|
99
|
+
placeholder={t("billing.ui.promo_code.placeholder")}
|
|
95
100
|
value={code}
|
|
96
101
|
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
97
102
|
onKeyDown={handleKeyDown}
|
|
@@ -99,7 +104,7 @@ export function PromoCodeInput({
|
|
|
99
104
|
className="flex-1"
|
|
100
105
|
/>
|
|
101
106
|
<Button variant="outline" onClick={handleApply} disabled={disabled || isValidating || !code.trim()}>
|
|
102
|
-
{isValidating ? <Loader2 className="h-4 w-4 animate-spin" /> : "
|
|
107
|
+
{isValidating ? <Loader2 className="h-4 w-4 animate-spin" /> : t("billing.ui.promo_code.apply")}
|
|
103
108
|
</Button>
|
|
104
109
|
</div>
|
|
105
110
|
{error && <p className="text-destructive text-xs/relaxed">{error}</p>}
|
|
@@ -42,7 +42,9 @@ export class StripePromotionCodeService {
|
|
|
42
42
|
return {
|
|
43
43
|
valid: false,
|
|
44
44
|
code: params.code,
|
|
45
|
-
|
|
45
|
+
// No English fallback here: a service cannot translate, so the caller words
|
|
46
|
+
// the missing message in the active language.
|
|
47
|
+
errorMessage: response.error || undefined,
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
|
package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { CreditCard } from "lucide-react";
|
|
4
5
|
import { useCallback, useEffect, useState } from "react";
|
|
5
6
|
import { Button } from "../../../../../shadcnui";
|
|
@@ -13,6 +14,7 @@ type SubscriptionsContainerProps = {
|
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
export function SubscriptionsContainer({ onOpenWizard, hasActiveRecurringSubscription }: SubscriptionsContainerProps) {
|
|
17
|
+
const t = useTranslations();
|
|
16
18
|
const [subscriptions, setSubscriptions] = useState<StripeSubscriptionInterface[]>([]);
|
|
17
19
|
const [loading, setLoading] = useState<boolean>(true);
|
|
18
20
|
|
|
@@ -44,7 +46,7 @@ export function SubscriptionsContainer({ onOpenWizard, hasActiveRecurringSubscri
|
|
|
44
46
|
if (loading) {
|
|
45
47
|
return (
|
|
46
48
|
<div className="flex h-64 items-center justify-center">
|
|
47
|
-
<p className="text-muted-foreground">
|
|
49
|
+
<p className="text-muted-foreground">{t("billing.ui.subscriptions.loading")}</p>
|
|
48
50
|
</div>
|
|
49
51
|
);
|
|
50
52
|
}
|
|
@@ -55,11 +57,13 @@ export function SubscriptionsContainer({ onOpenWizard, hasActiveRecurringSubscri
|
|
|
55
57
|
<div className="flex items-center justify-between">
|
|
56
58
|
<div className="flex items-center gap-x-3">
|
|
57
59
|
<CreditCard className="h-8 w-8" />
|
|
58
|
-
<h1 className="text-primary text-3xl font-semibold">
|
|
60
|
+
<h1 className="text-primary text-3xl font-semibold">{t("billing.ui.subscriptions.title")}</h1>
|
|
59
61
|
</div>
|
|
60
62
|
{subscriptions.length > 0 && (
|
|
61
63
|
<Button onClick={() => onOpenWizard?.()}>
|
|
62
|
-
{hasActiveRecurringSubscription
|
|
64
|
+
{hasActiveRecurringSubscription
|
|
65
|
+
? t("billing.ui.subscriptions.purchase_addons")
|
|
66
|
+
: t("billing.ui.subscriptions.subscribe")}
|
|
63
67
|
</Button>
|
|
64
68
|
)}
|
|
65
69
|
</div>
|
|
@@ -74,9 +78,9 @@ export function SubscriptionsContainer({ onOpenWizard, hasActiveRecurringSubscri
|
|
|
74
78
|
<div className="flex flex-col items-center justify-center py-12 space-y-4">
|
|
75
79
|
<CreditCard className="h-16 w-16 text-muted-foreground" />
|
|
76
80
|
<div className="text-center">
|
|
77
|
-
<h3 className="text-sm font-medium mb-2">
|
|
78
|
-
<p className="text-muted-foreground mb-6">
|
|
79
|
-
<Button onClick={() => onOpenWizard?.()}>
|
|
81
|
+
<h3 className="text-sm font-medium mb-2">{t("billing.ui.subscriptions.empty_title")}</h3>
|
|
82
|
+
<p className="text-muted-foreground mb-6">{t("billing.ui.subscriptions.empty_description")}</p>
|
|
83
|
+
<Button onClick={() => onOpenWizard?.()}>{t("billing.ui.subscriptions.subscribe")}</Button>
|
|
80
84
|
</div>
|
|
81
85
|
</div>
|
|
82
86
|
)}
|