@umituz/web-payment 0.0.0-stage → 1.0.1

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