@umituz/web-payment 0.0.0-stage → 1.0.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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -2
  3. package/dist/billing-adapter.interface-CDkeeowZ.d.mts +94 -0
  4. package/dist/billing-adapter.interface-m7yE50I0.d.ts +94 -0
  5. package/dist/billing-cycle.enum--hJRYlgP.d.mts +10 -0
  6. package/dist/billing-cycle.enum--hJRYlgP.d.ts +10 -0
  7. package/dist/billing-env-D4YjvA3M.d.mts +8 -0
  8. package/dist/billing-env-D4YjvA3M.d.ts +8 -0
  9. package/dist/checkout-metadata-D7Wp_DAD.d.mts +27 -0
  10. package/dist/checkout-metadata-D7Wp_DAD.d.ts +27 -0
  11. package/dist/chunk-2KV7JFYF.mjs +61 -0
  12. package/dist/chunk-2NH5DOLT.mjs +101 -0
  13. package/dist/chunk-3AXPZOCV.mjs +106 -0
  14. package/dist/chunk-4N3BASZ6.mjs +51 -0
  15. package/dist/chunk-NHKK3ENU.mjs +1128 -0
  16. package/dist/chunk-PIWNUAKW.mjs +30 -0
  17. package/dist/chunk-QFB7AOSQ.mjs +33 -0
  18. package/dist/chunk-RQTMBR74.mjs +30 -0
  19. package/dist/chunk-U45FS4FF.mjs +110 -0
  20. package/dist/chunk-X6R76FZT.mjs +0 -0
  21. package/dist/chunk-XQ2HJTTA.mjs +252 -0
  22. package/dist/core.d.mts +17 -0
  23. package/dist/core.d.ts +17 -0
  24. package/dist/core.js +425 -0
  25. package/dist/core.mjs +66 -0
  26. package/dist/http.d.mts +12 -0
  27. package/dist/http.d.ts +12 -0
  28. package/dist/http.js +115 -0
  29. package/dist/http.mjs +88 -0
  30. package/dist/index.d.mts +17 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.js +1929 -0
  33. package/dist/index.mjs +153 -0
  34. package/dist/memory.d.mts +14 -0
  35. package/dist/memory.d.ts +14 -0
  36. package/dist/memory.js +130 -0
  37. package/dist/memory.mjs +6 -0
  38. package/dist/portal.d.mts +445 -0
  39. package/dist/portal.d.ts +445 -0
  40. package/dist/portal.js +2927 -0
  41. package/dist/portal.mjs +2737 -0
  42. package/dist/product.mapper-CnY1ISOp.d.ts +97 -0
  43. package/dist/product.mapper-noVl-sel.d.mts +97 -0
  44. package/dist/products.d.mts +64 -0
  45. package/dist/products.d.ts +64 -0
  46. package/dist/products.js +370 -0
  47. package/dist/products.mjs +21 -0
  48. package/dist/react.d.mts +182 -0
  49. package/dist/react.d.ts +182 -0
  50. package/dist/react.js +1874 -0
  51. package/dist/react.mjs +105 -0
  52. package/dist/server.d.mts +177 -0
  53. package/dist/server.d.ts +177 -0
  54. package/dist/server.js +622 -0
  55. package/dist/server.mjs +417 -0
  56. package/dist/visual-tokens-BCLVR85G.d.mts +94 -0
  57. package/dist/visual-tokens-BCLVR85G.d.ts +94 -0
  58. package/package.json +122 -4
  59. package/src/core.ts +81 -0
  60. package/src/domain/entities/cancellation.entity.ts +28 -0
  61. package/src/domain/entities/checkout.entity.ts +21 -0
  62. package/src/domain/entities/index.ts +10 -0
  63. package/src/domain/entities/order.entity.ts +15 -0
  64. package/src/domain/entities/product.entity.ts +16 -0
  65. package/src/domain/entities/subscription.entity.ts +24 -0
  66. package/src/domain/entities/sync.entity.ts +4 -0
  67. package/src/domain/enums/amount-type.enum.ts +10 -0
  68. package/src/domain/enums/billing-cycle.enum.ts +30 -0
  69. package/src/domain/enums/index.ts +5 -0
  70. package/src/domain/enums/payment-provider.enum.ts +14 -0
  71. package/src/domain/enums/subscription-status.enum.ts +32 -0
  72. package/src/domain/index.ts +5 -0
  73. package/src/domain/interfaces/billing-adapter.interface.ts +18 -0
  74. package/src/domain/interfaces/index.ts +1 -0
  75. package/src/domain/mappers/product.mapper.ts +176 -0
  76. package/src/domain/utils/minor-units.ts +20 -0
  77. package/src/domain/utils/percent-discount.ts +17 -0
  78. package/src/http.ts +3 -0
  79. package/src/index.ts +12 -0
  80. package/src/infrastructure/browser/hosted-checkout.ts +26 -0
  81. package/src/infrastructure/config/billing-env.ts +31 -0
  82. package/src/infrastructure/constants/billing.constants.ts +10 -0
  83. package/src/infrastructure/http/http-client.ts +83 -0
  84. package/src/infrastructure/pagination/page-iterator.ts +43 -0
  85. package/src/infrastructure/query/query-builder.ts +26 -0
  86. package/src/infrastructure/services/billing.client.ts +300 -0
  87. package/src/infrastructure/services/create-billing-client.ts +23 -0
  88. package/src/infrastructure/services/http/create-http-adapter.ts +123 -0
  89. package/src/infrastructure/services/memory/create-memory-adapter.ts +126 -0
  90. package/src/infrastructure/services/product.repository.ts +198 -0
  91. package/src/infrastructure/utils/checkout-metadata.ts +55 -0
  92. package/src/infrastructure/utils/date-renderer.ts +53 -0
  93. package/src/infrastructure/utils/money-formatter.ts +39 -0
  94. package/src/infrastructure/utils/normalization.util.ts +22 -0
  95. package/src/infrastructure/utils/pagination-clamp.ts +11 -0
  96. package/src/infrastructure/utils/receipt.util.ts +6 -0
  97. package/src/infrastructure/utils/security.util.ts +23 -0
  98. package/src/infrastructure/utils/type-guards.util.ts +23 -0
  99. package/src/infrastructure/utils/validation.util.ts +10 -0
  100. package/src/infrastructure/webhook/parse-event.ts +135 -0
  101. package/src/infrastructure/webhook/verify-webhook.ts +124 -0
  102. package/src/memory.ts +3 -0
  103. package/src/portal.ts +2 -0
  104. package/src/presentation/components/BillingHistoryList.tsx +172 -0
  105. package/src/presentation/components/BillingProvider.tsx +179 -0
  106. package/src/presentation/components/CancelSubscriptionButton.tsx +155 -0
  107. package/src/presentation/components/CheckoutButton.tsx +76 -0
  108. package/src/presentation/components/CustomerPortalLink.tsx +71 -0
  109. package/src/presentation/components/PricingTable.tsx +225 -0
  110. package/src/presentation/components/SubscriptionStatusBadge.tsx +102 -0
  111. package/src/presentation/context/billing-context.ts +30 -0
  112. package/src/presentation/hooks/useBilling.ts +3 -0
  113. package/src/presentation/hooks/useCancelSubscription.ts +47 -0
  114. package/src/presentation/hooks/useCheckoutButton.ts +49 -0
  115. package/src/presentation/hooks/useProducts.ts +76 -0
  116. package/src/presentation/navigation/navigation.strategy.ts +16 -0
  117. package/src/presentation/portal/TurnkeyBillingPage.tsx +144 -0
  118. package/src/presentation/portal/components/BillingLayout.tsx +163 -0
  119. package/src/presentation/portal/components/BillingPage.tsx +22 -0
  120. package/src/presentation/portal/components/BillingPortal.tsx +248 -0
  121. package/src/presentation/portal/components/InvoiceCard.tsx +275 -0
  122. package/src/presentation/portal/components/OverviewTab.tsx +137 -0
  123. package/src/presentation/portal/components/PaymentMethodsList.tsx +268 -0
  124. package/src/presentation/portal/components/PlanComparison.tsx +405 -0
  125. package/src/presentation/portal/components/PlanTab.tsx +197 -0
  126. package/src/presentation/portal/components/PortalButton.tsx +89 -0
  127. package/src/presentation/portal/components/PortalSpinner.tsx +30 -0
  128. package/src/presentation/portal/components/UsageCard.tsx +158 -0
  129. package/src/presentation/portal/components/index.ts +11 -0
  130. package/src/presentation/portal/constants.ts +44 -0
  131. package/src/presentation/portal/create-portal-client.ts +202 -0
  132. package/src/presentation/portal/hooks/index.ts +3 -0
  133. package/src/presentation/portal/hooks/useBilling.ts +240 -0
  134. package/src/presentation/portal/index.ts +97 -0
  135. package/src/presentation/portal/labels.tsx +218 -0
  136. package/src/presentation/portal/styles.ts +70 -0
  137. package/src/presentation/portal/types.ts +213 -0
  138. package/src/presentation/portal/utils/billing.ts +193 -0
  139. package/src/presentation/portal/utils/index.ts +27 -0
  140. package/src/presentation/portal/utils/plans.ts +71 -0
  141. package/src/presentation/theme/visual-tokens.ts +103 -0
  142. package/src/presentation/utils/cancellation-reason-catalog.ts +18 -0
  143. package/src/presentation/utils/checkout-button-visual.ts +50 -0
  144. package/src/presentation/utils/inline-error-visual.ts +21 -0
  145. package/src/presentation/utils/product-pricing.ts +33 -0
  146. package/src/presentation/utils/status-label.ts +23 -0
  147. package/src/presentation/utils/status-tone.ts +25 -0
  148. package/src/products.ts +20 -0
  149. package/src/react.tsx +63 -0
  150. package/src/server.ts +79 -0
package/dist/portal.js ADDED
@@ -0,0 +1,2927 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/portal.ts
21
+ var portal_exports = {};
22
+ __export(portal_exports, {
23
+ BILLING_CYCLE: () => BILLING_CYCLE,
24
+ BillingLabelsProvider: () => BillingLabelsProvider,
25
+ BillingLayout: () => BillingLayout,
26
+ BillingPage: () => BillingPage,
27
+ BillingPortal: () => BillingPortal,
28
+ CURRENCY: () => CURRENCY,
29
+ DEFAULT_BILLING_CONFIG: () => DEFAULT_BILLING_CONFIG,
30
+ DEFAULT_BILLING_LABELS: () => DEFAULT_BILLING_LABELS,
31
+ INVOICE_STATUS: () => INVOICE_STATUS,
32
+ InvoiceCard: () => InvoiceCard,
33
+ PLAN_TYPE: () => PLAN_TYPE,
34
+ PaymentMethodsList: () => PaymentMethodsList,
35
+ PlanComparison: () => PlanComparison,
36
+ PortalButton: () => PortalButton,
37
+ PortalSpinner: () => PortalSpinner,
38
+ RADIO_GROUP_NAME: () => RADIO_GROUP_NAME,
39
+ SUBSCRIPTION_STATUS: () => SUBSCRIPTION_STATUS,
40
+ TurnkeyBillingPage: () => TurnkeyBillingPage,
41
+ UsageCard: () => UsageCard,
42
+ calculateDiscount: () => calculateDiscount,
43
+ calculateInvoiceTotal: () => calculateInvoiceTotal,
44
+ calculateProratedAmount: () => calculateProratedAmount,
45
+ calculateUsagePercentage: () => calculateUsagePercentage,
46
+ createPortalClient: () => createPortalClient,
47
+ createStubBillingApiClient: () => createStubBillingApiClient,
48
+ formatCardNumber: () => formatCardNumber,
49
+ formatExpiry: () => formatExpiry,
50
+ formatFeature: () => formatFeature,
51
+ formatLabel: () => formatLabel,
52
+ formatNumber: () => formatNumber,
53
+ formatPrice: () => formatPrice,
54
+ getDaysRemaining: () => getDaysRemaining,
55
+ getInvoiceStatusLabel: () => getInvoiceStatusLabel,
56
+ getInvoiceStatusTone: () => getInvoiceStatusTone,
57
+ getNextBillingDate: () => getNextBillingDate,
58
+ getPlanPrice: () => getPlanPrice,
59
+ getStatusLabel: () => getStatusLabel,
60
+ getStatusTone: () => getStatusTone,
61
+ getTrialDaysText: () => getTrialDaysText,
62
+ groupInvoicesByStatus: () => groupInvoicesByStatus,
63
+ isNearLimit: () => isNearLimit,
64
+ isPopularPlan: () => isPopularPlan,
65
+ isTrialExpiringSoon: () => isTrialExpiringSoon,
66
+ mergeLabels: () => mergeLabels,
67
+ sortInvoicesByDate: () => sortInvoicesByDate,
68
+ transformProductToPlan: () => transformProductToPlan,
69
+ transformProducts: () => transformProducts,
70
+ useBilling: () => useBilling,
71
+ useBillingLabels: () => useBillingLabels
72
+ });
73
+ module.exports = __toCommonJS(portal_exports);
74
+
75
+ // src/presentation/portal/components/BillingPortal.tsx
76
+ var import_lucide_react5 = require("lucide-react");
77
+
78
+ // src/presentation/theme/visual-tokens.ts
79
+ var VisualTokens = Object.freeze({
80
+ spacing: Object.freeze({
81
+ none: 0,
82
+ xs: 4,
83
+ sm: 6,
84
+ md: 8,
85
+ lg: 12,
86
+ xl: 16,
87
+ xxl: 20,
88
+ section: 24,
89
+ page: 32
90
+ }),
91
+ radius: Object.freeze({
92
+ sm: 6,
93
+ md: 8,
94
+ lg: 12,
95
+ pill: 999
96
+ }),
97
+ fontSize: Object.freeze({
98
+ caption: 12,
99
+ body: 14,
100
+ title: 18,
101
+ display: 24
102
+ }),
103
+ fontWeight: Object.freeze({
104
+ regular: 400,
105
+ medium: 500,
106
+ strong: 600
107
+ }),
108
+ lineHeight: Object.freeze({
109
+ tight: 1.25,
110
+ normal: 1.5
111
+ }),
112
+ opacity: Object.freeze({
113
+ muted: 0.7,
114
+ disabled: 0.6,
115
+ hover: 0.85
116
+ }),
117
+ color: Object.freeze({
118
+ textPrimary: "#111827",
119
+ textMuted: "#6b7280",
120
+ textOnPrimary: "#ffffff",
121
+ textInverse: "#ffffff",
122
+ textError: "#b91c1c",
123
+ textOnError: "#ffffff",
124
+ surfacePrimary: "#ffffff",
125
+ surfaceMuted: "#f9fafb",
126
+ surfaceSubtle: "#f3f4f6",
127
+ surfaceWarning: "#fef3c7",
128
+ surfaceDanger: "#fee2e2",
129
+ surfaceDangerMuted: "#fef2f2",
130
+ borderSubtle: "#e5e7eb",
131
+ borderStrong: "#d1d5db",
132
+ borderWarning: "#fecaca",
133
+ brandPrimary: "#2563eb",
134
+ brandPrimaryHover: "#1d4ed8",
135
+ brandDanger: "#dc2626",
136
+ brandDangerHover: "#b91c1c",
137
+ brandDangerText: "#991b1b",
138
+ badgeNeutralBg: "#e5e7eb",
139
+ badgeNeutralFg: "#111827",
140
+ badgeActiveBg: "#dcfce7",
141
+ badgeActiveFg: "#166534",
142
+ badgeWarningBg: "#fef3c7",
143
+ badgeWarningFg: "#92400e",
144
+ badgeDangerBg: "#fee2e2",
145
+ badgeDangerFg: "#991b1b"
146
+ }),
147
+ transition: Object.freeze({
148
+ instant: "120ms ease"
149
+ }),
150
+ layout: Object.freeze({
151
+ cardMinWidth: 260,
152
+ benefitListMax: 5
153
+ })
154
+ });
155
+ var TONE_PAIRS = Object.freeze({
156
+ neutral: { bg: VisualTokens.color.badgeNeutralBg, fg: VisualTokens.color.badgeNeutralFg },
157
+ active: { bg: VisualTokens.color.badgeActiveBg, fg: VisualTokens.color.badgeActiveFg },
158
+ warning: { bg: VisualTokens.color.badgeWarningBg, fg: VisualTokens.color.badgeWarningFg },
159
+ danger: { bg: VisualTokens.color.badgeDangerBg, fg: VisualTokens.color.badgeDangerFg }
160
+ });
161
+
162
+ // src/presentation/portal/labels.tsx
163
+ var import_react = require("react");
164
+ var import_jsx_runtime = require("react/jsx-runtime");
165
+ function formatLabel(template, params) {
166
+ return template.replace(
167
+ /\{(\w+)\}/g,
168
+ (match, key) => key in params ? String(params[key]) : match
169
+ );
170
+ }
171
+ var DEFAULT_BILLING_LABELS = Object.freeze({
172
+ common: Object.freeze({
173
+ loading: "Loading\u2026",
174
+ usage: "usage"
175
+ }),
176
+ page: Object.freeze({
177
+ backToDashboard: "Back to dashboard",
178
+ title: "Billing",
179
+ description: "Manage your subscription, payment methods, and invoices.",
180
+ needHelp: "Need help?",
181
+ contactAria: "Contact {email}"
182
+ }),
183
+ tabs: Object.freeze({
184
+ overview: "Overview",
185
+ paymentMethods: "Payment Methods",
186
+ invoices: "Invoices",
187
+ plan: "Plan"
188
+ }),
189
+ overview: Object.freeze({
190
+ currentSubscription: "Current Subscription",
191
+ trialDaysLeft: "days left in trial",
192
+ nextBilling: "Next billing:",
193
+ upcomingInvoice: "Upcoming Invoice"
194
+ }),
195
+ invoices: Object.freeze({
196
+ empty: "No invoices yet",
197
+ moreItems: "more items",
198
+ due: "Due",
199
+ viewInvoice: "View invoice",
200
+ downloadPdf: "Download PDF"
201
+ }),
202
+ plan: Object.freeze({
203
+ title: "Current Plan",
204
+ actionsTitle: "Plan Actions",
205
+ changePlan: "Change Plan",
206
+ cancelSubscription: "Cancel Subscription",
207
+ confirmCancel: "Click again to confirm",
208
+ cancelling: "Cancelling\u2026",
209
+ cancelConfirmMessage: "Your subscription stays active until the end of the current billing period.",
210
+ cancelFailed: "Failed to cancel subscription. Please try again."
211
+ }),
212
+ planComparison: Object.freeze({
213
+ monthly: "Monthly",
214
+ yearly: "Yearly",
215
+ monthlyShort: "mo",
216
+ yearlyShort: "yr",
217
+ save: "Save {percent}%",
218
+ saveWithYearly: "Save {percent}% with yearly billing",
219
+ mostPopular: "Most Popular",
220
+ currentPlan: "Current Plan",
221
+ selectPlan: "Select Plan",
222
+ processing: "Processing\u2026",
223
+ billingCycleLabel: "Billing cycle",
224
+ radiogroupLabel: "Available plans"
225
+ }),
226
+ paymentMethods: Object.freeze({
227
+ none: "No payment methods on file",
228
+ add: "Add Payment Method",
229
+ bankAccount: "Bank Account",
230
+ expires: "Expires",
231
+ default: "Default",
232
+ setDefault: "Set as default",
233
+ remove: "Remove",
234
+ removeConfirm: "Remove this payment method?",
235
+ confirmRemove: "Remove",
236
+ cancelRemove: "Cancel"
237
+ }),
238
+ usage: Object.freeze({
239
+ limit: "limit",
240
+ resets: "Resets"
241
+ }),
242
+ status: Object.freeze({
243
+ active: "Active",
244
+ trialing: "Trial",
245
+ past_due: "Past Due",
246
+ canceled: "Canceled",
247
+ unpaid: "Unpaid",
248
+ incomplete: "Incomplete",
249
+ revoked: "Revoked"
250
+ }),
251
+ invoiceStatus: Object.freeze({
252
+ draft: "Draft",
253
+ open: "Open",
254
+ paid: "Paid",
255
+ void: "Void",
256
+ uncollectible: "Uncollectible",
257
+ refunded: "Refunded"
258
+ })
259
+ });
260
+ function mergeLabels(overrides) {
261
+ if (!overrides) return { ...DEFAULT_BILLING_LABELS };
262
+ const merged = { ...DEFAULT_BILLING_LABELS };
263
+ for (const key of Object.keys(overrides)) {
264
+ const override = overrides[key];
265
+ if (override === void 0) continue;
266
+ const base = DEFAULT_BILLING_LABELS[key];
267
+ const target = merged;
268
+ if (typeof base === "string" || typeof override === "string") {
269
+ target[key] = override;
270
+ } else {
271
+ target[key] = { ...base, ...override };
272
+ }
273
+ }
274
+ return merged;
275
+ }
276
+ var BillingLabelsContext = (0, import_react.createContext)(
277
+ DEFAULT_BILLING_LABELS
278
+ );
279
+ function BillingLabelsProvider({
280
+ labels,
281
+ children
282
+ }) {
283
+ const value = (0, import_react.useMemo)(() => mergeLabels(labels), [labels]);
284
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BillingLabelsContext.Provider, { value, children });
285
+ }
286
+ function useBillingLabels() {
287
+ return (0, import_react.useContext)(BillingLabelsContext);
288
+ }
289
+
290
+ // src/presentation/portal/components/PaymentMethodsList.tsx
291
+ var import_react2 = require("react");
292
+ var import_lucide_react = require("lucide-react");
293
+
294
+ // src/domain/utils/minor-units.ts
295
+ var MINOR_UNIT_DIVISOR = 100;
296
+ var ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
297
+ "JPY",
298
+ "KRW",
299
+ "VND",
300
+ "CLP",
301
+ "PYG",
302
+ "XAF",
303
+ "XOF",
304
+ "BIF",
305
+ "DJF",
306
+ "GNF",
307
+ "XPF"
308
+ ]);
309
+ function minorUnitDivisor(currency) {
310
+ return ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase()) ? 1 : MINOR_UNIT_DIVISOR;
311
+ }
312
+ function toMajorUnits(amount, currency) {
313
+ return amount / minorUnitDivisor(currency);
314
+ }
315
+
316
+ // src/presentation/portal/utils/billing.ts
317
+ function formatNumber(num, locale = "en-US") {
318
+ return new Intl.NumberFormat(locale).format(num);
319
+ }
320
+ function formatPrice(amount, currency = "USD") {
321
+ const localeMap = {
322
+ USD: "en-US",
323
+ EUR: "de-DE",
324
+ GBP: "en-GB",
325
+ TRY: "tr-TR",
326
+ JPY: "ja-JP"
327
+ };
328
+ return new Intl.NumberFormat(localeMap[currency] ?? "en-US", {
329
+ style: "currency",
330
+ currency
331
+ }).format(toMajorUnits(amount, currency));
332
+ }
333
+ function calculateDiscount(monthlyPrice, yearlyPrice) {
334
+ if (monthlyPrice <= 0) return 0;
335
+ const yearlyMonthly = yearlyPrice / 12;
336
+ const discount = (monthlyPrice - yearlyMonthly) / monthlyPrice * 100;
337
+ return Math.round(Math.max(discount, 0));
338
+ }
339
+ function calculateProratedAmount(amount, daysUsed, totalDays) {
340
+ if (totalDays === 0) return 0;
341
+ return amount / totalDays * daysUsed;
342
+ }
343
+ function getPlanPrice(plan, cycle) {
344
+ return cycle === "monthly" ? plan.monthlyPrice : plan.yearlyPrice;
345
+ }
346
+ function calculateUsagePercentage(metric) {
347
+ if (metric.limit === 0) return 0;
348
+ return Math.min(metric.current / metric.limit * 100, 100);
349
+ }
350
+ function isNearLimit(metric, threshold = 80) {
351
+ return calculateUsagePercentage(metric) >= threshold;
352
+ }
353
+ function getStatusTone(status) {
354
+ const toneMap = {
355
+ active: "active",
356
+ trialing: "active",
357
+ past_due: "warning",
358
+ canceled: "neutral",
359
+ unpaid: "danger",
360
+ incomplete: "warning",
361
+ revoked: "danger"
362
+ };
363
+ return toneMap[status] ?? "neutral";
364
+ }
365
+ function getStatusLabel(status) {
366
+ const labelMap = {
367
+ active: "Active",
368
+ trialing: "Trial",
369
+ past_due: "Past Due",
370
+ canceled: "Canceled",
371
+ unpaid: "Unpaid",
372
+ incomplete: "Incomplete",
373
+ revoked: "Revoked"
374
+ };
375
+ return labelMap[status] || status;
376
+ }
377
+ function getInvoiceStatusTone(status) {
378
+ const toneMap = {
379
+ draft: "neutral",
380
+ open: "warning",
381
+ paid: "active",
382
+ void: "neutral",
383
+ uncollectible: "danger",
384
+ refunded: "neutral"
385
+ };
386
+ return toneMap[status] ?? "neutral";
387
+ }
388
+ function getInvoiceStatusLabel(status) {
389
+ const labelMap = {
390
+ draft: "Draft",
391
+ open: "Open",
392
+ paid: "Paid",
393
+ void: "Void",
394
+ uncollectible: "Uncollectible",
395
+ refunded: "Refunded"
396
+ };
397
+ return labelMap[status] || status;
398
+ }
399
+ function formatCardNumber(last4, brand) {
400
+ return `${brand.toUpperCase()} \u2022\u2022\u2022\u2022 ${last4}`;
401
+ }
402
+ function formatExpiry(month, year) {
403
+ return `${String(month).padStart(2, "0")}/${String(year).slice(-2)}`;
404
+ }
405
+ function getDaysRemaining(endDate) {
406
+ const end = new Date(endDate);
407
+ const now = /* @__PURE__ */ new Date();
408
+ const diff = end.getTime() - now.getTime();
409
+ return Math.max(0, Math.ceil(diff / (1e3 * 60 * 60 * 24)));
410
+ }
411
+ function isTrialExpiringSoon(trialEnd, daysThreshold = 7) {
412
+ const daysRemaining = getDaysRemaining(trialEnd);
413
+ return daysRemaining > 0 && daysRemaining <= daysThreshold;
414
+ }
415
+ function getNextBillingDate(currentPeriodEnd, cycle) {
416
+ const nextDate = new Date(currentPeriodEnd);
417
+ if (cycle === "monthly") {
418
+ nextDate.setMonth(nextDate.getMonth() + 1);
419
+ } else {
420
+ nextDate.setFullYear(nextDate.getFullYear() + 1);
421
+ }
422
+ return nextDate;
423
+ }
424
+ function groupInvoicesByStatus(invoices) {
425
+ const grouped = {
426
+ draft: [],
427
+ open: [],
428
+ paid: [],
429
+ void: [],
430
+ uncollectible: [],
431
+ refunded: []
432
+ };
433
+ invoices.forEach((invoice) => {
434
+ grouped[invoice.status].push(invoice);
435
+ });
436
+ return grouped;
437
+ }
438
+ function calculateInvoiceTotal(invoices, status) {
439
+ const filtered = status ? invoices.filter((inv) => inv.status === status) : invoices;
440
+ return filtered.reduce((sum, inv) => sum + inv.amount, 0);
441
+ }
442
+ function sortInvoicesByDate(invoices, order = "desc") {
443
+ return [...invoices].sort((a, b) => {
444
+ const dateA = new Date(a.date).getTime();
445
+ const dateB = new Date(b.date).getTime();
446
+ return order === "asc" ? dateA - dateB : dateB - dateA;
447
+ });
448
+ }
449
+ function formatFeature(feature) {
450
+ if (typeof feature === "string") {
451
+ return { text: feature, included: true };
452
+ }
453
+ return feature;
454
+ }
455
+ function isPopularPlan(plan, plans) {
456
+ const middleIndex = Math.floor(plans.length / 2);
457
+ return plans[middleIndex]?.id === plan.id;
458
+ }
459
+ function getTrialDaysText(trialEnd) {
460
+ const days = getDaysRemaining(trialEnd);
461
+ if (days === 0) return "Trial ends today";
462
+ if (days === 1) return "1 day left";
463
+ return `${days} days left`;
464
+ }
465
+
466
+ // src/presentation/portal/components/PortalButton.tsx
467
+ var import_jsx_runtime2 = require("react/jsx-runtime");
468
+ var VARIANT_STYLES = {
469
+ default: {
470
+ background: VisualTokens.color.brandPrimary,
471
+ color: VisualTokens.color.textOnPrimary,
472
+ border: "1px solid transparent"
473
+ },
474
+ outline: {
475
+ background: "transparent",
476
+ color: VisualTokens.color.textPrimary,
477
+ border: `1px solid ${VisualTokens.color.borderStrong}`
478
+ },
479
+ destructive: {
480
+ background: VisualTokens.color.brandDanger,
481
+ color: VisualTokens.color.textOnError,
482
+ border: "1px solid transparent"
483
+ },
484
+ ghost: {
485
+ background: "transparent",
486
+ color: VisualTokens.color.textMuted,
487
+ border: "1px solid transparent"
488
+ }
489
+ };
490
+ var SIZE_STYLES = {
491
+ sm: {
492
+ padding: `${VisualTokens.spacing.xs}px ${VisualTokens.spacing.lg}px`,
493
+ fontSize: VisualTokens.fontSize.caption
494
+ },
495
+ md: {
496
+ padding: `${VisualTokens.spacing.md}px ${VisualTokens.spacing.xxl}px`,
497
+ fontSize: VisualTokens.fontSize.body
498
+ }
499
+ };
500
+ var PortalButton = ({
501
+ variant = "default",
502
+ size = "md",
503
+ fullWidth = false,
504
+ pill = false,
505
+ icon,
506
+ children,
507
+ disabled,
508
+ style,
509
+ type = "button",
510
+ ...rest
511
+ }) => {
512
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
513
+ "button",
514
+ {
515
+ type,
516
+ disabled,
517
+ style: {
518
+ display: "inline-flex",
519
+ alignItems: "center",
520
+ justifyContent: "center",
521
+ gap: VisualTokens.spacing.sm,
522
+ borderRadius: pill ? VisualTokens.radius.pill : VisualTokens.radius.md,
523
+ fontWeight: VisualTokens.fontWeight.medium,
524
+ lineHeight: VisualTokens.lineHeight.normal,
525
+ cursor: disabled ? "not-allowed" : "pointer",
526
+ opacity: disabled ? VisualTokens.opacity.disabled : 1,
527
+ width: fullWidth ? "100%" : void 0,
528
+ boxSizing: "border-box",
529
+ ...VARIANT_STYLES[variant],
530
+ ...SIZE_STYLES[size],
531
+ ...style
532
+ },
533
+ ...rest,
534
+ children: [
535
+ icon,
536
+ children
537
+ ]
538
+ }
539
+ );
540
+ };
541
+
542
+ // src/presentation/portal/components/PortalSpinner.tsx
543
+ var import_jsx_runtime3 = require("react/jsx-runtime");
544
+ var PortalSpinner = ({ size = 24 }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
545
+ "svg",
546
+ {
547
+ width: size,
548
+ height: size,
549
+ viewBox: "0 0 24 24",
550
+ fill: "none",
551
+ "aria-hidden": "true",
552
+ style: { display: "inline-block", verticalAlign: "middle" },
553
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
554
+ "circle",
555
+ {
556
+ cx: "12",
557
+ cy: "12",
558
+ r: "10",
559
+ stroke: "currentColor",
560
+ strokeWidth: "2.5",
561
+ strokeLinecap: "round",
562
+ strokeDasharray: "44 19",
563
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
564
+ "animateTransform",
565
+ {
566
+ attributeName: "transform",
567
+ type: "rotate",
568
+ from: "0 12 12",
569
+ to: "360 12 12",
570
+ dur: "0.8s",
571
+ repeatCount: "indefinite"
572
+ }
573
+ )
574
+ }
575
+ )
576
+ }
577
+ );
578
+
579
+ // src/presentation/portal/styles.ts
580
+ var visuallyHidden = {
581
+ position: "absolute",
582
+ width: 1,
583
+ height: 1,
584
+ padding: 0,
585
+ margin: -1,
586
+ overflow: "hidden",
587
+ clip: "rect(0 0 0 0)",
588
+ whiteSpace: "nowrap",
589
+ border: 0
590
+ };
591
+ var card = {
592
+ background: VisualTokens.color.surfacePrimary,
593
+ border: `1px solid ${VisualTokens.color.borderSubtle}`,
594
+ borderRadius: VisualTokens.radius.lg,
595
+ padding: VisualTokens.spacing.section,
596
+ boxSizing: "border-box"
597
+ };
598
+ var cardInteractive = {
599
+ ...card,
600
+ cursor: "pointer"
601
+ };
602
+ var sectionTitle = {
603
+ fontSize: VisualTokens.fontSize.title,
604
+ fontWeight: VisualTokens.fontWeight.strong,
605
+ color: VisualTokens.color.textPrimary,
606
+ margin: 0,
607
+ marginBottom: VisualTokens.spacing.xl
608
+ };
609
+ var mutedText = {
610
+ color: VisualTokens.color.textMuted,
611
+ fontSize: VisualTokens.fontSize.body
612
+ };
613
+ var stack = {
614
+ display: "flex",
615
+ flexDirection: "column",
616
+ gap: VisualTokens.spacing.xl
617
+ };
618
+ var row = {
619
+ display: "flex",
620
+ alignItems: "center"
621
+ };
622
+ var alertDanger = {
623
+ display: "flex",
624
+ alignItems: "flex-start",
625
+ gap: VisualTokens.spacing.md,
626
+ padding: VisualTokens.spacing.lg,
627
+ borderRadius: VisualTokens.radius.md,
628
+ border: `1px solid ${VisualTokens.color.borderWarning}`,
629
+ background: VisualTokens.color.surfaceDangerMuted,
630
+ color: VisualTokens.color.brandDangerText,
631
+ fontSize: VisualTokens.fontSize.body
632
+ };
633
+
634
+ // src/presentation/portal/components/PaymentMethodsList.tsx
635
+ var import_jsx_runtime4 = require("react/jsx-runtime");
636
+ var BRAND_GRADIENT = {
637
+ visa: "linear-gradient(135deg, #1d4ed8, #3b82f6)",
638
+ mastercard: "linear-gradient(135deg, #f97316, #ef4444)",
639
+ amex: "linear-gradient(135deg, #0e7490, #06b6d4)",
640
+ discover: "linear-gradient(135deg, #f59e0b, #f97316)",
641
+ default: "linear-gradient(135deg, #334155, #64748b)"
642
+ };
643
+ var cardGradient = (brand) => {
644
+ if (!brand) return BRAND_GRADIENT.default;
645
+ const key = brand.toLowerCase();
646
+ return BRAND_GRADIENT[key] ?? BRAND_GRADIENT.default;
647
+ };
648
+ var PaymentMethodsList = ({
649
+ paymentMethods,
650
+ loading = false,
651
+ busyId,
652
+ onSetDefault,
653
+ onRemove,
654
+ onAddNew
655
+ }) => {
656
+ const labels = useBillingLabels();
657
+ const [pendingRemove, setPendingRemove] = (0, import_react2.useState)(null);
658
+ if (loading) {
659
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { ...row, justifyContent: "center", padding: `${VisualTokens.spacing.section}px 0` }, role: "status", "aria-live": "polite", children: [
660
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PortalSpinner, { size: 32 }),
661
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: visuallyHidden, children: labels.common.loading })
662
+ ] });
663
+ }
664
+ if (paymentMethods.length === 0) {
665
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { textAlign: "center", padding: `${VisualTokens.spacing.section}px 0` }, children: [
666
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
667
+ import_lucide_react.CreditCard,
668
+ {
669
+ size: 48,
670
+ "aria-hidden": "true",
671
+ style: { color: VisualTokens.color.textMuted, marginBottom: VisualTokens.spacing.lg }
672
+ }
673
+ ),
674
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { ...mutedText, margin: 0, marginBottom: VisualTokens.spacing.section }, children: labels.paymentMethods.none }),
675
+ onAddNew && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PortalButton, { type: "button", onClick: onAddNew, variant: "outline", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.Plus, { size: 16 }), children: labels.paymentMethods.add })
676
+ ] });
677
+ }
678
+ const requestRemove = (id) => setPendingRemove(id);
679
+ const cancelRemove = () => setPendingRemove(null);
680
+ const confirmRemove = (id) => {
681
+ onRemove?.(id);
682
+ setPendingRemove(null);
683
+ };
684
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: stack, children: [
685
+ paymentMethods.map((method) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
686
+ PaymentMethodItem,
687
+ {
688
+ method,
689
+ allMethods: paymentMethods,
690
+ onRemove,
691
+ isBusy: busyId === method.id,
692
+ pendingRemove: pendingRemove === method.id,
693
+ onRequestRemove: requestRemove,
694
+ onCancelRemove: cancelRemove,
695
+ onConfirmRemove: confirmRemove,
696
+ onSetDefault
697
+ },
698
+ method.id
699
+ )),
700
+ onAddNew && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
701
+ PortalButton,
702
+ {
703
+ type: "button",
704
+ onClick: onAddNew,
705
+ variant: "outline",
706
+ fullWidth: true,
707
+ icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.Plus, { size: 16 }),
708
+ children: labels.paymentMethods.add
709
+ }
710
+ )
711
+ ] });
712
+ };
713
+ var PaymentMethodItem = ({
714
+ method,
715
+ allMethods,
716
+ onRemove,
717
+ isBusy,
718
+ pendingRemove,
719
+ onRequestRemove,
720
+ onCancelRemove,
721
+ onConfirmRemove,
722
+ onSetDefault
723
+ }) => {
724
+ const labels = useBillingLabels();
725
+ const itemStyle = {
726
+ borderRadius: VisualTokens.radius.lg,
727
+ border: `1px solid ${method.isDefault ? VisualTokens.color.brandPrimary : VisualTokens.color.borderSubtle}`,
728
+ background: method.isDefault ? VisualTokens.color.surfaceMuted : VisualTokens.color.surfacePrimary,
729
+ padding: VisualTokens.spacing.xl
730
+ };
731
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: itemStyle, children: [
732
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { ...row, justifyContent: "space-between", gap: VisualTokens.spacing.xl }, children: [
733
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { ...row, gap: VisualTokens.spacing.xl, minWidth: 0 }, children: [
734
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
735
+ "div",
736
+ {
737
+ style: {
738
+ width: 48,
739
+ height: 32,
740
+ borderRadius: VisualTokens.radius.sm,
741
+ background: cardGradient(method.card?.brand),
742
+ display: "flex",
743
+ alignItems: "center",
744
+ justifyContent: "center",
745
+ flexShrink: 0
746
+ },
747
+ children: method.card && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.CreditCard, { size: 20, color: VisualTokens.color.textInverse, "aria-hidden": "true" })
748
+ }
749
+ ),
750
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { minWidth: 0 }, children: [
751
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
752
+ "p",
753
+ {
754
+ style: {
755
+ fontWeight: VisualTokens.fontWeight.medium,
756
+ color: VisualTokens.color.textPrimary,
757
+ margin: 0,
758
+ overflow: "hidden",
759
+ textOverflow: "ellipsis",
760
+ whiteSpace: "nowrap"
761
+ },
762
+ children: method.card ? formatCardNumber(method.card.last4, method.card.brand) : labels.paymentMethods.bankAccount
763
+ }
764
+ ),
765
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { ...mutedText, margin: 0 }, children: method.card ? `${labels.paymentMethods.expires} ${formatExpiry(
766
+ method.card.expiryMonth,
767
+ method.card.expiryYear
768
+ )}` : method.bankAccount ? `${method.bankAccount.bankName} \u2022\u2022\u2022\u2022 ${method.bankAccount.last4}` : null })
769
+ ] }),
770
+ method.isDefault && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
771
+ "span",
772
+ {
773
+ style: {
774
+ fontSize: VisualTokens.fontSize.caption,
775
+ background: VisualTokens.color.brandPrimary,
776
+ color: VisualTokens.color.textOnPrimary,
777
+ padding: `${VisualTokens.spacing.xs}px ${VisualTokens.spacing.md}px`,
778
+ borderRadius: VisualTokens.radius.pill,
779
+ fontWeight: VisualTokens.fontWeight.medium,
780
+ flexShrink: 0
781
+ },
782
+ "aria-label": labels.paymentMethods.default,
783
+ children: labels.paymentMethods.default
784
+ }
785
+ )
786
+ ] }),
787
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { ...row, gap: VisualTokens.spacing.sm, flexShrink: 0 }, children: [
788
+ !method.isDefault && onSetDefault && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
789
+ PortalButton,
790
+ {
791
+ type: "button",
792
+ variant: "ghost",
793
+ size: "sm",
794
+ onClick: () => onSetDefault(method.id),
795
+ disabled: isBusy,
796
+ "aria-label": labels.paymentMethods.setDefault,
797
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.Check, { size: 16 })
798
+ }
799
+ ),
800
+ onRemove && paymentMethodsAllowsRemove(method, allMethods) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
801
+ PortalButton,
802
+ {
803
+ type: "button",
804
+ variant: "ghost",
805
+ size: "sm",
806
+ onClick: () => onRequestRemove(method.id),
807
+ disabled: isBusy,
808
+ "aria-label": labels.paymentMethods.remove,
809
+ style: { color: isBusy ? void 0 : VisualTokens.color.brandDanger },
810
+ children: isBusy ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PortalSpinner, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.Trash2, { size: 16 })
811
+ }
812
+ )
813
+ ] })
814
+ ] }),
815
+ pendingRemove && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
816
+ "div",
817
+ {
818
+ role: "alert",
819
+ style: { ...alertDanger, marginTop: VisualTokens.spacing.lg, justifyContent: "space-between" },
820
+ children: [
821
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { ...row, gap: VisualTokens.spacing.md }, children: [
822
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.AlertTriangle, { size: 16, style: { flexShrink: 0 } }),
823
+ labels.paymentMethods.removeConfirm
824
+ ] }),
825
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { ...row, gap: VisualTokens.spacing.sm }, children: [
826
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PortalButton, { type: "button", variant: "ghost", size: "sm", onClick: onCancelRemove, children: labels.paymentMethods.cancelRemove }),
827
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
828
+ PortalButton,
829
+ {
830
+ type: "button",
831
+ variant: "destructive",
832
+ size: "sm",
833
+ onClick: () => onConfirmRemove(method.id),
834
+ children: labels.paymentMethods.confirmRemove
835
+ }
836
+ )
837
+ ] })
838
+ ]
839
+ }
840
+ )
841
+ ] });
842
+ };
843
+ var paymentMethodsAllowsRemove = (method, all) => all.length > 1 || !method.isDefault;
844
+
845
+ // src/presentation/portal/components/InvoiceCard.tsx
846
+ var import_lucide_react2 = require("lucide-react");
847
+ var import_jsx_runtime5 = require("react/jsx-runtime");
848
+ var DATE_FORMAT_OPTIONS = {
849
+ year: "numeric",
850
+ month: "long",
851
+ day: "numeric"
852
+ };
853
+ var openExternal = (url) => {
854
+ window.open(url, "_blank", "noopener,noreferrer");
855
+ };
856
+ var InvoiceCard = ({
857
+ invoice,
858
+ compact = false,
859
+ locale = "en-US",
860
+ onClick,
861
+ onDownload
862
+ }) => {
863
+ const labels = useBillingLabels();
864
+ const handleKeyDown = (e) => {
865
+ if (e.key === "Enter" || e.key === " ") {
866
+ e.preventDefault();
867
+ onClick?.(invoice);
868
+ }
869
+ };
870
+ const statusTone = getInvoiceStatusTone(invoice.status);
871
+ const iconBox = {
872
+ width: compact ? 40 : 48,
873
+ height: compact ? 40 : 48,
874
+ borderRadius: VisualTokens.radius.md,
875
+ background: VisualTokens.color.surfaceMuted,
876
+ display: "flex",
877
+ alignItems: "center",
878
+ justifyContent: "center",
879
+ color: VisualTokens.color.textMuted,
880
+ flexShrink: 0
881
+ };
882
+ if (compact) {
883
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
884
+ "div",
885
+ {
886
+ role: onClick ? "button" : void 0,
887
+ tabIndex: onClick ? 0 : void 0,
888
+ "aria-label": onClick ? `Invoice ${invoice.number}` : void 0,
889
+ onClick: onClick ? () => onClick(invoice) : void 0,
890
+ onKeyDown: onClick ? handleKeyDown : void 0,
891
+ style: { ...cardInteractive, ...row, justifyContent: "space-between", padding: VisualTokens.spacing.xl },
892
+ children: [
893
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...row, gap: VisualTokens.spacing.lg }, children: [
894
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: iconBox, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.FileText, { size: 20 }) }),
895
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
896
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
897
+ "p",
898
+ {
899
+ style: {
900
+ fontSize: VisualTokens.fontSize.body,
901
+ fontWeight: VisualTokens.fontWeight.medium,
902
+ color: VisualTokens.color.textPrimary,
903
+ margin: 0
904
+ },
905
+ children: invoice.number
906
+ }
907
+ ),
908
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { ...mutedText, fontSize: VisualTokens.fontSize.caption, margin: 0 }, children: new Date(invoice.date).toLocaleDateString(locale) })
909
+ ] })
910
+ ] }),
911
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { textAlign: "right" }, children: [
912
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
913
+ "p",
914
+ {
915
+ style: {
916
+ fontWeight: VisualTokens.fontWeight.strong,
917
+ color: VisualTokens.color.textPrimary,
918
+ margin: 0
919
+ },
920
+ children: formatPrice(invoice.amount, invoice.currency)
921
+ }
922
+ ),
923
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
924
+ "p",
925
+ {
926
+ style: {
927
+ fontSize: VisualTokens.fontSize.caption,
928
+ fontWeight: VisualTokens.fontWeight.medium,
929
+ color: TONE_PAIRS[statusTone].fg,
930
+ margin: 0
931
+ },
932
+ children: labels.invoiceStatus[invoice.status]
933
+ }
934
+ )
935
+ ] })
936
+ ]
937
+ }
938
+ );
939
+ }
940
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
941
+ "div",
942
+ {
943
+ role: onClick ? "button" : void 0,
944
+ tabIndex: onClick ? 0 : void 0,
945
+ "aria-label": onClick ? `Invoice ${invoice.number}` : void 0,
946
+ onClick: onClick ? () => onClick(invoice) : void 0,
947
+ onKeyDown: onClick ? handleKeyDown : void 0,
948
+ style: cardInteractive,
949
+ children: [
950
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
951
+ "div",
952
+ {
953
+ style: {
954
+ ...row,
955
+ justifyContent: "space-between",
956
+ gap: VisualTokens.spacing.lg,
957
+ marginBottom: VisualTokens.spacing.xl
958
+ },
959
+ children: [
960
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...row, gap: VisualTokens.spacing.lg }, children: [
961
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: iconBox, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.FileText, { size: 24 }) }),
962
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
963
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
964
+ "p",
965
+ {
966
+ style: {
967
+ fontWeight: VisualTokens.fontWeight.strong,
968
+ color: VisualTokens.color.textPrimary,
969
+ margin: 0
970
+ },
971
+ children: invoice.number
972
+ }
973
+ ),
974
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { ...mutedText, margin: 0 }, children: new Date(invoice.date).toLocaleDateString(locale, DATE_FORMAT_OPTIONS) })
975
+ ] })
976
+ ] }),
977
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { textAlign: "right" }, children: [
978
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
979
+ "p",
980
+ {
981
+ style: {
982
+ fontSize: VisualTokens.fontSize.display,
983
+ fontWeight: VisualTokens.fontWeight.strong,
984
+ color: VisualTokens.color.textPrimary,
985
+ margin: 0
986
+ },
987
+ children: formatPrice(invoice.amount, invoice.currency)
988
+ }
989
+ ),
990
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
991
+ "p",
992
+ {
993
+ style: {
994
+ fontSize: VisualTokens.fontSize.body,
995
+ fontWeight: VisualTokens.fontWeight.medium,
996
+ color: TONE_PAIRS[statusTone].fg,
997
+ margin: 0
998
+ },
999
+ children: labels.invoiceStatus[invoice.status]
1000
+ }
1001
+ )
1002
+ ] })
1003
+ ]
1004
+ }
1005
+ ),
1006
+ invoice.items && invoice.items.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1007
+ "div",
1008
+ {
1009
+ style: {
1010
+ display: "flex",
1011
+ flexDirection: "column",
1012
+ gap: VisualTokens.spacing.sm,
1013
+ marginBottom: VisualTokens.spacing.xl
1014
+ },
1015
+ children: [
1016
+ invoice.items.slice(0, 2).map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1017
+ "div",
1018
+ {
1019
+ style: { ...row, justifyContent: "space-between", fontSize: VisualTokens.fontSize.body },
1020
+ children: [
1021
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: VisualTokens.color.textMuted }, children: item.description }),
1022
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: VisualTokens.color.textPrimary }, children: formatPrice(item.amount, invoice.currency) })
1023
+ ]
1024
+ },
1025
+ `${item.description}-${index}`
1026
+ )),
1027
+ invoice.items.length > 2 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { style: { ...mutedText, fontSize: VisualTokens.fontSize.caption, margin: 0 }, children: [
1028
+ "+",
1029
+ formatNumber(invoice.items.length - 2, locale),
1030
+ " ",
1031
+ labels.invoices.moreItems
1032
+ ] })
1033
+ ]
1034
+ }
1035
+ ),
1036
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1037
+ "div",
1038
+ {
1039
+ style: {
1040
+ ...row,
1041
+ justifyContent: "space-between",
1042
+ paddingTop: VisualTokens.spacing.xl,
1043
+ borderTop: `1px solid ${VisualTokens.color.borderSubtle}`
1044
+ },
1045
+ children: [
1046
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { style: { ...mutedText, fontSize: VisualTokens.fontSize.caption, margin: 0 }, children: [
1047
+ labels.invoices.due,
1048
+ " ",
1049
+ new Date(invoice.dueDate).toLocaleDateString(locale)
1050
+ ] }),
1051
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...row, gap: VisualTokens.spacing.sm }, children: [
1052
+ invoice.invoiceUrl && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1053
+ PortalGhostIconButton,
1054
+ {
1055
+ label: labels.invoices.viewInvoice,
1056
+ onClick: (e) => {
1057
+ e.stopPropagation();
1058
+ openExternal(invoice.invoiceUrl);
1059
+ },
1060
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.ExternalLink, { size: 16 })
1061
+ }
1062
+ ),
1063
+ invoice.pdfUrl && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1064
+ PortalGhostIconButton,
1065
+ {
1066
+ label: labels.invoices.downloadPdf,
1067
+ onClick: (e) => {
1068
+ e.stopPropagation();
1069
+ if (onDownload) {
1070
+ onDownload(invoice);
1071
+ } else {
1072
+ openExternal(invoice.pdfUrl);
1073
+ }
1074
+ },
1075
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.Download, { size: 16 })
1076
+ }
1077
+ )
1078
+ ] })
1079
+ ]
1080
+ }
1081
+ )
1082
+ ]
1083
+ }
1084
+ );
1085
+ };
1086
+ var PortalGhostIconButton = ({
1087
+ label,
1088
+ onClick,
1089
+ children
1090
+ }) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1091
+ "button",
1092
+ {
1093
+ type: "button",
1094
+ "aria-label": label,
1095
+ onClick,
1096
+ style: {
1097
+ display: "inline-flex",
1098
+ alignItems: "center",
1099
+ justifyContent: "center",
1100
+ padding: VisualTokens.spacing.xs,
1101
+ borderRadius: VisualTokens.radius.sm,
1102
+ border: "1px solid transparent",
1103
+ background: "transparent",
1104
+ color: VisualTokens.color.textMuted,
1105
+ cursor: "pointer"
1106
+ },
1107
+ children
1108
+ }
1109
+ );
1110
+
1111
+ // src/presentation/portal/components/PlanTab.tsx
1112
+ var import_react3 = require("react");
1113
+ var import_lucide_react3 = require("lucide-react");
1114
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1115
+ var PlanTab = ({
1116
+ subscription,
1117
+ onChangePlan,
1118
+ onCancelSubscription,
1119
+ locale = "en-US"
1120
+ }) => {
1121
+ const labels = useBillingLabels();
1122
+ const [confirmingCancel, setConfirmingCancel] = (0, import_react3.useState)(false);
1123
+ const [cancelling, setCancelling] = (0, import_react3.useState)(false);
1124
+ const [cancelError, setCancelError] = (0, import_react3.useState)(null);
1125
+ const handleCancel = async () => {
1126
+ if (!onCancelSubscription) return;
1127
+ if (!confirmingCancel) {
1128
+ setConfirmingCancel(true);
1129
+ return;
1130
+ }
1131
+ setCancelling(true);
1132
+ setCancelError(null);
1133
+ try {
1134
+ await onCancelSubscription();
1135
+ } catch {
1136
+ setCancelError(labels.plan.cancelFailed);
1137
+ } finally {
1138
+ setCancelling(false);
1139
+ setConfirmingCancel(false);
1140
+ }
1141
+ };
1142
+ const isActive = subscription.status === "active" || subscription.status === "trialing";
1143
+ const statusTone = getStatusTone(subscription.status);
1144
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: stack, children: [
1145
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { style: card, "aria-labelledby": "plan-current-heading", children: [
1146
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { id: "plan-current-heading", style: sectionTitle, children: labels.plan.title }),
1147
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1148
+ "div",
1149
+ {
1150
+ style: {
1151
+ display: "flex",
1152
+ alignItems: "flex-start",
1153
+ justifyContent: "space-between",
1154
+ gap: VisualTokens.spacing.xl,
1155
+ flexWrap: "wrap"
1156
+ },
1157
+ children: [
1158
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
1159
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1160
+ "p",
1161
+ {
1162
+ style: {
1163
+ fontSize: VisualTokens.fontSize.display,
1164
+ fontWeight: VisualTokens.fontWeight.strong,
1165
+ color: VisualTokens.color.textPrimary,
1166
+ margin: 0
1167
+ },
1168
+ children: subscription.plan.name
1169
+ }
1170
+ ),
1171
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1172
+ "p",
1173
+ {
1174
+ style: {
1175
+ fontSize: VisualTokens.fontSize.body,
1176
+ fontWeight: VisualTokens.fontWeight.medium,
1177
+ color: TONE_PAIRS[statusTone].fg,
1178
+ margin: 0,
1179
+ marginTop: VisualTokens.spacing.xs
1180
+ },
1181
+ children: labels.status[subscription.status]
1182
+ }
1183
+ ),
1184
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1185
+ "p",
1186
+ {
1187
+ style: {
1188
+ fontSize: VisualTokens.fontSize.title,
1189
+ fontWeight: VisualTokens.fontWeight.strong,
1190
+ color: VisualTokens.color.textPrimary,
1191
+ margin: 0,
1192
+ marginTop: VisualTokens.spacing.lg
1193
+ },
1194
+ children: [
1195
+ formatPrice(
1196
+ getPlanPrice(subscription.plan, subscription.cycle),
1197
+ subscription.plan.currency
1198
+ ),
1199
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1200
+ "span",
1201
+ {
1202
+ style: {
1203
+ fontSize: VisualTokens.fontSize.body,
1204
+ fontWeight: VisualTokens.fontWeight.regular,
1205
+ color: VisualTokens.color.textMuted
1206
+ },
1207
+ children: [
1208
+ "/",
1209
+ subscription.cycle
1210
+ ]
1211
+ }
1212
+ )
1213
+ ]
1214
+ }
1215
+ ),
1216
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1217
+ "p",
1218
+ {
1219
+ style: {
1220
+ display: "flex",
1221
+ alignItems: "center",
1222
+ gap: 6,
1223
+ fontSize: VisualTokens.fontSize.caption,
1224
+ color: VisualTokens.color.textMuted,
1225
+ margin: 0,
1226
+ marginTop: VisualTokens.spacing.sm
1227
+ },
1228
+ children: [
1229
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Calendar, { size: 14, "aria-hidden": "true" }),
1230
+ subscription.currentPeriodStart && new Date(subscription.currentPeriodStart).toLocaleDateString(locale),
1231
+ " \u2192 ",
1232
+ subscription.currentPeriodEnd && new Date(subscription.currentPeriodEnd).toLocaleDateString(locale)
1233
+ ]
1234
+ }
1235
+ )
1236
+ ] }),
1237
+ isActive ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1238
+ import_lucide_react3.CheckCircle2,
1239
+ {
1240
+ size: 24,
1241
+ color: TONE_PAIRS.active.fg,
1242
+ "aria-hidden": "true"
1243
+ }
1244
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.XCircle, { size: 24, color: VisualTokens.color.brandDanger, "aria-hidden": "true" })
1245
+ ]
1246
+ }
1247
+ )
1248
+ ] }),
1249
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { style: card, "aria-labelledby": "plan-actions-heading", children: [
1250
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1251
+ "h4",
1252
+ {
1253
+ id: "plan-actions-heading",
1254
+ style: {
1255
+ fontSize: VisualTokens.fontSize.body,
1256
+ fontWeight: VisualTokens.fontWeight.strong,
1257
+ color: VisualTokens.color.textPrimary,
1258
+ margin: 0,
1259
+ marginBottom: VisualTokens.spacing.xl
1260
+ },
1261
+ children: labels.plan.actionsTitle
1262
+ }
1263
+ ),
1264
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: VisualTokens.spacing.lg }, children: [
1265
+ onChangePlan && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PortalButton, { onClick: onChangePlan, variant: "default", children: labels.plan.changePlan }),
1266
+ onCancelSubscription && isActive && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1267
+ PortalButton,
1268
+ {
1269
+ onClick: handleCancel,
1270
+ variant: confirmingCancel ? "destructive" : "outline",
1271
+ disabled: cancelling,
1272
+ children: cancelling ? labels.plan.cancelling : confirmingCancel ? labels.plan.confirmCancel : labels.plan.cancelSubscription
1273
+ }
1274
+ )
1275
+ ] }),
1276
+ confirmingCancel && !cancelError && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { role: "alert", style: { ...alertDanger, marginTop: VisualTokens.spacing.xl }, children: [
1277
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.AlertTriangle, { size: 16, style: { marginTop: 2, flexShrink: 0 }, "aria-hidden": "true" }),
1278
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: labels.plan.cancelConfirmMessage })
1279
+ ] }),
1280
+ cancelError && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { role: "alert", style: { ...alertDanger, marginTop: VisualTokens.spacing.xl }, children: [
1281
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.AlertTriangle, { size: 16, style: { marginTop: 2, flexShrink: 0 }, "aria-hidden": "true" }),
1282
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: cancelError })
1283
+ ] })
1284
+ ] })
1285
+ ] });
1286
+ };
1287
+
1288
+ // src/presentation/portal/components/UsageCard.tsx
1289
+ var import_lucide_react4 = require("lucide-react");
1290
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1291
+ var UsageCard = ({
1292
+ metric,
1293
+ locale = "en-US",
1294
+ showProgress = true,
1295
+ showLimit = true
1296
+ }) => {
1297
+ const labels = useBillingLabels();
1298
+ const { percentage, isOverLimit, nearLimit, isUnlimited } = evaluateUsage(metric);
1299
+ const tone = isOverLimit ? "danger" : nearLimit ? "warning" : "active";
1300
+ const toneColor = TONE_PAIRS[tone].fg;
1301
+ const barColor = isOverLimit ? VisualTokens.color.brandDanger : nearLimit ? VisualTokens.color.badgeWarningFg : VisualTokens.color.brandPrimary;
1302
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { ...card, display: "flex", flexDirection: "column", gap: VisualTokens.spacing.lg }, children: [
1303
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { ...row, justifyContent: "space-between", gap: VisualTokens.spacing.lg }, children: [
1304
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
1305
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { style: { ...mutedText, fontSize: VisualTokens.fontSize.caption, margin: 0, marginBottom: VisualTokens.spacing.xs }, children: metric.name }),
1306
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1307
+ "p",
1308
+ {
1309
+ style: {
1310
+ fontSize: VisualTokens.fontSize.display,
1311
+ fontWeight: VisualTokens.fontWeight.strong,
1312
+ color: VisualTokens.color.textPrimary,
1313
+ margin: 0
1314
+ },
1315
+ children: [
1316
+ formatNumber(metric.current, locale),
1317
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1318
+ "span",
1319
+ {
1320
+ style: {
1321
+ fontSize: VisualTokens.fontSize.body,
1322
+ fontWeight: VisualTokens.fontWeight.regular,
1323
+ color: VisualTokens.color.textMuted,
1324
+ marginLeft: VisualTokens.spacing.xs
1325
+ },
1326
+ children: metric.unit
1327
+ }
1328
+ )
1329
+ ]
1330
+ }
1331
+ )
1332
+ ] }),
1333
+ (nearLimit || isOverLimit) && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1334
+ "div",
1335
+ {
1336
+ style: {
1337
+ width: 40,
1338
+ height: 40,
1339
+ borderRadius: VisualTokens.radius.md,
1340
+ display: "flex",
1341
+ alignItems: "center",
1342
+ justifyContent: "center",
1343
+ color: toneColor,
1344
+ background: TONE_PAIRS[tone].bg,
1345
+ flexShrink: 0
1346
+ },
1347
+ "aria-hidden": "true",
1348
+ children: isOverLimit ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react4.AlertTriangle, { size: 20 }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react4.TrendingUp, { size: 20 })
1349
+ }
1350
+ )
1351
+ ] }),
1352
+ showProgress && !isUnlimited && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: VisualTokens.spacing.sm }, children: [
1353
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1354
+ "div",
1355
+ {
1356
+ style: {
1357
+ ...row,
1358
+ justifyContent: "space-between",
1359
+ fontSize: VisualTokens.fontSize.caption
1360
+ },
1361
+ children: [
1362
+ showLimit && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { style: { color: VisualTokens.color.textMuted }, children: [
1363
+ formatNumber(metric.limit, locale),
1364
+ " ",
1365
+ metric.unit,
1366
+ " ",
1367
+ labels.usage.limit
1368
+ ] }),
1369
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1370
+ "span",
1371
+ {
1372
+ style: {
1373
+ fontWeight: VisualTokens.fontWeight.medium,
1374
+ color: isOverLimit || nearLimit ? toneColor : VisualTokens.color.textPrimary
1375
+ },
1376
+ children: [
1377
+ percentage.toFixed(0),
1378
+ "%"
1379
+ ]
1380
+ }
1381
+ )
1382
+ ]
1383
+ }
1384
+ ),
1385
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1386
+ "div",
1387
+ {
1388
+ style: {
1389
+ width: "100%",
1390
+ height: 8,
1391
+ background: VisualTokens.color.surfaceSubtle,
1392
+ borderRadius: VisualTokens.radius.pill,
1393
+ overflow: "hidden"
1394
+ },
1395
+ role: "progressbar",
1396
+ "aria-label": `${metric.name} ${labels.common.usage}`,
1397
+ "aria-valuemin": 0,
1398
+ "aria-valuemax": Math.max(metric.limit, metric.current),
1399
+ "aria-valuenow": metric.current,
1400
+ "aria-valuetext": `${formatNumber(metric.current, locale)} / ${formatNumber(metric.limit, locale)} ${metric.unit}`,
1401
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1402
+ "div",
1403
+ {
1404
+ style: {
1405
+ height: "100%",
1406
+ background: barColor,
1407
+ width: `${Math.min(percentage, 100)}%`
1408
+ }
1409
+ }
1410
+ )
1411
+ }
1412
+ ),
1413
+ metric.resetAt && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { style: { ...mutedText, fontSize: VisualTokens.fontSize.caption, margin: 0 }, children: [
1414
+ labels.usage.resets,
1415
+ " ",
1416
+ new Date(metric.resetAt).toLocaleDateString(locale)
1417
+ ] })
1418
+ ] })
1419
+ ] });
1420
+ };
1421
+ var evaluateUsage = (metric) => {
1422
+ if (metric.limit <= 0) {
1423
+ return { percentage: 0, isOverLimit: false, nearLimit: false, isUnlimited: true };
1424
+ }
1425
+ return {
1426
+ percentage: calculateUsagePercentage(metric),
1427
+ isOverLimit: metric.current > metric.limit,
1428
+ nearLimit: isNearLimit(metric),
1429
+ isUnlimited: false
1430
+ };
1431
+ };
1432
+
1433
+ // src/presentation/portal/components/OverviewTab.tsx
1434
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1435
+ var OverviewTab = ({ billing, locale }) => {
1436
+ const labels = useBillingLabels();
1437
+ const { subscription, usage, upcomingInvoice } = billing;
1438
+ const statusTone = getStatusTone(subscription.status);
1439
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: stack, children: [
1440
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { style: card, "aria-labelledby": "overview-subscription-heading", children: [
1441
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { id: "overview-subscription-heading", style: sectionTitle, children: labels.overview.currentSubscription }),
1442
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: VisualTokens.spacing.lg }, children: [
1443
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1444
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1445
+ "p",
1446
+ {
1447
+ style: {
1448
+ fontSize: VisualTokens.fontSize.display,
1449
+ fontWeight: VisualTokens.fontWeight.strong,
1450
+ color: VisualTokens.color.textPrimary,
1451
+ margin: 0
1452
+ },
1453
+ children: subscription.plan.name
1454
+ }
1455
+ ),
1456
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1457
+ "p",
1458
+ {
1459
+ style: {
1460
+ fontSize: VisualTokens.fontSize.body,
1461
+ fontWeight: VisualTokens.fontWeight.medium,
1462
+ color: TONE_PAIRS[statusTone].fg,
1463
+ margin: 0
1464
+ },
1465
+ children: labels.status[subscription.status]
1466
+ }
1467
+ ),
1468
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1469
+ "p",
1470
+ {
1471
+ style: {
1472
+ fontSize: VisualTokens.fontSize.body,
1473
+ color: VisualTokens.color.textMuted,
1474
+ margin: 0,
1475
+ marginTop: VisualTokens.spacing.xs
1476
+ },
1477
+ children: [
1478
+ formatPrice(
1479
+ getPlanPrice(subscription.plan, subscription.cycle),
1480
+ subscription.plan.currency
1481
+ ),
1482
+ "/",
1483
+ subscription.cycle
1484
+ ]
1485
+ }
1486
+ )
1487
+ ] }),
1488
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { textAlign: "right" }, children: [
1489
+ subscription.status === "trialing" && subscription.trialEnd && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1490
+ "p",
1491
+ {
1492
+ style: {
1493
+ fontSize: VisualTokens.fontSize.body,
1494
+ color: VisualTokens.color.textMuted,
1495
+ margin: 0
1496
+ },
1497
+ children: [
1498
+ getDaysRemaining(subscription.trialEnd),
1499
+ " ",
1500
+ labels.overview.trialDaysLeft
1501
+ ]
1502
+ }
1503
+ ),
1504
+ upcomingInvoice && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1505
+ "p",
1506
+ {
1507
+ style: {
1508
+ fontSize: VisualTokens.fontSize.body,
1509
+ color: VisualTokens.color.textMuted,
1510
+ margin: 0
1511
+ },
1512
+ children: [
1513
+ labels.overview.nextBilling,
1514
+ " ",
1515
+ new Date(upcomingInvoice.date).toLocaleDateString(locale)
1516
+ ]
1517
+ }
1518
+ )
1519
+ ] })
1520
+ ] })
1521
+ ] }),
1522
+ usage.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("section", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: VisualTokens.spacing.xl }, "aria-label": labels.common.usage, children: usage.map((metric) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(UsageCard, { metric, locale }, metric.id)) }),
1523
+ upcomingInvoice && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { style: card, "aria-labelledby": "overview-upcoming-heading", children: [
1524
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { id: "overview-upcoming-heading", style: { ...sectionTitle, marginBottom: VisualTokens.spacing.md }, children: labels.overview.upcomingInvoice }),
1525
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1526
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1527
+ "p",
1528
+ {
1529
+ style: {
1530
+ fontSize: VisualTokens.fontSize.display,
1531
+ fontWeight: VisualTokens.fontWeight.strong,
1532
+ color: VisualTokens.color.textPrimary,
1533
+ margin: 0
1534
+ },
1535
+ children: formatPrice(upcomingInvoice.amount, upcomingInvoice.currency)
1536
+ }
1537
+ ),
1538
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1539
+ "p",
1540
+ {
1541
+ style: {
1542
+ fontSize: VisualTokens.fontSize.body,
1543
+ color: VisualTokens.color.textMuted,
1544
+ margin: 0
1545
+ },
1546
+ children: [
1547
+ labels.invoices.due,
1548
+ " ",
1549
+ new Date(upcomingInvoice.date).toLocaleDateString(locale)
1550
+ ]
1551
+ }
1552
+ )
1553
+ ] }) })
1554
+ ] })
1555
+ ] });
1556
+ };
1557
+
1558
+ // src/presentation/portal/components/BillingPortal.tsx
1559
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1560
+ var TABS = [
1561
+ { id: "overview", labelKey: "overview", icon: import_lucide_react5.BarChart3 },
1562
+ { id: "payment-methods", labelKey: "paymentMethods", icon: import_lucide_react5.CreditCard },
1563
+ { id: "invoices", labelKey: "invoices", icon: import_lucide_react5.FileText },
1564
+ { id: "plan", labelKey: "plan", icon: import_lucide_react5.Settings }
1565
+ ];
1566
+ var BillingPortal = ({
1567
+ billing,
1568
+ loading = false,
1569
+ error,
1570
+ showTabs = true,
1571
+ activeTab = "overview",
1572
+ locale = "en-US",
1573
+ onTabChange,
1574
+ onAddPaymentMethod,
1575
+ onSetDefaultPaymentMethod,
1576
+ onRemovePaymentMethod,
1577
+ onViewInvoice,
1578
+ onChangePlan,
1579
+ onCancelSubscription,
1580
+ labels
1581
+ }) => {
1582
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(BillingLabelsProvider, { labels, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1583
+ BillingPortalBody,
1584
+ {
1585
+ billing,
1586
+ loading,
1587
+ error,
1588
+ showTabs,
1589
+ activeTab,
1590
+ locale,
1591
+ onTabChange,
1592
+ onAddPaymentMethod,
1593
+ onSetDefaultPaymentMethod,
1594
+ onRemovePaymentMethod,
1595
+ onViewInvoice,
1596
+ onChangePlan,
1597
+ onCancelSubscription
1598
+ }
1599
+ ) });
1600
+ };
1601
+ var BillingPortalBody = ({
1602
+ billing,
1603
+ loading = false,
1604
+ error,
1605
+ showTabs = true,
1606
+ activeTab = "overview",
1607
+ locale = "en-US",
1608
+ onTabChange,
1609
+ onAddPaymentMethod,
1610
+ onSetDefaultPaymentMethod,
1611
+ onRemovePaymentMethod,
1612
+ onViewInvoice,
1613
+ onChangePlan,
1614
+ onCancelSubscription
1615
+ }) => {
1616
+ const labels = useBillingLabels();
1617
+ if (loading) {
1618
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1619
+ "div",
1620
+ {
1621
+ style: {
1622
+ display: "flex",
1623
+ alignItems: "center",
1624
+ justifyContent: "center",
1625
+ padding: `${VisualTokens.spacing.page * 3}px 0`,
1626
+ color: VisualTokens.color.textMuted
1627
+ },
1628
+ role: "status",
1629
+ "aria-live": "polite",
1630
+ children: [
1631
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PortalSpinner, { size: 48 }),
1632
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: visuallyHidden, children: labels.common.loading })
1633
+ ]
1634
+ }
1635
+ );
1636
+ }
1637
+ if (error) {
1638
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1639
+ "div",
1640
+ {
1641
+ style: {
1642
+ display: "flex",
1643
+ alignItems: "center",
1644
+ justifyContent: "center",
1645
+ gap: VisualTokens.spacing.xl,
1646
+ padding: `${VisualTokens.spacing.page * 3}px 0`,
1647
+ color: VisualTokens.color.brandDangerText
1648
+ },
1649
+ role: "alert",
1650
+ children: [
1651
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react5.AlertCircle, { size: 24, "aria-hidden": "true" }),
1652
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: { margin: 0 }, children: error })
1653
+ ]
1654
+ }
1655
+ );
1656
+ }
1657
+ if (!billing) return null;
1658
+ const renderContent = () => {
1659
+ switch (activeTab) {
1660
+ case "overview":
1661
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(OverviewTab, { billing, locale });
1662
+ case "payment-methods":
1663
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1664
+ PaymentMethodsList,
1665
+ {
1666
+ paymentMethods: billing.paymentMethods,
1667
+ onAddNew: onAddPaymentMethod,
1668
+ onSetDefault: onSetDefaultPaymentMethod,
1669
+ onRemove: onRemovePaymentMethod
1670
+ }
1671
+ );
1672
+ case "invoices":
1673
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(InvoiceList, { invoices: billing.recentInvoices, onView: onViewInvoice, locale });
1674
+ case "plan":
1675
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1676
+ PlanTab,
1677
+ {
1678
+ subscription: billing.subscription,
1679
+ onChangePlan,
1680
+ onCancelSubscription,
1681
+ locale
1682
+ }
1683
+ );
1684
+ default:
1685
+ return null;
1686
+ }
1687
+ };
1688
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { width: "100%" }, children: [
1689
+ showTabs && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1690
+ "div",
1691
+ {
1692
+ style: { borderBottom: `1px solid ${VisualTokens.color.borderSubtle}`, marginBottom: VisualTokens.spacing.section },
1693
+ role: "tablist",
1694
+ "aria-orientation": "horizontal",
1695
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { display: "flex", gap: VisualTokens.spacing.section }, children: TABS.map((tab) => {
1696
+ const Icon = tab.icon;
1697
+ const isActive = activeTab === tab.id;
1698
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1699
+ "button",
1700
+ {
1701
+ id: `billing-tab-${tab.id}`,
1702
+ role: "tab",
1703
+ type: "button",
1704
+ "aria-selected": isActive,
1705
+ "aria-controls": "billing-tab-panel",
1706
+ tabIndex: isActive ? 0 : -1,
1707
+ onClick: () => onTabChange?.(tab.id),
1708
+ onKeyDown: (e) => {
1709
+ if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
1710
+ e.preventDefault();
1711
+ const idx = TABS.findIndex((tt) => tt.id === activeTab);
1712
+ const nextIdx = e.key === "ArrowRight" ? (idx + 1) % TABS.length : (idx - 1 + TABS.length) % TABS.length;
1713
+ onTabChange?.(TABS[nextIdx].id);
1714
+ }
1715
+ },
1716
+ style: {
1717
+ display: "flex",
1718
+ alignItems: "center",
1719
+ gap: VisualTokens.spacing.sm,
1720
+ paddingBottom: VisualTokens.spacing.section,
1721
+ border: "none",
1722
+ borderBottom: `2px solid ${isActive ? VisualTokens.color.brandPrimary : "transparent"}`,
1723
+ background: "transparent",
1724
+ fontSize: VisualTokens.fontSize.body,
1725
+ fontWeight: isActive ? VisualTokens.fontWeight.medium : VisualTokens.fontWeight.regular,
1726
+ color: isActive ? VisualTokens.color.textPrimary : VisualTokens.color.textMuted,
1727
+ cursor: "pointer"
1728
+ },
1729
+ children: [
1730
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Icon, { size: 16, "aria-hidden": "true" }),
1731
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: labels.tabs[tab.labelKey] })
1732
+ ]
1733
+ },
1734
+ tab.id
1735
+ );
1736
+ }) })
1737
+ }
1738
+ ),
1739
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1740
+ "div",
1741
+ {
1742
+ id: "billing-tab-panel",
1743
+ role: "tabpanel",
1744
+ "aria-labelledby": `billing-tab-${activeTab}`,
1745
+ children: renderContent()
1746
+ }
1747
+ )
1748
+ ] });
1749
+ };
1750
+ var InvoiceList = ({
1751
+ invoices,
1752
+ onView,
1753
+ locale
1754
+ }) => {
1755
+ const labels = useBillingLabels();
1756
+ if (invoices.length === 0) {
1757
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1758
+ "div",
1759
+ {
1760
+ style: {
1761
+ textAlign: "center",
1762
+ padding: `${VisualTokens.spacing.page * 1.5}px 0`,
1763
+ color: VisualTokens.color.textMuted
1764
+ },
1765
+ children: [
1766
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react5.FileText, { size: 48, style: { opacity: 0.5, marginBottom: VisualTokens.spacing.xl }, "aria-hidden": "true" }),
1767
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: { margin: 0 }, children: labels.invoices.empty })
1768
+ ]
1769
+ }
1770
+ );
1771
+ }
1772
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: stack, children: invoices.map((invoice) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(InvoiceCard, { invoice, onClick: onView, locale }, invoice.id)) });
1773
+ };
1774
+
1775
+ // src/presentation/portal/components/BillingLayout.tsx
1776
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1777
+ var BRAND_LOGO_SIZE = 32;
1778
+ var DefaultBrandMark = ({ initial }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1779
+ "span",
1780
+ {
1781
+ "aria-hidden": "true",
1782
+ style: {
1783
+ width: BRAND_LOGO_SIZE,
1784
+ height: BRAND_LOGO_SIZE,
1785
+ borderRadius: VisualTokens.radius.md,
1786
+ background: VisualTokens.color.brandPrimary,
1787
+ color: VisualTokens.color.textOnPrimary,
1788
+ display: "inline-flex",
1789
+ alignItems: "center",
1790
+ justifyContent: "center",
1791
+ fontWeight: VisualTokens.fontWeight.strong,
1792
+ fontSize: VisualTokens.fontSize.body,
1793
+ flexShrink: 0
1794
+ },
1795
+ children: initial
1796
+ }
1797
+ );
1798
+ var container = {
1799
+ maxWidth: 1152,
1800
+ marginLeft: "auto",
1801
+ marginRight: "auto",
1802
+ paddingLeft: VisualTokens.spacing.section,
1803
+ paddingRight: VisualTokens.spacing.section
1804
+ };
1805
+ var BillingLayout = ({ config, children, onBack }) => {
1806
+ const labels = useBillingLabels();
1807
+ const dashboardRoute = config.cancelRoute ?? "/dashboard";
1808
+ const contactAria = formatLabel(labels.page.contactAria, { email: config.supportEmail ?? "" });
1809
+ const backControl = () => onBack ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1810
+ "button",
1811
+ {
1812
+ type: "button",
1813
+ onClick: onBack,
1814
+ style: {
1815
+ fontSize: VisualTokens.fontSize.body,
1816
+ color: VisualTokens.color.textMuted,
1817
+ background: "transparent",
1818
+ border: "none",
1819
+ padding: 0,
1820
+ cursor: "pointer"
1821
+ },
1822
+ children: labels.page.backToDashboard
1823
+ }
1824
+ ) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1825
+ "a",
1826
+ {
1827
+ href: dashboardRoute,
1828
+ style: {
1829
+ fontSize: VisualTokens.fontSize.body,
1830
+ color: VisualTokens.color.textMuted,
1831
+ textDecoration: "none"
1832
+ },
1833
+ children: labels.page.backToDashboard
1834
+ }
1835
+ );
1836
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1837
+ "div",
1838
+ {
1839
+ style: {
1840
+ minHeight: "100vh",
1841
+ background: VisualTokens.color.surfaceMuted,
1842
+ display: "flex",
1843
+ flexDirection: "column"
1844
+ },
1845
+ children: [
1846
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1847
+ "header",
1848
+ {
1849
+ style: {
1850
+ borderBottom: `1px solid ${VisualTokens.color.borderSubtle}`,
1851
+ padding: `${VisualTokens.spacing.xl}px ${VisualTokens.spacing.section}px`,
1852
+ display: "flex",
1853
+ alignItems: "center",
1854
+ justifyContent: "space-between",
1855
+ background: VisualTokens.color.surfacePrimary
1856
+ },
1857
+ children: [
1858
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1859
+ "a",
1860
+ {
1861
+ href: dashboardRoute,
1862
+ onClick: onBack ? (e) => {
1863
+ e.preventDefault();
1864
+ onBack();
1865
+ } : void 0,
1866
+ style: { display: "flex", alignItems: "center", gap: VisualTokens.spacing.sm, textDecoration: "none" },
1867
+ "aria-label": config.brandName,
1868
+ children: [
1869
+ config.logo ?? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DefaultBrandMark, { initial: config.brandName.charAt(0).toUpperCase() || "?" }),
1870
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1871
+ "span",
1872
+ {
1873
+ style: {
1874
+ fontWeight: VisualTokens.fontWeight.strong,
1875
+ fontSize: VisualTokens.fontSize.title,
1876
+ color: VisualTokens.color.textPrimary
1877
+ },
1878
+ children: config.brandName
1879
+ }
1880
+ )
1881
+ ]
1882
+ }
1883
+ ),
1884
+ backControl()
1885
+ ]
1886
+ }
1887
+ ),
1888
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("main", { style: { flex: 1, ...container, paddingTop: 48, paddingBottom: 48, width: "100%", boxSizing: "border-box" }, children: [
1889
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { marginBottom: VisualTokens.spacing.page * 1.5 }, children: [
1890
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1891
+ "h1",
1892
+ {
1893
+ style: {
1894
+ fontSize: 32,
1895
+ fontWeight: VisualTokens.fontWeight.strong,
1896
+ color: VisualTokens.color.textPrimary,
1897
+ margin: 0,
1898
+ marginBottom: VisualTokens.spacing.sm
1899
+ },
1900
+ children: labels.page.title
1901
+ }
1902
+ ),
1903
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: { color: VisualTokens.color.textMuted, margin: 0 }, children: labels.page.description })
1904
+ ] }),
1905
+ children
1906
+ ] }),
1907
+ config.supportEmail && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1908
+ "footer",
1909
+ {
1910
+ style: {
1911
+ borderTop: `1px solid ${VisualTokens.color.borderSubtle}`,
1912
+ padding: `${VisualTokens.spacing.xl}px ${VisualTokens.spacing.section}px`,
1913
+ background: VisualTokens.color.surfacePrimary
1914
+ },
1915
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { ...container, textAlign: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1916
+ "p",
1917
+ {
1918
+ style: {
1919
+ fontSize: VisualTokens.fontSize.body,
1920
+ color: VisualTokens.color.textMuted,
1921
+ margin: 0
1922
+ },
1923
+ children: [
1924
+ labels.page.needHelp,
1925
+ " ",
1926
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1927
+ "a",
1928
+ {
1929
+ href: `mailto:${config.supportEmail}`,
1930
+ style: { color: VisualTokens.color.brandPrimary },
1931
+ "aria-label": contactAria,
1932
+ children: config.supportEmail
1933
+ }
1934
+ )
1935
+ ]
1936
+ }
1937
+ ) })
1938
+ }
1939
+ )
1940
+ ]
1941
+ }
1942
+ );
1943
+ };
1944
+
1945
+ // src/presentation/portal/constants.ts
1946
+ var SUBSCRIPTION_STATUS = {
1947
+ ACTIVE: "active",
1948
+ TRIALING: "trialing",
1949
+ PAST_DUE: "past_due",
1950
+ CANCELED: "canceled",
1951
+ UNPAID: "unpaid",
1952
+ INCOMPLETE: "incomplete",
1953
+ REVOKED: "revoked"
1954
+ };
1955
+ var INVOICE_STATUS = {
1956
+ DRAFT: "draft",
1957
+ OPEN: "open",
1958
+ PAID: "paid",
1959
+ VOID: "void",
1960
+ UNCOLLECTIBLE: "uncollectible",
1961
+ REFUNDED: "refunded"
1962
+ };
1963
+ var CURRENCY = {
1964
+ USD: "USD",
1965
+ EUR: "EUR",
1966
+ GBP: "GBP",
1967
+ TRY: "TRY",
1968
+ JPY: "JPY"
1969
+ };
1970
+ var PLAN_TYPE = {
1971
+ STANDARD: "standard",
1972
+ PRO: "pro",
1973
+ BUSINESS: "business",
1974
+ ENTERPRISE: "enterprise"
1975
+ };
1976
+ var DEFAULT_BILLING_CONFIG = {
1977
+ enableYearlyDiscount: true,
1978
+ yearlyDiscount: 20,
1979
+ currencies: [CURRENCY.USD, CURRENCY.EUR, CURRENCY.GBP],
1980
+ defaultCurrency: CURRENCY.USD,
1981
+ taxRate: 0
1982
+ };
1983
+
1984
+ // src/presentation/portal/components/BillingPage.tsx
1985
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1986
+ var BillingPage = ({ config, labels, ...portalProps }) => {
1987
+ const mergedConfig = {
1988
+ ...DEFAULT_BILLING_CONFIG,
1989
+ ...config
1990
+ };
1991
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BillingLabelsProvider, { labels, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BillingLayout, { config: mergedConfig, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BillingPortal, { showTabs: true, ...portalProps }) }) });
1992
+ };
1993
+
1994
+ // src/presentation/portal/components/PlanComparison.tsx
1995
+ var import_lucide_react6 = require("lucide-react");
1996
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1997
+ var RADIO_GROUP_NAME = "plan-comparison";
1998
+ var PlanComparison = ({
1999
+ plans,
2000
+ selectedPlan,
2001
+ cycle = "monthly",
2002
+ showCycleToggle = true,
2003
+ showFeatures = true,
2004
+ onPlanSelect,
2005
+ onCycleChange,
2006
+ loading = false,
2007
+ labels: labelsOverride
2008
+ }) => {
2009
+ const labels = useBillingLabels();
2010
+ const discount = pickLargestDiscount(plans);
2011
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(BillingLabelsProvider, { labels: labelsOverride, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { width: "100%", display: "flex", flexDirection: "column", gap: VisualTokens.spacing.page }, children: [
2012
+ showCycleToggle && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2013
+ "div",
2014
+ {
2015
+ style: { display: "flex", alignItems: "center", justifyContent: "center" },
2016
+ role: "group",
2017
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(CycleToggle, { cycle, onChange: onCycleChange, discountPercent: discount })
2018
+ }
2019
+ ),
2020
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2021
+ "div",
2022
+ {
2023
+ style: {
2024
+ display: "grid",
2025
+ gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))",
2026
+ gap: VisualTokens.spacing.section
2027
+ },
2028
+ role: "radiogroup",
2029
+ "aria-label": labels.planComparison.radiogroupLabel,
2030
+ children: plans.map((plan, index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2031
+ PlanCard,
2032
+ {
2033
+ plan,
2034
+ isSelected: selectedPlan === plan.id,
2035
+ isPopular: isPopularPlan(plan, plans),
2036
+ cycle,
2037
+ showFeatures,
2038
+ loading,
2039
+ onSelect: onPlanSelect,
2040
+ tabbable: selectedPlan === plan.id || !selectedPlan && index === 0
2041
+ },
2042
+ plan.id
2043
+ ))
2044
+ }
2045
+ )
2046
+ ] }) });
2047
+ };
2048
+ var pillBase = {
2049
+ padding: `${VisualTokens.spacing.sm}px ${VisualTokens.spacing.section}px`,
2050
+ borderRadius: VisualTokens.radius.pill,
2051
+ fontSize: VisualTokens.fontSize.body,
2052
+ fontWeight: VisualTokens.fontWeight.medium,
2053
+ cursor: "pointer",
2054
+ border: "1px solid transparent"
2055
+ };
2056
+ var CycleToggle = ({ cycle, onChange, discountPercent }) => {
2057
+ const labels = useBillingLabels();
2058
+ const active = {
2059
+ background: VisualTokens.color.surfacePrimary,
2060
+ color: VisualTokens.color.textPrimary
2061
+ };
2062
+ const inactive = {
2063
+ background: "transparent",
2064
+ color: VisualTokens.color.textMuted
2065
+ };
2066
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2067
+ "div",
2068
+ {
2069
+ style: {
2070
+ display: "inline-flex",
2071
+ alignItems: "center",
2072
+ background: VisualTokens.color.surfaceSubtle,
2073
+ borderRadius: VisualTokens.radius.pill,
2074
+ padding: VisualTokens.spacing.xs,
2075
+ gap: VisualTokens.spacing.xs
2076
+ },
2077
+ children: [
2078
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2079
+ "button",
2080
+ {
2081
+ type: "button",
2082
+ "aria-pressed": cycle === "monthly",
2083
+ onClick: () => onChange?.("monthly"),
2084
+ style: { ...pillBase, ...cycle === "monthly" ? active : inactive },
2085
+ children: labels.planComparison.monthly
2086
+ }
2087
+ ),
2088
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2089
+ "button",
2090
+ {
2091
+ type: "button",
2092
+ "aria-pressed": cycle === "yearly",
2093
+ onClick: () => onChange?.("yearly"),
2094
+ style: { ...pillBase, ...cycle === "yearly" ? active : inactive },
2095
+ children: [
2096
+ labels.planComparison.yearly,
2097
+ discountPercent > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2098
+ "span",
2099
+ {
2100
+ style: {
2101
+ marginLeft: VisualTokens.spacing.xs,
2102
+ fontSize: VisualTokens.fontSize.caption,
2103
+ background: VisualTokens.color.brandPrimary,
2104
+ color: VisualTokens.color.textOnPrimary,
2105
+ padding: `${2}px ${VisualTokens.spacing.md}px`,
2106
+ borderRadius: VisualTokens.radius.pill
2107
+ },
2108
+ children: formatLabel(labels.planComparison.save, { percent: discountPercent })
2109
+ }
2110
+ )
2111
+ ]
2112
+ }
2113
+ )
2114
+ ]
2115
+ }
2116
+ );
2117
+ };
2118
+ var PlanCard = ({
2119
+ plan,
2120
+ isSelected,
2121
+ isPopular,
2122
+ cycle,
2123
+ showFeatures,
2124
+ loading,
2125
+ onSelect,
2126
+ tabbable
2127
+ }) => {
2128
+ const labels = useBillingLabels();
2129
+ const price = getPlanPrice(plan, cycle);
2130
+ const discount = calculateDiscount(plan.monthlyPrice, plan.yearlyPrice);
2131
+ const cardStyle = {
2132
+ position: "relative",
2133
+ background: VisualTokens.color.surfacePrimary,
2134
+ border: `2px solid ${isSelected ? VisualTokens.color.brandPrimary : isPopular ? VisualTokens.color.brandPrimary : VisualTokens.color.borderSubtle}`,
2135
+ opacity: isPopular && !isSelected ? 0.85 : 1,
2136
+ borderRadius: VisualTokens.radius.lg,
2137
+ padding: VisualTokens.spacing.section,
2138
+ cursor: loading ? "wait" : "pointer",
2139
+ boxSizing: "border-box",
2140
+ display: "flex",
2141
+ flexDirection: "column"
2142
+ };
2143
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2144
+ "div",
2145
+ {
2146
+ role: "radio",
2147
+ "aria-checked": isSelected,
2148
+ tabIndex: tabbable ? 0 : -1,
2149
+ onClick: () => !loading && onSelect?.(plan.id),
2150
+ onKeyDown: (e) => {
2151
+ if (e.key === "Enter" || e.key === " ") {
2152
+ e.preventDefault();
2153
+ if (!loading) onSelect?.(plan.id);
2154
+ }
2155
+ },
2156
+ style: cardStyle,
2157
+ children: [
2158
+ isPopular && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2159
+ "div",
2160
+ {
2161
+ style: {
2162
+ position: "absolute",
2163
+ top: -12,
2164
+ left: "50%",
2165
+ transform: "translateX(-50%)"
2166
+ },
2167
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2168
+ "span",
2169
+ {
2170
+ style: {
2171
+ background: VisualTokens.color.brandPrimary,
2172
+ color: VisualTokens.color.textOnPrimary,
2173
+ fontSize: VisualTokens.fontSize.caption,
2174
+ fontWeight: VisualTokens.fontWeight.strong,
2175
+ padding: `${VisualTokens.spacing.xs}px ${VisualTokens.spacing.xl}px`,
2176
+ borderRadius: VisualTokens.radius.pill,
2177
+ whiteSpace: "nowrap"
2178
+ },
2179
+ children: labels.planComparison.mostPopular
2180
+ }
2181
+ )
2182
+ }
2183
+ ),
2184
+ plan.badge && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { marginBottom: VisualTokens.spacing.xl }, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2185
+ "span",
2186
+ {
2187
+ style: {
2188
+ fontSize: VisualTokens.fontSize.caption,
2189
+ fontWeight: VisualTokens.fontWeight.strong,
2190
+ textTransform: "uppercase",
2191
+ letterSpacing: "0.05em",
2192
+ padding: `${VisualTokens.spacing.xs}px ${VisualTokens.spacing.lg}px`,
2193
+ borderRadius: VisualTokens.radius.pill,
2194
+ background: VisualTokens.color.surfaceSubtle,
2195
+ color: VisualTokens.color.brandPrimary
2196
+ },
2197
+ children: plan.badge
2198
+ }
2199
+ ) }),
2200
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2201
+ "h3",
2202
+ {
2203
+ style: {
2204
+ fontSize: VisualTokens.fontSize.title,
2205
+ fontWeight: VisualTokens.fontWeight.strong,
2206
+ color: VisualTokens.color.textPrimary,
2207
+ margin: 0,
2208
+ marginBottom: VisualTokens.spacing.sm
2209
+ },
2210
+ children: plan.name
2211
+ }
2212
+ ),
2213
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2214
+ "p",
2215
+ {
2216
+ style: {
2217
+ fontSize: VisualTokens.fontSize.body,
2218
+ color: VisualTokens.color.textMuted,
2219
+ margin: 0,
2220
+ marginBottom: VisualTokens.spacing.section
2221
+ },
2222
+ children: plan.description
2223
+ }
2224
+ ),
2225
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { marginBottom: VisualTokens.spacing.section }, children: [
2226
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { display: "flex", alignItems: "baseline", gap: VisualTokens.spacing.xs }, children: [
2227
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2228
+ "span",
2229
+ {
2230
+ style: {
2231
+ fontSize: 32,
2232
+ fontWeight: 800,
2233
+ color: VisualTokens.color.textPrimary
2234
+ },
2235
+ children: formatPrice(price, plan.currency)
2236
+ }
2237
+ ),
2238
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { style: { color: VisualTokens.color.textMuted, fontSize: VisualTokens.fontSize.body }, children: [
2239
+ "/",
2240
+ cycle === "monthly" ? labels.planComparison.monthlyShort : labels.planComparison.yearlyShort
2241
+ ] })
2242
+ ] }),
2243
+ cycle === "yearly" && discount > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2244
+ "p",
2245
+ {
2246
+ style: {
2247
+ fontSize: VisualTokens.fontSize.body,
2248
+ color: VisualTokens.color.badgeActiveFg,
2249
+ margin: 0,
2250
+ marginTop: VisualTokens.spacing.xs
2251
+ },
2252
+ children: formatLabel(labels.planComparison.saveWithYearly, { percent: discount })
2253
+ }
2254
+ )
2255
+ ] }),
2256
+ showFeatures && plan.features.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2257
+ "ul",
2258
+ {
2259
+ style: {
2260
+ listStyle: "none",
2261
+ margin: 0,
2262
+ padding: 0,
2263
+ display: "flex",
2264
+ flexDirection: "column",
2265
+ gap: VisualTokens.spacing.lg,
2266
+ marginBottom: VisualTokens.spacing.section,
2267
+ flex: 1
2268
+ },
2269
+ children: plan.features.map((feature, index) => {
2270
+ const { text, bold, included } = formatFeature(feature);
2271
+ const isExcluded = included === false;
2272
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2273
+ "li",
2274
+ {
2275
+ style: {
2276
+ display: "flex",
2277
+ alignItems: "flex-start",
2278
+ gap: VisualTokens.spacing.lg,
2279
+ fontSize: VisualTokens.fontSize.body,
2280
+ opacity: isExcluded ? VisualTokens.opacity.disabled : 1,
2281
+ textDecoration: isExcluded ? "line-through" : "none"
2282
+ },
2283
+ children: [
2284
+ isExcluded ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2285
+ import_lucide_react6.X,
2286
+ {
2287
+ size: 20,
2288
+ style: { flexShrink: 0, marginTop: 2 },
2289
+ color: VisualTokens.color.textMuted,
2290
+ "aria-hidden": "true"
2291
+ }
2292
+ ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2293
+ import_lucide_react6.Check,
2294
+ {
2295
+ size: 20,
2296
+ style: { flexShrink: 0, marginTop: 2 },
2297
+ color: VisualTokens.color.brandPrimary,
2298
+ "aria-hidden": "true"
2299
+ }
2300
+ ),
2301
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2302
+ "span",
2303
+ {
2304
+ style: {
2305
+ fontWeight: bold ? VisualTokens.fontWeight.strong : VisualTokens.fontWeight.regular,
2306
+ color: VisualTokens.color.textPrimary
2307
+ },
2308
+ children: text
2309
+ }
2310
+ )
2311
+ ]
2312
+ },
2313
+ `${plan.id}-feature-${index}`
2314
+ );
2315
+ })
2316
+ }
2317
+ ),
2318
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2319
+ PortalButton,
2320
+ {
2321
+ type: "button",
2322
+ pill: true,
2323
+ fullWidth: true,
2324
+ onClick: (e) => {
2325
+ e.stopPropagation();
2326
+ onSelect?.(plan.id);
2327
+ },
2328
+ disabled: loading,
2329
+ "aria-label": isSelected ? labels.planComparison.currentPlan : `${labels.planComparison.selectPlan} \u2014 ${plan.name}`,
2330
+ variant: isSelected ? "default" : "ghost",
2331
+ style: isSelected ? void 0 : {
2332
+ background: VisualTokens.color.surfaceSubtle,
2333
+ color: VisualTokens.color.textPrimary,
2334
+ padding: `${VisualTokens.spacing.lg}px 0`
2335
+ },
2336
+ children: loading ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
2337
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PortalSpinner, { size: 16 }),
2338
+ labels.planComparison.processing
2339
+ ] }) : isSelected ? labels.planComparison.currentPlan : labels.planComparison.selectPlan
2340
+ }
2341
+ )
2342
+ ]
2343
+ }
2344
+ );
2345
+ };
2346
+ var pickLargestDiscount = (plans) => {
2347
+ if (plans.length === 0) return 0;
2348
+ return plans.reduce((max, plan) => {
2349
+ const d = calculateDiscount(plan.monthlyPrice, plan.yearlyPrice);
2350
+ return d > max ? d : max;
2351
+ }, 0);
2352
+ };
2353
+
2354
+ // src/presentation/portal/hooks/useBilling.ts
2355
+ var import_react4 = require("react");
2356
+ var toErrorMessage = (err, fallback) => {
2357
+ if (err instanceof Error && err.message) return err.message;
2358
+ return fallback;
2359
+ };
2360
+ var executeBillingAction = async (setIsLoading, setError, fallbackMessage, action) => {
2361
+ setIsLoading(true);
2362
+ setError(null);
2363
+ try {
2364
+ return await action();
2365
+ } catch (err) {
2366
+ setError(toErrorMessage(err, fallbackMessage));
2367
+ return null;
2368
+ } finally {
2369
+ setIsLoading(false);
2370
+ }
2371
+ };
2372
+ function useBilling(options) {
2373
+ const { initialData, apiClient } = options;
2374
+ const [billing, setBilling] = (0, import_react4.useState)(initialData ?? null);
2375
+ const [isLoading, setIsLoading] = (0, import_react4.useState)(false);
2376
+ const [error, setError] = (0, import_react4.useState)(null);
2377
+ const loadBilling = (0, import_react4.useCallback)(async () => {
2378
+ const result = await executeBillingAction(
2379
+ setIsLoading,
2380
+ setError,
2381
+ "Failed to load billing",
2382
+ () => apiClient.loadBilling()
2383
+ );
2384
+ if (result) setBilling(result);
2385
+ }, [apiClient]);
2386
+ const updatePlan = (0, import_react4.useCallback)(
2387
+ async (planId) => {
2388
+ await executeBillingAction(
2389
+ setIsLoading,
2390
+ setError,
2391
+ "Failed to update plan",
2392
+ () => apiClient.updatePlan(planId).then(() => {
2393
+ setBilling(
2394
+ (prev) => prev ? { ...prev, subscription: { ...prev.subscription, planId } } : prev
2395
+ );
2396
+ })
2397
+ );
2398
+ },
2399
+ [apiClient]
2400
+ );
2401
+ const cancelSubscription = (0, import_react4.useCallback)(async () => {
2402
+ await executeBillingAction(
2403
+ setIsLoading,
2404
+ setError,
2405
+ "Failed to cancel subscription",
2406
+ () => apiClient.cancelSubscription().then(() => {
2407
+ setBilling(
2408
+ (prev) => prev ? {
2409
+ ...prev,
2410
+ subscription: {
2411
+ ...prev.subscription,
2412
+ status: "canceled",
2413
+ cancelAtPeriodEnd: true
2414
+ }
2415
+ } : prev
2416
+ );
2417
+ })
2418
+ );
2419
+ }, [apiClient]);
2420
+ const updateCycle = (0, import_react4.useCallback)(
2421
+ async (cycle) => {
2422
+ await executeBillingAction(
2423
+ setIsLoading,
2424
+ setError,
2425
+ "Failed to update cycle",
2426
+ () => apiClient.updateCycle(cycle).then(() => {
2427
+ setBilling(
2428
+ (prev) => prev ? { ...prev, subscription: { ...prev.subscription, cycle } } : prev
2429
+ );
2430
+ })
2431
+ );
2432
+ },
2433
+ [apiClient]
2434
+ );
2435
+ const addPaymentMethod = (0, import_react4.useCallback)(
2436
+ async (input) => {
2437
+ return executeBillingAction(
2438
+ setIsLoading,
2439
+ setError,
2440
+ "Failed to add payment method",
2441
+ () => apiClient.addPaymentMethod(input).then((method) => {
2442
+ setBilling(
2443
+ (prev) => prev ? { ...prev, paymentMethods: [...prev.paymentMethods, method] } : prev
2444
+ );
2445
+ return method;
2446
+ })
2447
+ );
2448
+ },
2449
+ [apiClient]
2450
+ );
2451
+ const removePaymentMethod = (0, import_react4.useCallback)(
2452
+ async (methodId) => {
2453
+ await executeBillingAction(
2454
+ setIsLoading,
2455
+ setError,
2456
+ "Failed to remove payment method",
2457
+ () => apiClient.removePaymentMethod(methodId).then(() => {
2458
+ setBilling(
2459
+ (prev) => prev ? {
2460
+ ...prev,
2461
+ paymentMethods: prev.paymentMethods.filter((pm) => pm.id !== methodId)
2462
+ } : prev
2463
+ );
2464
+ })
2465
+ );
2466
+ },
2467
+ [apiClient]
2468
+ );
2469
+ const setDefaultPaymentMethod = (0, import_react4.useCallback)(
2470
+ async (methodId) => {
2471
+ await executeBillingAction(
2472
+ setIsLoading,
2473
+ setError,
2474
+ "Failed to set default payment method",
2475
+ () => apiClient.setDefaultPaymentMethod(methodId).then(() => {
2476
+ setBilling((prev) => {
2477
+ if (!prev) return prev;
2478
+ const methods = prev.paymentMethods.map((pm) => ({
2479
+ ...pm,
2480
+ isDefault: pm.id === methodId
2481
+ }));
2482
+ const defaultPaymentMethod = methods.find((pm) => pm.id === methodId);
2483
+ return { ...prev, paymentMethods: methods, defaultPaymentMethod };
2484
+ });
2485
+ })
2486
+ );
2487
+ },
2488
+ [apiClient]
2489
+ );
2490
+ const getInvoiceUrl = (0, import_react4.useCallback)(
2491
+ async (invoiceId) => {
2492
+ const result = await executeBillingAction(
2493
+ setIsLoading,
2494
+ setError,
2495
+ "Failed to get invoice URL",
2496
+ () => apiClient.getInvoiceUrl(invoiceId)
2497
+ );
2498
+ return result;
2499
+ },
2500
+ [apiClient]
2501
+ );
2502
+ return (0, import_react4.useMemo)(
2503
+ () => ({
2504
+ billing,
2505
+ isLoading,
2506
+ error,
2507
+ loadBilling,
2508
+ updatePlan,
2509
+ cancelSubscription,
2510
+ updateCycle,
2511
+ addPaymentMethod,
2512
+ removePaymentMethod,
2513
+ setDefaultPaymentMethod,
2514
+ getInvoiceUrl
2515
+ }),
2516
+ [
2517
+ billing,
2518
+ isLoading,
2519
+ error,
2520
+ loadBilling,
2521
+ updatePlan,
2522
+ cancelSubscription,
2523
+ updateCycle,
2524
+ addPaymentMethod,
2525
+ removePaymentMethod,
2526
+ setDefaultPaymentMethod,
2527
+ getInvoiceUrl
2528
+ ]
2529
+ );
2530
+ }
2531
+ var createStubBillingApiClient = (overrides) => {
2532
+ const notConfigured = {
2533
+ loadBilling: () => Promise.reject(new Error("BillingApiClient not configured")),
2534
+ updatePlan: () => Promise.reject(new Error("BillingApiClient not configured")),
2535
+ cancelSubscription: () => Promise.reject(new Error("BillingApiClient not configured")),
2536
+ updateCycle: () => Promise.reject(new Error("BillingApiClient not configured")),
2537
+ addPaymentMethod: () => Promise.reject(new Error("BillingApiClient not configured")),
2538
+ removePaymentMethod: () => Promise.reject(new Error("BillingApiClient not configured")),
2539
+ setDefaultPaymentMethod: () => Promise.reject(new Error("BillingApiClient not configured")),
2540
+ getInvoiceUrl: () => Promise.reject(new Error("BillingApiClient not configured"))
2541
+ };
2542
+ return { ...notConfigured, ...overrides };
2543
+ };
2544
+
2545
+ // src/presentation/portal/utils/plans.ts
2546
+ var MONTHS_IN_YEAR = 12;
2547
+ var KNOWN_CURRENCIES = new Set(Object.values(CURRENCY));
2548
+ var productFeatures = (product) => product.benefits.map((benefit) => benefit.description ?? "").filter((description) => description.length > 0);
2549
+ var toCurrency = (raw) => {
2550
+ const upper = raw.toUpperCase();
2551
+ if (KNOWN_CURRENCIES.has(upper)) return upper;
2552
+ throw new Error(`Unsupported currency: ${raw}`);
2553
+ };
2554
+ var toMonthlyAmount = (amount, interval) => {
2555
+ if (interval === "monthly") return amount;
2556
+ if (interval === "yearly") return amount / MONTHS_IN_YEAR;
2557
+ throw new Error(
2558
+ `Cannot convert price to monthly: unsupported interval "${interval ?? "undefined"}"`
2559
+ );
2560
+ };
2561
+ function transformProductToPlan(product, price) {
2562
+ const interval = price.recurringInterval;
2563
+ const monthlyAmount = toMonthlyAmount(price.amount ?? 0, interval);
2564
+ const yearlyAmount = interval === "yearly" ? price.amount ?? 0 : (price.amount ?? 0) * MONTHS_IN_YEAR;
2565
+ return {
2566
+ id: product.id,
2567
+ name: product.name,
2568
+ description: product.description ?? "",
2569
+ monthlyPrice: Math.round(monthlyAmount),
2570
+ yearlyPrice: Math.round(yearlyAmount),
2571
+ currency: toCurrency(price.currency),
2572
+ features: productFeatures(product)
2573
+ };
2574
+ }
2575
+ function transformProducts(products) {
2576
+ const tiers = [];
2577
+ for (const product of products) {
2578
+ if (product.isArchived) continue;
2579
+ const recurring = product.prices.filter(
2580
+ (price) => !price.isArchived && price.recurringInterval !== void 0
2581
+ );
2582
+ const monthly = recurring.find((price) => price.recurringInterval === "monthly");
2583
+ const yearly = recurring.find((price) => price.recurringInterval === "yearly");
2584
+ if (monthly && yearly) {
2585
+ tiers.push({
2586
+ id: product.id,
2587
+ name: product.name,
2588
+ description: product.description ?? "",
2589
+ monthlyPrice: Math.round(monthly.amount ?? 0),
2590
+ yearlyPrice: Math.round(yearly.amount ?? 0),
2591
+ currency: toCurrency(monthly.currency),
2592
+ features: productFeatures(product)
2593
+ });
2594
+ } else if (monthly) {
2595
+ tiers.push(transformProductToPlan(product, monthly));
2596
+ } else if (yearly) {
2597
+ tiers.push(transformProductToPlan(product, yearly));
2598
+ }
2599
+ }
2600
+ return tiers;
2601
+ }
2602
+
2603
+ // src/domain/enums/billing-cycle.enum.ts
2604
+ var BILLING_CYCLE = Object.freeze({
2605
+ MONTHLY: "monthly",
2606
+ YEARLY: "yearly"
2607
+ });
2608
+ var BILLING_CYCLE_VALUES = Object.freeze([
2609
+ BILLING_CYCLE.MONTHLY,
2610
+ BILLING_CYCLE.YEARLY
2611
+ ]);
2612
+ var CANONICAL_BILLING_CYCLES = new Set(BILLING_CYCLE_VALUES);
2613
+ var BILLING_CYCLE_ALIASES = /* @__PURE__ */ new Map([
2614
+ [BILLING_CYCLE.MONTHLY, BILLING_CYCLE.MONTHLY],
2615
+ ["month", BILLING_CYCLE.MONTHLY],
2616
+ [BILLING_CYCLE.YEARLY, BILLING_CYCLE.YEARLY],
2617
+ ["year", BILLING_CYCLE.YEARLY]
2618
+ ]);
2619
+
2620
+ // src/presentation/navigation/navigation.strategy.ts
2621
+ var WindowNavigationStrategy = class {
2622
+ redirect(url) {
2623
+ window.location.href = url;
2624
+ }
2625
+ };
2626
+ var NoopNavigationStrategy = class {
2627
+ redirect(_url) {
2628
+ }
2629
+ };
2630
+
2631
+ // src/presentation/portal/create-portal-client.ts
2632
+ function portalStatus(core) {
2633
+ switch (core) {
2634
+ case "active":
2635
+ return SUBSCRIPTION_STATUS.ACTIVE;
2636
+ case "trialing":
2637
+ return SUBSCRIPTION_STATUS.TRIALING;
2638
+ case "past_due":
2639
+ return SUBSCRIPTION_STATUS.PAST_DUE;
2640
+ case "unpaid":
2641
+ return SUBSCRIPTION_STATUS.UNPAID;
2642
+ case "incomplete":
2643
+ case "incomplete_expired":
2644
+ return SUBSCRIPTION_STATUS.INCOMPLETE;
2645
+ case "revoked":
2646
+ return SUBSCRIPTION_STATUS.REVOKED;
2647
+ default:
2648
+ return SUBSCRIPTION_STATUS.CANCELED;
2649
+ }
2650
+ }
2651
+ function freeTier(planKey) {
2652
+ return {
2653
+ id: planKey,
2654
+ name: planKey === "free" ? "Free" : planKey,
2655
+ description: "",
2656
+ monthlyPrice: 0,
2657
+ yearlyPrice: 0,
2658
+ currency: CURRENCY.USD,
2659
+ features: []
2660
+ };
2661
+ }
2662
+ function orderToInvoice(order) {
2663
+ const paid = order.paid || order.status === "paid";
2664
+ const date = order.createdAt.length >= 10 ? order.createdAt.slice(0, 10) : order.createdAt;
2665
+ return {
2666
+ id: order.id,
2667
+ number: order.id.slice(-8).toUpperCase(),
2668
+ amount: order.amount,
2669
+ currency: order.currency.toUpperCase(),
2670
+ status: paid ? INVOICE_STATUS.PAID : INVOICE_STATUS.OPEN,
2671
+ date,
2672
+ dueDate: date,
2673
+ ...paid ? { paidAt: date } : {},
2674
+ ...order.invoiceUrl ? { invoiceUrl: order.invoiceUrl } : {},
2675
+ items: [
2676
+ {
2677
+ description: order.productName ?? order.itemType ?? "Subscription",
2678
+ quantity: 1,
2679
+ unitPrice: order.amount,
2680
+ amount: order.amount
2681
+ }
2682
+ ]
2683
+ };
2684
+ }
2685
+ var HOSTED_PORTAL_METHOD = {
2686
+ id: "hosted-portal",
2687
+ type: "card",
2688
+ isDefault: true,
2689
+ createdAt: (/* @__PURE__ */ new Date(0)).toISOString(),
2690
+ card: { last4: "\u2022\u2022\u2022\u2022", brand: "Managed in the billing portal", expiryMonth: 0, expiryYear: 0 }
2691
+ };
2692
+ function createPortalClient(options) {
2693
+ const { adapter, products, userId, successUrl } = options;
2694
+ const defaultCycle = options.defaultCycle ?? "monthly";
2695
+ const navigation = options.navigation ?? new WindowNavigationStrategy();
2696
+ const tiers = transformProducts(products);
2697
+ const productByPlanKey = /* @__PURE__ */ new Map();
2698
+ for (const product of products) {
2699
+ const key = product.metadata?.planId;
2700
+ if (typeof key === "string" && key.length > 0) productByPlanKey.set(key, product);
2701
+ }
2702
+ async function checkout(productId, cycle) {
2703
+ const { url } = await adapter.createCheckout({
2704
+ productId,
2705
+ billingCycle: cycle ?? defaultCycle,
2706
+ ...successUrl ? { successUrl } : {},
2707
+ ...userId ? { userId } : {}
2708
+ });
2709
+ navigation.redirect(url);
2710
+ }
2711
+ async function openPortal() {
2712
+ const url = await adapter.getPortalUrl(userId ?? "");
2713
+ navigation.redirect(url);
2714
+ return url;
2715
+ }
2716
+ return {
2717
+ async loadBilling() {
2718
+ const [status, history] = await Promise.all([
2719
+ adapter.getStatus(userId ?? ""),
2720
+ adapter.getBillingHistory(userId ?? "")
2721
+ ]);
2722
+ const tierById = tiers.find((tier2) => tier2.id === status.plan);
2723
+ const product = productByPlanKey.get(status.plan);
2724
+ const tier = tierById ?? (product ? tiers.find((t) => t.id === product.id) : void 0) ?? freeTier(status.plan);
2725
+ const subscription = {
2726
+ id: status.subscriptionId ?? "none",
2727
+ planId: status.plan,
2728
+ plan: tier,
2729
+ status: portalStatus(status.subscriptionStatus),
2730
+ cycle: status.billingCycle ?? defaultCycle,
2731
+ currentPeriodStart: "",
2732
+ currentPeriodEnd: status.currentPeriodEnd ?? "",
2733
+ ...status.cancelAtPeriodEnd !== void 0 ? { cancelAtPeriodEnd: status.cancelAtPeriodEnd } : {}
2734
+ };
2735
+ const price = subscription.cycle === "yearly" ? tier.yearlyPrice : tier.monthlyPrice;
2736
+ return {
2737
+ subscription,
2738
+ paymentMethods: [],
2739
+ usage: [],
2740
+ recentInvoices: history.map(orderToInvoice),
2741
+ ...subscription.status === SUBSCRIPTION_STATUS.ACTIVE && subscription.currentPeriodEnd ? {
2742
+ upcomingInvoice: {
2743
+ amount: price,
2744
+ currency: tier.currency,
2745
+ date: subscription.currentPeriodEnd
2746
+ }
2747
+ } : {}
2748
+ };
2749
+ },
2750
+ updatePlan: (productId) => checkout(productId),
2751
+ selectPlan: (productId, cycle) => checkout(productId, cycle),
2752
+ updateCycle: async (cycle) => {
2753
+ const status = await adapter.getStatus(userId ?? "");
2754
+ const product = productByPlanKey.get(status.plan) ?? tiers.find((t) => t.id === status.plan);
2755
+ if (!product) throw new Error("Cannot switch cycle without a purchasable plan catalog");
2756
+ await checkout(product.id, cycle);
2757
+ },
2758
+ cancelSubscription: async () => {
2759
+ await adapter.cancelSubscription();
2760
+ },
2761
+ addPaymentMethod: async () => {
2762
+ await openPortal();
2763
+ return HOSTED_PORTAL_METHOD;
2764
+ },
2765
+ removePaymentMethod: () => openPortal().then(() => void 0),
2766
+ setDefaultPaymentMethod: () => openPortal().then(() => void 0),
2767
+ getInvoiceUrl: async (invoiceId) => {
2768
+ const history = await adapter.getBillingHistory(userId ?? "");
2769
+ const order = history.find((item) => item.id === invoiceId);
2770
+ return order?.invoiceUrl ?? await openPortal();
2771
+ },
2772
+ openPortal
2773
+ };
2774
+ }
2775
+
2776
+ // src/presentation/portal/TurnkeyBillingPage.tsx
2777
+ var import_react5 = require("react");
2778
+ var import_jsx_runtime13 = require("react/jsx-runtime");
2779
+ function browserNavigation() {
2780
+ if (typeof window === "undefined") return new NoopNavigationStrategy();
2781
+ return {
2782
+ redirect(url) {
2783
+ window.location.href = url;
2784
+ }
2785
+ };
2786
+ }
2787
+ function TurnkeyBillingPage({
2788
+ adapter,
2789
+ products,
2790
+ config,
2791
+ userId,
2792
+ defaultCycle,
2793
+ successUrl,
2794
+ navigation,
2795
+ locale,
2796
+ labels,
2797
+ onError
2798
+ }) {
2799
+ const [cycle, setCycle] = (0, import_react5.useState)(defaultCycle ?? "monthly");
2800
+ const [selecting, setSelecting] = (0, import_react5.useState)(false);
2801
+ const client = (0, import_react5.useMemo)(
2802
+ () => createPortalClient({
2803
+ adapter,
2804
+ products,
2805
+ ...userId !== void 0 ? { userId } : {},
2806
+ ...defaultCycle !== void 0 ? { defaultCycle } : {},
2807
+ ...successUrl !== void 0 ? { successUrl } : {},
2808
+ navigation: navigation ?? browserNavigation()
2809
+ }),
2810
+ [adapter, products, userId, defaultCycle, successUrl, navigation]
2811
+ );
2812
+ const { billing, isLoading, error, loadBilling, cancelSubscription } = useBilling({ apiClient: client });
2813
+ (0, import_react5.useEffect)(() => {
2814
+ void loadBilling();
2815
+ }, [loadBilling]);
2816
+ const plans = config.plans ?? transformProducts(products);
2817
+ const selectPlan = async (productId) => {
2818
+ setSelecting(true);
2819
+ try {
2820
+ await client.selectPlan(productId, cycle);
2821
+ } catch (err) {
2822
+ onError?.(err instanceof Error ? err : new Error(String(err)));
2823
+ } finally {
2824
+ setSelecting(false);
2825
+ }
2826
+ };
2827
+ const openPortalSafely = async () => {
2828
+ try {
2829
+ await client.openPortal();
2830
+ } catch (err) {
2831
+ onError?.(err instanceof Error ? err : new Error(String(err)));
2832
+ }
2833
+ };
2834
+ const viewInvoice = async (invoiceId) => {
2835
+ try {
2836
+ await client.getInvoiceUrl(invoiceId);
2837
+ } catch (err) {
2838
+ onError?.(err instanceof Error ? err : new Error(String(err)));
2839
+ }
2840
+ };
2841
+ const mergedConfig = { ...config, plans };
2842
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 24, width: "100%" }, children: [
2843
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2844
+ PlanComparison,
2845
+ {
2846
+ plans,
2847
+ selectedPlan: billing?.subscription.plan.id,
2848
+ cycle,
2849
+ onPlanSelect: (productId) => void selectPlan(productId),
2850
+ onCycleChange: setCycle,
2851
+ loading: selecting,
2852
+ labels
2853
+ }
2854
+ ),
2855
+ billing ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2856
+ BillingPage,
2857
+ {
2858
+ config: mergedConfig,
2859
+ billing,
2860
+ loading: isLoading,
2861
+ error: error ?? void 0,
2862
+ locale,
2863
+ labels,
2864
+ onAddPaymentMethod: () => void openPortalSafely(),
2865
+ onSetDefaultPaymentMethod: () => void openPortalSafely(),
2866
+ onRemovePaymentMethod: () => void openPortalSafely(),
2867
+ onViewInvoice: (invoice) => void viewInvoice(invoice.id),
2868
+ onCancelSubscription: () => cancelSubscription().then(() => loadBilling())
2869
+ }
2870
+ ) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 12, padding: 32 }, children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PortalSpinner, {}) : error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
2871
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: error }),
2872
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PortalButton, { onClick: () => void loadBilling(), children: "Retry" })
2873
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PortalSpinner, {}) })
2874
+ ] });
2875
+ }
2876
+ // Annotate the CommonJS export names for ESM import in node:
2877
+ 0 && (module.exports = {
2878
+ BILLING_CYCLE,
2879
+ BillingLabelsProvider,
2880
+ BillingLayout,
2881
+ BillingPage,
2882
+ BillingPortal,
2883
+ CURRENCY,
2884
+ DEFAULT_BILLING_CONFIG,
2885
+ DEFAULT_BILLING_LABELS,
2886
+ INVOICE_STATUS,
2887
+ InvoiceCard,
2888
+ PLAN_TYPE,
2889
+ PaymentMethodsList,
2890
+ PlanComparison,
2891
+ PortalButton,
2892
+ PortalSpinner,
2893
+ RADIO_GROUP_NAME,
2894
+ SUBSCRIPTION_STATUS,
2895
+ TurnkeyBillingPage,
2896
+ UsageCard,
2897
+ calculateDiscount,
2898
+ calculateInvoiceTotal,
2899
+ calculateProratedAmount,
2900
+ calculateUsagePercentage,
2901
+ createPortalClient,
2902
+ createStubBillingApiClient,
2903
+ formatCardNumber,
2904
+ formatExpiry,
2905
+ formatFeature,
2906
+ formatLabel,
2907
+ formatNumber,
2908
+ formatPrice,
2909
+ getDaysRemaining,
2910
+ getInvoiceStatusLabel,
2911
+ getInvoiceStatusTone,
2912
+ getNextBillingDate,
2913
+ getPlanPrice,
2914
+ getStatusLabel,
2915
+ getStatusTone,
2916
+ getTrialDaysText,
2917
+ groupInvoicesByStatus,
2918
+ isNearLimit,
2919
+ isPopularPlan,
2920
+ isTrialExpiringSoon,
2921
+ mergeLabels,
2922
+ sortInvoicesByDate,
2923
+ transformProductToPlan,
2924
+ transformProducts,
2925
+ useBilling,
2926
+ useBillingLabels
2927
+ });