@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
package/dist/react.js ADDED
@@ -0,0 +1,1874 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react.tsx
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ BILLING_CYCLE: () => BILLING_CYCLE,
24
+ BILLING_CYCLE_VALUES: () => BILLING_CYCLE_VALUES,
25
+ BillingContext: () => BillingContext,
26
+ BillingHistoryList: () => BillingHistoryList,
27
+ BillingProvider: () => BillingProvider,
28
+ CANCELLATION_REASON: () => CANCELLATION_REASON,
29
+ CANCELLATION_REASON_CATALOG: () => CANCELLATION_REASON_CATALOG,
30
+ CANCELLATION_REASON_VALUES: () => CANCELLATION_REASON_VALUES,
31
+ CancelSubscriptionButton: () => CancelSubscriptionButton,
32
+ CheckoutButton: () => CheckoutButton,
33
+ CustomerPortalLink: () => CustomerPortalLink,
34
+ FREE_SUBSCRIPTION_STATUS: () => FREE_SUBSCRIPTION_STATUS,
35
+ NoopNavigationStrategy: () => NoopNavigationStrategy,
36
+ PAYMENT_PROVIDER: () => PAYMENT_PROVIDER,
37
+ PAYMENT_PROVIDER_VALUES: () => PAYMENT_PROVIDER_VALUES,
38
+ POLAR_AMOUNT_TYPE: () => POLAR_AMOUNT_TYPE,
39
+ PricingTable: () => PricingTable,
40
+ SUBSCRIPTION_STATUS: () => SUBSCRIPTION_STATUS,
41
+ SUBSCRIPTION_STATUS_VALUES: () => SUBSCRIPTION_STATUS_VALUES,
42
+ SubscriptionStatusBadge: () => SubscriptionStatusBadge,
43
+ TONE_PAIRS: () => TONE_PAIRS,
44
+ VisualTokens: () => VisualTokens,
45
+ WindowNavigationStrategy: () => WindowNavigationStrategy,
46
+ applyPercentDiscount: () => applyPercentDiscount,
47
+ buildCheckoutMetadata: () => buildCheckoutMetadata,
48
+ createMemoryBillingAdapter: () => createMemoryBillingAdapter,
49
+ formatReceiptNumber: () => formatReceiptNumber,
50
+ isActiveSubscription: () => isActiveSubscription,
51
+ isKnownBillingCycle: () => isKnownBillingCycle,
52
+ isKnownPaymentProvider: () => isKnownPaymentProvider,
53
+ isKnownSubscriptionStatus: () => isKnownSubscriptionStatus,
54
+ mapProduct: () => mapProduct,
55
+ mapProductList: () => mapProductList,
56
+ minorUnitDivisor: () => minorUnitDivisor,
57
+ normalizeCouponCode: () => normalizeCouponCode,
58
+ parseBillingCycle: () => parseBillingCycle,
59
+ readCheckoutMetadata: () => readCheckoutMetadata,
60
+ toMajorUnits: () => toMajorUnits,
61
+ toMinorUnits: () => toMinorUnits,
62
+ useBilling: () => useBilling,
63
+ useCancelSubscription: () => useCancelSubscription,
64
+ useCheckoutButton: () => useCheckoutButton,
65
+ useProducts: () => useProducts
66
+ });
67
+ module.exports = __toCommonJS(react_exports);
68
+
69
+ // src/domain/enums/payment-provider.enum.ts
70
+ var PAYMENT_PROVIDER = {
71
+ POLAR: "polar",
72
+ STRIPE: "stripe"
73
+ };
74
+ var PAYMENT_PROVIDER_VALUES = Object.values(PAYMENT_PROVIDER);
75
+ var PAYMENT_PROVIDER_SET = new Set(PAYMENT_PROVIDER_VALUES);
76
+ function isKnownPaymentProvider(value) {
77
+ return typeof value === "string" && PAYMENT_PROVIDER_SET.has(value);
78
+ }
79
+
80
+ // src/domain/enums/subscription-status.enum.ts
81
+ var SUBSCRIPTION_STATUS = {
82
+ ACTIVE: "active",
83
+ CANCELED: "canceled",
84
+ REVOKED: "revoked",
85
+ TRIALING: "trialing",
86
+ PAST_DUE: "past_due",
87
+ INCOMPLETE: "incomplete",
88
+ INCOMPLETE_EXPIRED: "incomplete_expired",
89
+ UNPAID: "unpaid",
90
+ NONE: "none"
91
+ };
92
+ var SUBSCRIPTION_STATUS_VALUES = Object.freeze([
93
+ SUBSCRIPTION_STATUS.ACTIVE,
94
+ SUBSCRIPTION_STATUS.CANCELED,
95
+ SUBSCRIPTION_STATUS.REVOKED,
96
+ SUBSCRIPTION_STATUS.TRIALING,
97
+ SUBSCRIPTION_STATUS.PAST_DUE,
98
+ SUBSCRIPTION_STATUS.INCOMPLETE,
99
+ SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED,
100
+ SUBSCRIPTION_STATUS.UNPAID,
101
+ SUBSCRIPTION_STATUS.NONE
102
+ ]);
103
+ var SUBSCRIPTION_STATUS_SET = new Set(SUBSCRIPTION_STATUS_VALUES);
104
+ function isKnownSubscriptionStatus(value) {
105
+ return typeof value === "string" && SUBSCRIPTION_STATUS_SET.has(value.toLowerCase());
106
+ }
107
+
108
+ // src/domain/enums/billing-cycle.enum.ts
109
+ var BILLING_CYCLE = Object.freeze({
110
+ MONTHLY: "monthly",
111
+ YEARLY: "yearly"
112
+ });
113
+ var BILLING_CYCLE_VALUES = Object.freeze([
114
+ BILLING_CYCLE.MONTHLY,
115
+ BILLING_CYCLE.YEARLY
116
+ ]);
117
+ var CANONICAL_BILLING_CYCLES = new Set(BILLING_CYCLE_VALUES);
118
+ var BILLING_CYCLE_ALIASES = /* @__PURE__ */ new Map([
119
+ [BILLING_CYCLE.MONTHLY, BILLING_CYCLE.MONTHLY],
120
+ ["month", BILLING_CYCLE.MONTHLY],
121
+ [BILLING_CYCLE.YEARLY, BILLING_CYCLE.YEARLY],
122
+ ["year", BILLING_CYCLE.YEARLY]
123
+ ]);
124
+ function isKnownBillingCycle(value) {
125
+ return typeof value === "string" && CANONICAL_BILLING_CYCLES.has(value.toLowerCase());
126
+ }
127
+ function parseBillingCycle(value) {
128
+ if (typeof value !== "string") return null;
129
+ return BILLING_CYCLE_ALIASES.get(value.toLowerCase()) ?? null;
130
+ }
131
+
132
+ // src/domain/enums/amount-type.enum.ts
133
+ var POLAR_AMOUNT_TYPE = Object.freeze({
134
+ FIXED: "fixed",
135
+ CUSTOM: "custom",
136
+ FREE: "free",
137
+ SEAT_BASED: "seat_based",
138
+ METERED_UNIT: "metered_unit"
139
+ });
140
+
141
+ // src/domain/entities/cancellation.entity.ts
142
+ var CANCELLATION_REASON = Object.freeze({
143
+ TOO_EXPENSIVE: "too_expensive",
144
+ MISSING_FEATURES: "missing_features",
145
+ SWITCHED_SERVICE: "switched_service",
146
+ UNUSED: "unused",
147
+ CUSTOMER_SERVICE: "customer_service",
148
+ LOW_QUALITY: "low_quality",
149
+ TOO_COMPLEX: "too_complex",
150
+ OTHER: "other"
151
+ });
152
+ var CANCELLATION_REASON_VALUES = Object.freeze([
153
+ CANCELLATION_REASON.TOO_EXPENSIVE,
154
+ CANCELLATION_REASON.MISSING_FEATURES,
155
+ CANCELLATION_REASON.SWITCHED_SERVICE,
156
+ CANCELLATION_REASON.UNUSED,
157
+ CANCELLATION_REASON.CUSTOMER_SERVICE,
158
+ CANCELLATION_REASON.LOW_QUALITY,
159
+ CANCELLATION_REASON.TOO_COMPLEX,
160
+ CANCELLATION_REASON.OTHER
161
+ ]);
162
+
163
+ // src/domain/entities/subscription.entity.ts
164
+ var FREE_SUBSCRIPTION_STATUS = Object.freeze({
165
+ plan: "free",
166
+ subscriptionStatus: "none"
167
+ });
168
+ function isActiveSubscription(status) {
169
+ return status.subscriptionStatus === "active" || status.subscriptionStatus === "trialing";
170
+ }
171
+
172
+ // src/infrastructure/services/memory/create-memory-adapter.ts
173
+ function createMemoryBillingAdapter(config = {}) {
174
+ let status = {
175
+ plan: "free",
176
+ subscriptionStatus: "none",
177
+ billingCycle: "monthly",
178
+ cancelAtPeriodEnd: false,
179
+ ...config.initialStatus
180
+ };
181
+ const history = config.initialHistory ? [...config.initialHistory] : [];
182
+ const pending = /* @__PURE__ */ new Map();
183
+ let sequence = 0;
184
+ function checkoutUrl(checkout) {
185
+ if (checkout.params.successUrl) {
186
+ return checkout.params.successUrl.replace("{CHECKOUT_ID}", checkout.id);
187
+ }
188
+ const base = config.checkoutUrlBase ?? "https://checkout.example";
189
+ return `${base.replace(/\/$/, "")}/${checkout.id}`;
190
+ }
191
+ function fulfill(checkout) {
192
+ const planName = checkout.params.planKey ?? checkout.params.productId ?? checkout.params.productKey ?? "default";
193
+ return {
194
+ ...status,
195
+ plan: planName,
196
+ subscriptionStatus: "active",
197
+ subscriptionId: `sub_${checkout.id.slice(4)}`,
198
+ billingCycle: checkout.params.billingCycle ?? status.billingCycle,
199
+ cancelAtPeriodEnd: false,
200
+ currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString()
201
+ };
202
+ }
203
+ return {
204
+ async getStatus(_userId) {
205
+ return { ...status };
206
+ },
207
+ async createCheckout(params) {
208
+ config.onCheckout?.(params);
209
+ sequence += 1;
210
+ const checkout = {
211
+ id: `chk_${Date.now()}_${sequence}`,
212
+ params
213
+ };
214
+ pending.set(checkout.id, checkout);
215
+ return {
216
+ id: checkout.id,
217
+ url: checkoutUrl(checkout)
218
+ };
219
+ },
220
+ async syncSubscription(_userId, checkoutId) {
221
+ const checkout = checkoutId ? pending.get(checkoutId) : void 0;
222
+ const last = checkout ? void 0 : [...pending.values()].pop();
223
+ const target = checkout ?? last;
224
+ if (!target) {
225
+ return { synced: false, plan: status.plan };
226
+ }
227
+ status = fulfill(target);
228
+ pending.delete(target.id);
229
+ history.unshift({
230
+ id: `ord_${target.id}`,
231
+ amount: 0,
232
+ currency: "usd",
233
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
234
+ productName: target.params.planKey ?? target.params.productId,
235
+ status: "paid",
236
+ paid: true
237
+ });
238
+ return { synced: true, plan: status.plan };
239
+ },
240
+ async getBillingHistory(_userId) {
241
+ return [...history];
242
+ },
243
+ async cancelSubscription(reason) {
244
+ config.onCancel?.(reason);
245
+ if (!isActiveSubscription(status)) {
246
+ return { success: false };
247
+ }
248
+ status = {
249
+ ...status,
250
+ subscriptionStatus: "canceled",
251
+ cancelAtPeriodEnd: true
252
+ };
253
+ return {
254
+ success: true,
255
+ endsAt: status.currentPeriodEnd ?? (/* @__PURE__ */ new Date()).toISOString()
256
+ };
257
+ },
258
+ async getPortalUrl(_userId) {
259
+ return config.portalUrl ?? "https://portal.example/billing";
260
+ }
261
+ };
262
+ }
263
+
264
+ // src/domain/mappers/product.mapper.ts
265
+ var KNOWN_AMOUNT_TYPES = /* @__PURE__ */ new Set([
266
+ "fixed",
267
+ "custom",
268
+ "free",
269
+ "seat_based",
270
+ "metered_unit"
271
+ ]);
272
+ function toAmountType(raw) {
273
+ if (raw && KNOWN_AMOUNT_TYPES.has(raw)) return raw;
274
+ return "fixed";
275
+ }
276
+ function toBillingCycle(interval) {
277
+ return parseBillingCycle(interval);
278
+ }
279
+ function asRecord(value) {
280
+ return value && typeof value === "object" ? value : {};
281
+ }
282
+ function toPrice(dto) {
283
+ return {
284
+ id: String(dto.id),
285
+ type: toAmountType(dto.type),
286
+ amountType: toAmountType(dto.amount_type),
287
+ amount: typeof dto.amount === "number" ? dto.amount : void 0,
288
+ currency: String(dto.currency),
289
+ recurringInterval: toBillingCycle(dto.recurring_interval) ?? void 0,
290
+ isArchived: Boolean(dto.is_archived),
291
+ productId: String(dto.product_id),
292
+ legacy: Boolean(dto.legacy)
293
+ };
294
+ }
295
+ function toBenefit(dto) {
296
+ return {
297
+ id: String(dto.id),
298
+ type: String(dto.type),
299
+ description: dto.description ?? null,
300
+ properties: asRecord(dto.properties)
301
+ };
302
+ }
303
+ function mapProduct(dto) {
304
+ if (!dto || typeof dto !== "object") {
305
+ throw new TypeError("mapProduct: expected a product DTO object");
306
+ }
307
+ return {
308
+ id: String(dto.id),
309
+ name: String(dto.name),
310
+ description: dto.description ?? null,
311
+ isRecurring: Boolean(dto.is_recurring),
312
+ recurringInterval: toBillingCycle(dto.recurring_interval),
313
+ isArchived: Boolean(dto.is_archived),
314
+ organizationId: String(dto.organization_id),
315
+ prices: Array.isArray(dto.prices) ? dto.prices.map(toPrice) : [],
316
+ benefits: Array.isArray(dto.benefits) ? dto.benefits.map(toBenefit) : [],
317
+ metadata: asRecord(dto.metadata),
318
+ createdAt: String(dto.created_at),
319
+ modifiedAt: dto.modified_at ?? null
320
+ };
321
+ }
322
+ function mapProductList(dto) {
323
+ if (!dto || typeof dto !== "object") {
324
+ return { items: [], pagination: { totalCount: 0, maxPage: 1 } };
325
+ }
326
+ return {
327
+ items: Array.isArray(dto.items) ? dto.items.map(mapProduct) : [],
328
+ pagination: {
329
+ totalCount: Number(dto.pagination?.total_count ?? 0),
330
+ maxPage: Number(dto.pagination?.max_page ?? 1)
331
+ }
332
+ };
333
+ }
334
+
335
+ // src/domain/utils/minor-units.ts
336
+ var MINOR_UNIT_DIVISOR = 100;
337
+ var ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
338
+ "JPY",
339
+ "KRW",
340
+ "VND",
341
+ "CLP",
342
+ "PYG",
343
+ "XAF",
344
+ "XOF",
345
+ "BIF",
346
+ "DJF",
347
+ "GNF",
348
+ "XPF"
349
+ ]);
350
+ function minorUnitDivisor(currency) {
351
+ return ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase()) ? 1 : MINOR_UNIT_DIVISOR;
352
+ }
353
+ function toMajorUnits(amount, currency) {
354
+ return amount / minorUnitDivisor(currency);
355
+ }
356
+ function toMinorUnits(amount, currency) {
357
+ return Math.round(amount * minorUnitDivisor(currency));
358
+ }
359
+
360
+ // src/domain/utils/percent-discount.ts
361
+ function normalizeCouponCode(code) {
362
+ if (typeof code !== "string") return null;
363
+ const normalized = code.trim().toUpperCase();
364
+ return normalized.length > 0 ? normalized : null;
365
+ }
366
+ function applyPercentDiscount(baseAmount, percent, minimumAmount) {
367
+ if (!Number.isFinite(baseAmount) || baseAmount <= 0) return 0;
368
+ if (!Number.isFinite(percent) || percent <= 0) return Math.round(baseAmount);
369
+ const discounted = Math.round(baseAmount * (1 - percent / 100));
370
+ return Math.max(minimumAmount, discounted);
371
+ }
372
+
373
+ // src/infrastructure/utils/receipt.util.ts
374
+ function formatReceiptNumber(id, prefix = "RCP") {
375
+ if (!id || typeof id !== "string") return `${prefix}-00000000`;
376
+ const clean = id.replace(/[^a-zA-Z0-9]/g, "");
377
+ const tail = clean.length >= 8 ? clean.slice(-8) : clean.padStart(8, "0");
378
+ return `${prefix}-${tail.toUpperCase()}`;
379
+ }
380
+
381
+ // src/infrastructure/utils/checkout-metadata.ts
382
+ function asNonEmptyString(value) {
383
+ if (typeof value !== "string" || value.length === 0) return void 0;
384
+ return value;
385
+ }
386
+ function buildCheckoutMetadata(input) {
387
+ const meta = {
388
+ userId: input.userId,
389
+ itemType: input.itemType,
390
+ itemId: input.itemId
391
+ };
392
+ if (input.couponCode && input.couponCode.trim().length > 0) {
393
+ meta.couponCode = input.couponCode.trim();
394
+ }
395
+ if (input.extra) {
396
+ for (const [key, value] of Object.entries(input.extra)) {
397
+ if (typeof value === "string" && value.length > 0) {
398
+ meta[key] = value;
399
+ }
400
+ }
401
+ }
402
+ return meta;
403
+ }
404
+ function readCheckoutMetadata(metadata, allowedItemTypes) {
405
+ if (metadata === void 0) return null;
406
+ const userId = asNonEmptyString(metadata.userId);
407
+ const itemType = asNonEmptyString(metadata.itemType);
408
+ const itemId = asNonEmptyString(metadata.itemId);
409
+ if (!userId || !itemType || !itemId) return null;
410
+ if (!allowedItemTypes.includes(itemType)) return null;
411
+ const couponCode = asNonEmptyString(metadata.couponCode);
412
+ return { userId, itemType, itemId, ...couponCode ? { couponCode } : {} };
413
+ }
414
+
415
+ // src/presentation/components/BillingProvider.tsx
416
+ var import_react2 = require("react");
417
+
418
+ // src/infrastructure/utils/security.util.ts
419
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "[::1]"]);
420
+ function isValidCheckoutUrl(url) {
421
+ return typeof url === "string" && (url.startsWith("https://") || url.startsWith("http://"));
422
+ }
423
+ function isInsecureUrl(url) {
424
+ return typeof url === "string" && url.startsWith("http://");
425
+ }
426
+ function isLoopbackHost(url) {
427
+ try {
428
+ const host = new URL(url).hostname.toLowerCase();
429
+ return LOOPBACK_HOSTS.has(host);
430
+ } catch {
431
+ return false;
432
+ }
433
+ }
434
+ function isProductionInsecureUrl(url) {
435
+ return isInsecureUrl(url) && !isLoopbackHost(url);
436
+ }
437
+
438
+ // src/infrastructure/utils/validation.util.ts
439
+ function normalizeUserId(userId) {
440
+ if (typeof userId !== "string") return void 0;
441
+ const trimmed = userId.trim();
442
+ return trimmed.length > 0 ? trimmed : void 0;
443
+ }
444
+ function isValidProductId(productId) {
445
+ return typeof productId === "string" && productId.trim().length > 0;
446
+ }
447
+
448
+ // src/presentation/navigation/navigation.strategy.ts
449
+ var WindowNavigationStrategy = class {
450
+ redirect(url) {
451
+ window.location.href = url;
452
+ }
453
+ };
454
+ var NoopNavigationStrategy = class {
455
+ redirect(_url) {
456
+ }
457
+ };
458
+
459
+ // src/presentation/context/billing-context.ts
460
+ var import_react = require("react");
461
+ var BillingContext = (0, import_react.createContext)(void 0);
462
+ function useBilling() {
463
+ const ctx = (0, import_react.useContext)(BillingContext);
464
+ if (!ctx) {
465
+ throw new Error("useBilling must be used within <BillingProvider>");
466
+ }
467
+ return ctx;
468
+ }
469
+
470
+ // src/presentation/components/BillingProvider.tsx
471
+ var import_jsx_runtime = require("react/jsx-runtime");
472
+ function defaultNavigation() {
473
+ if (typeof window === "undefined") return new NoopNavigationStrategy();
474
+ return {
475
+ redirect(url) {
476
+ window.location.href = url;
477
+ }
478
+ };
479
+ }
480
+ function BillingProvider({
481
+ adapter,
482
+ userId,
483
+ navigation,
484
+ children
485
+ }) {
486
+ const [status, setStatus] = (0, import_react2.useState)(FREE_SUBSCRIPTION_STATUS);
487
+ const [loading, setLoading] = (0, import_react2.useState)(true);
488
+ const adapterRef = (0, import_react2.useRef)(adapter);
489
+ const userIdRef = (0, import_react2.useRef)(normalizeUserId(userId));
490
+ const navigationRef = (0, import_react2.useRef)(navigation ?? defaultNavigation());
491
+ const refreshGenerationRef = (0, import_react2.useRef)(0);
492
+ adapterRef.current = adapter;
493
+ userIdRef.current = normalizeUserId(userId);
494
+ navigationRef.current = navigation ?? navigationRef.current;
495
+ const refresh = (0, import_react2.useCallback)(async () => {
496
+ const generation = ++refreshGenerationRef.current;
497
+ const uid = userIdRef.current;
498
+ if (!uid) {
499
+ setStatus(FREE_SUBSCRIPTION_STATUS);
500
+ setLoading(false);
501
+ return;
502
+ }
503
+ setLoading(true);
504
+ try {
505
+ const next = await adapterRef.current.getStatus(uid);
506
+ if (generation !== refreshGenerationRef.current) return;
507
+ setStatus(next);
508
+ } catch {
509
+ if (generation !== refreshGenerationRef.current) return;
510
+ setStatus(FREE_SUBSCRIPTION_STATUS);
511
+ } finally {
512
+ if (generation === refreshGenerationRef.current) {
513
+ setLoading(false);
514
+ }
515
+ }
516
+ }, []);
517
+ (0, import_react2.useEffect)(() => {
518
+ void refresh();
519
+ }, [userId, refresh]);
520
+ const startCheckout = (0, import_react2.useCallback)(async (params) => {
521
+ if (!isValidProductId(params.productId)) {
522
+ throw new Error("[billing] Invalid productId: must be a non-empty string");
523
+ }
524
+ const uid = userIdRef.current;
525
+ if (!uid) {
526
+ throw new Error("[billing] Cannot start checkout: no authenticated user");
527
+ }
528
+ const result = await adapterRef.current.createCheckout({ ...params, userId: uid });
529
+ if (!isValidCheckoutUrl(result.url)) {
530
+ throw new Error("[billing] Invalid checkout URL: must start with https:// or http://");
531
+ }
532
+ if (isProductionInsecureUrl(result.url)) {
533
+ throw new Error("[billing] Insecure http:// checkout URL in production");
534
+ }
535
+ navigationRef.current.redirect(result.url);
536
+ }, []);
537
+ const syncSubscription = (0, import_react2.useCallback)(async () => {
538
+ const uid = userIdRef.current;
539
+ if (!uid) return { synced: false };
540
+ const checkoutId = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("checkout_id") ?? void 0 : void 0;
541
+ const result = await adapterRef.current.syncSubscription(uid, checkoutId);
542
+ if (result.synced) {
543
+ await refresh();
544
+ }
545
+ return result;
546
+ }, [refresh]);
547
+ const getBillingHistory = (0, import_react2.useCallback)(async () => {
548
+ const uid = userIdRef.current;
549
+ if (!uid) return [];
550
+ return adapterRef.current.getBillingHistory(uid);
551
+ }, []);
552
+ const cancelSubscription = (0, import_react2.useCallback)(
553
+ async (reason) => {
554
+ const result = await adapterRef.current.cancelSubscription(reason);
555
+ if (result.success) {
556
+ await refresh();
557
+ }
558
+ return result;
559
+ },
560
+ [refresh]
561
+ );
562
+ const getPortalUrl = (0, import_react2.useCallback)(async () => {
563
+ const uid = userIdRef.current;
564
+ if (!uid) {
565
+ throw new Error("[billing] Cannot get portal URL: no authenticated user");
566
+ }
567
+ return adapterRef.current.getPortalUrl(uid);
568
+ }, []);
569
+ const value = (0, import_react2.useMemo)(
570
+ () => ({
571
+ status,
572
+ loading,
573
+ refresh,
574
+ startCheckout,
575
+ syncSubscription,
576
+ getBillingHistory,
577
+ cancelSubscription,
578
+ getPortalUrl
579
+ }),
580
+ [
581
+ status,
582
+ loading,
583
+ refresh,
584
+ startCheckout,
585
+ syncSubscription,
586
+ getBillingHistory,
587
+ cancelSubscription,
588
+ getPortalUrl
589
+ ]
590
+ );
591
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BillingContext.Provider, { value, children });
592
+ }
593
+
594
+ // src/presentation/components/CheckoutButton.tsx
595
+ var import_react5 = require("react");
596
+
597
+ // src/presentation/hooks/useCheckoutButton.ts
598
+ var import_react3 = require("react");
599
+ function useCheckoutButton(options) {
600
+ const { startCheckout } = useBilling();
601
+ const [loading, setLoading] = (0, import_react3.useState)(false);
602
+ const [error, setError] = (0, import_react3.useState)(null);
603
+ const { productId, billingCycle, successUrl, planKey, onError } = options;
604
+ const start = (0, import_react3.useCallback)(async () => {
605
+ setLoading(true);
606
+ setError(null);
607
+ try {
608
+ await startCheckout({
609
+ productId,
610
+ billingCycle,
611
+ successUrl,
612
+ planKey
613
+ });
614
+ } catch (err) {
615
+ const normalized = err instanceof Error ? err : new Error(String(err));
616
+ setError(normalized);
617
+ onError?.(normalized);
618
+ } finally {
619
+ setLoading(false);
620
+ }
621
+ }, [startCheckout, productId, billingCycle, successUrl, planKey, onError]);
622
+ return { start, loading, error };
623
+ }
624
+
625
+ // src/presentation/theme/visual-tokens.ts
626
+ var VisualTokens = Object.freeze({
627
+ spacing: Object.freeze({
628
+ none: 0,
629
+ xs: 4,
630
+ sm: 6,
631
+ md: 8,
632
+ lg: 12,
633
+ xl: 16,
634
+ xxl: 20,
635
+ section: 24,
636
+ page: 32
637
+ }),
638
+ radius: Object.freeze({
639
+ sm: 6,
640
+ md: 8,
641
+ lg: 12,
642
+ pill: 999
643
+ }),
644
+ fontSize: Object.freeze({
645
+ caption: 12,
646
+ body: 14,
647
+ title: 18,
648
+ display: 24
649
+ }),
650
+ fontWeight: Object.freeze({
651
+ regular: 400,
652
+ medium: 500,
653
+ strong: 600
654
+ }),
655
+ lineHeight: Object.freeze({
656
+ tight: 1.25,
657
+ normal: 1.5
658
+ }),
659
+ opacity: Object.freeze({
660
+ muted: 0.7,
661
+ disabled: 0.6,
662
+ hover: 0.85
663
+ }),
664
+ color: Object.freeze({
665
+ textPrimary: "#111827",
666
+ textMuted: "#6b7280",
667
+ textOnPrimary: "#ffffff",
668
+ textInverse: "#ffffff",
669
+ textError: "#b91c1c",
670
+ textOnError: "#ffffff",
671
+ surfacePrimary: "#ffffff",
672
+ surfaceMuted: "#f9fafb",
673
+ surfaceSubtle: "#f3f4f6",
674
+ surfaceWarning: "#fef3c7",
675
+ surfaceDanger: "#fee2e2",
676
+ surfaceDangerMuted: "#fef2f2",
677
+ borderSubtle: "#e5e7eb",
678
+ borderStrong: "#d1d5db",
679
+ borderWarning: "#fecaca",
680
+ brandPrimary: "#2563eb",
681
+ brandPrimaryHover: "#1d4ed8",
682
+ brandDanger: "#dc2626",
683
+ brandDangerHover: "#b91c1c",
684
+ brandDangerText: "#991b1b",
685
+ badgeNeutralBg: "#e5e7eb",
686
+ badgeNeutralFg: "#111827",
687
+ badgeActiveBg: "#dcfce7",
688
+ badgeActiveFg: "#166534",
689
+ badgeWarningBg: "#fef3c7",
690
+ badgeWarningFg: "#92400e",
691
+ badgeDangerBg: "#fee2e2",
692
+ badgeDangerFg: "#991b1b"
693
+ }),
694
+ transition: Object.freeze({
695
+ instant: "120ms ease"
696
+ }),
697
+ layout: Object.freeze({
698
+ cardMinWidth: 260,
699
+ benefitListMax: 5
700
+ })
701
+ });
702
+ var TONE_PAIRS = Object.freeze({
703
+ neutral: { bg: VisualTokens.color.badgeNeutralBg, fg: VisualTokens.color.badgeNeutralFg },
704
+ active: { bg: VisualTokens.color.badgeActiveBg, fg: VisualTokens.color.badgeActiveFg },
705
+ warning: { bg: VisualTokens.color.badgeWarningBg, fg: VisualTokens.color.badgeWarningFg },
706
+ danger: { bg: VisualTokens.color.badgeDangerBg, fg: VisualTokens.color.badgeDangerFg }
707
+ });
708
+
709
+ // src/presentation/utils/checkout-button-visual.ts
710
+ var VARIANT_COLOR_SCHEMES = Object.freeze({
711
+ primary: Object.freeze({
712
+ backgroundColor: VisualTokens.color.brandPrimary,
713
+ color: VisualTokens.color.textOnPrimary,
714
+ border: `1px solid ${VisualTokens.color.brandPrimary}`
715
+ }),
716
+ secondary: Object.freeze({
717
+ backgroundColor: VisualTokens.color.surfacePrimary,
718
+ color: VisualTokens.color.textPrimary,
719
+ border: `1px solid ${VisualTokens.color.borderStrong}`
720
+ }),
721
+ danger: Object.freeze({
722
+ backgroundColor: VisualTokens.color.brandDanger,
723
+ color: VisualTokens.color.textOnPrimary,
724
+ border: `1px solid ${VisualTokens.color.brandDanger}`
725
+ })
726
+ });
727
+ var CheckoutButtonVisual = class {
728
+ static resolve({ variant, disabled, loading }) {
729
+ const scheme = VARIANT_COLOR_SCHEMES[variant];
730
+ const isInactive = disabled || loading;
731
+ return {
732
+ ...scheme,
733
+ padding: `${VisualTokens.spacing.lg}px ${VisualTokens.spacing.xl}px`,
734
+ fontSize: VisualTokens.fontSize.body,
735
+ fontWeight: VisualTokens.fontWeight.strong,
736
+ borderRadius: VisualTokens.radius.md,
737
+ cursor: isInactive ? "not-allowed" : "pointer",
738
+ opacity: isInactive ? VisualTokens.opacity.disabled : 1,
739
+ transition: VisualTokens.transition.instant
740
+ };
741
+ }
742
+ };
743
+
744
+ // src/presentation/utils/inline-error-visual.ts
745
+ var import_react4 = require("react");
746
+ var InlineErrorVisual = class _InlineErrorVisual {
747
+ static style() {
748
+ return {
749
+ color: VisualTokens.color.textError,
750
+ fontSize: VisualTokens.fontSize.caption
751
+ };
752
+ }
753
+ static element(message) {
754
+ if (!message) return null;
755
+ return (0, import_react4.createElement)(
756
+ "span",
757
+ { role: "alert", style: _InlineErrorVisual.style() },
758
+ message
759
+ );
760
+ }
761
+ };
762
+
763
+ // src/presentation/components/CheckoutButton.tsx
764
+ var import_jsx_runtime2 = require("react/jsx-runtime");
765
+ var DEFAULT_LABELS = Object.freeze({
766
+ loading: "Redirecting\u2026"
767
+ });
768
+ function CheckoutButton({
769
+ productId,
770
+ billingCycle,
771
+ successUrl,
772
+ planKey,
773
+ label,
774
+ variant = "primary",
775
+ disabled = false,
776
+ onError,
777
+ className,
778
+ labels
779
+ }) {
780
+ const { start, loading, error } = useCheckoutButton({
781
+ productId,
782
+ billingCycle,
783
+ successUrl,
784
+ planKey,
785
+ onError
786
+ });
787
+ const style = (0, import_react5.useMemo)(
788
+ () => CheckoutButtonVisual.resolve({ variant, disabled, loading }),
789
+ [variant, disabled, loading]
790
+ );
791
+ const text = (0, import_react5.useMemo)(
792
+ () => ({ ...DEFAULT_LABELS, ...labels }),
793
+ [labels]
794
+ );
795
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { display: "inline-flex", flexDirection: "column", gap: 4 }, children: [
796
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
797
+ "button",
798
+ {
799
+ type: "button",
800
+ style,
801
+ className,
802
+ onClick: () => void start(),
803
+ disabled: disabled || loading,
804
+ "aria-busy": loading,
805
+ children: loading ? text.loading : label
806
+ }
807
+ ),
808
+ InlineErrorVisual.element(error?.message)
809
+ ] });
810
+ }
811
+
812
+ // src/presentation/components/SubscriptionStatusBadge.tsx
813
+ var import_react6 = require("react");
814
+
815
+ // src/presentation/utils/status-tone.ts
816
+ var StatusTone = class _StatusTone {
817
+ static MAP = Object.freeze({
818
+ active: "active",
819
+ trialing: "active",
820
+ past_due: "warning",
821
+ incomplete: "warning",
822
+ canceled: "danger",
823
+ revoked: "danger",
824
+ unpaid: "danger",
825
+ incomplete_expired: "danger",
826
+ none: "neutral"
827
+ });
828
+ static forStatus(status) {
829
+ return _StatusTone.MAP[status] ?? "neutral";
830
+ }
831
+ static pairFor(status) {
832
+ return TONE_PAIRS[_StatusTone.forStatus(status)];
833
+ }
834
+ };
835
+
836
+ // src/presentation/utils/status-label.ts
837
+ var StatusLabel = class _StatusLabel {
838
+ static MAP = Object.freeze({
839
+ active: "Active",
840
+ trialing: "Trial",
841
+ past_due: "Past due",
842
+ incomplete: "Incomplete",
843
+ canceled: "Canceled",
844
+ revoked: "Revoked",
845
+ unpaid: "Unpaid",
846
+ incomplete_expired: "Expired",
847
+ none: "Free"
848
+ });
849
+ static forStatus(status, overrides) {
850
+ return overrides?.[status] ?? _StatusLabel.MAP[status] ?? "Free";
851
+ }
852
+ };
853
+
854
+ // src/infrastructure/utils/date-renderer.ts
855
+ var DEFAULT_LOCALE = "en-US";
856
+ var ISO_DATE_DISPLAY_LENGTH = 10;
857
+ var FALLBACK_DATE_LABEL = "soon";
858
+ var FALLBACK_LONG_DATE = "\u2014";
859
+ var DateRenderer = class _DateRenderer {
860
+ dateLength;
861
+ shortFormatter;
862
+ longFormatter;
863
+ constructor(locale = DEFAULT_LOCALE, dateLength = ISO_DATE_DISPLAY_LENGTH) {
864
+ this.dateLength = dateLength;
865
+ this.shortFormatter = new Intl.DateTimeFormat(locale, {
866
+ year: "numeric",
867
+ month: "short",
868
+ day: "numeric"
869
+ });
870
+ this.longFormatter = new Intl.DateTimeFormat(locale, {
871
+ year: "numeric",
872
+ month: "long",
873
+ day: "numeric"
874
+ });
875
+ }
876
+ short(iso) {
877
+ if (!iso) return FALLBACK_LONG_DATE;
878
+ return iso.slice(0, this.dateLength);
879
+ }
880
+ renewal(iso, fallback = FALLBACK_DATE_LABEL) {
881
+ const date = _DateRenderer.parse(iso);
882
+ if (!date) return fallback;
883
+ return this.shortFormatter.format(date);
884
+ }
885
+ long(iso) {
886
+ if (!iso) return FALLBACK_LONG_DATE;
887
+ const date = _DateRenderer.parse(iso);
888
+ if (!date) return iso;
889
+ return this.longFormatter.format(date);
890
+ }
891
+ static parse(iso) {
892
+ if (!iso) return null;
893
+ const date = new Date(iso);
894
+ return Number.isNaN(date.getTime()) ? null : date;
895
+ }
896
+ };
897
+
898
+ // src/presentation/components/SubscriptionStatusBadge.tsx
899
+ var import_jsx_runtime3 = require("react/jsx-runtime");
900
+ var DEFAULT_LABELS2 = Object.freeze({
901
+ loading: "Loading\u2026",
902
+ endsPrefix: "ends"
903
+ });
904
+ function SubscriptionStatusBadge({
905
+ status: statusOverride,
906
+ loading: loadingOverride,
907
+ locale,
908
+ labels
909
+ }) {
910
+ const ctx = useBilling();
911
+ const status = statusOverride ?? ctx.status;
912
+ const loading = loadingOverride ?? ctx.loading;
913
+ const dateRenderer = (0, import_react6.useMemo)(() => new DateRenderer(locale), [locale]);
914
+ const text = (0, import_react6.useMemo)(
915
+ () => ({ ...DEFAULT_LABELS2, ...labels }),
916
+ [labels]
917
+ );
918
+ const pillStyle = (0, import_react6.useMemo)(
919
+ () => ({
920
+ padding: `${VisualTokens.spacing.xs}px ${VisualTokens.spacing.lg - 2}px`,
921
+ borderRadius: VisualTokens.radius.pill,
922
+ fontSize: VisualTokens.fontSize.caption,
923
+ fontWeight: VisualTokens.fontWeight.strong
924
+ }),
925
+ []
926
+ );
927
+ if (loading) {
928
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
929
+ "span",
930
+ {
931
+ style: {
932
+ ...pillStyle,
933
+ ...TONE_PAIRS.neutral
934
+ },
935
+ children: text.loading
936
+ }
937
+ );
938
+ }
939
+ const tone = StatusTone.forStatus(status.subscriptionStatus);
940
+ const color = TONE_PAIRS[tone];
941
+ const label = StatusLabel.forStatus(status.subscriptionStatus, labels?.statuses);
942
+ const showPlan = status.plan && status.plan !== "free";
943
+ const showEnds = status.cancelAtPeriodEnd;
944
+ const endsLabel = showEnds ? `${text.endsPrefix} ${dateRenderer.renewal(status.currentPeriodEnd)}` : null;
945
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
946
+ "span",
947
+ {
948
+ "aria-label": `Subscription ${label}`,
949
+ style: {
950
+ backgroundColor: color.bg,
951
+ color: color.fg,
952
+ padding: pillStyle.padding,
953
+ borderRadius: VisualTokens.radius.pill,
954
+ fontSize: VisualTokens.fontSize.caption,
955
+ fontWeight: VisualTokens.fontWeight.strong,
956
+ display: "inline-flex",
957
+ alignItems: "center",
958
+ gap: VisualTokens.spacing.xs + 2
959
+ },
960
+ children: [
961
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { textTransform: "capitalize" }, children: label }),
962
+ showPlan ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { opacity: VisualTokens.opacity.muted }, children: [
963
+ "\xB7 ",
964
+ status.plan
965
+ ] }) : null,
966
+ endsLabel ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { opacity: VisualTokens.opacity.muted }, children: [
967
+ "\xB7 ",
968
+ endsLabel
969
+ ] }) : null
970
+ ]
971
+ }
972
+ );
973
+ }
974
+
975
+ // src/presentation/components/PricingTable.tsx
976
+ var import_react8 = require("react");
977
+
978
+ // src/presentation/hooks/useProducts.ts
979
+ var import_react7 = require("react");
980
+
981
+ // src/infrastructure/constants/billing.constants.ts
982
+ var API_BASE_URL = "https://api.polar.sh/v1";
983
+ var API_MAX_PAGE_SIZE = 100;
984
+
985
+ // src/infrastructure/http/http-client.ts
986
+ var DEFAULT_TIMEOUT_MS = 15e3;
987
+ var FetchHttpClient = class {
988
+ async request(request) {
989
+ const init = {
990
+ method: request.method,
991
+ headers: {
992
+ Accept: "application/json",
993
+ ...request.headers ?? {}
994
+ }
995
+ };
996
+ if (request.body !== void 0) {
997
+ init.body = JSON.stringify(request.body);
998
+ init.headers["Content-Type"] = "application/json";
999
+ }
1000
+ const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1001
+ const controller = new AbortController();
1002
+ let timedOut = false;
1003
+ const timer = setTimeout(() => {
1004
+ timedOut = true;
1005
+ controller.abort();
1006
+ }, timeoutMs);
1007
+ const forwardAbort = () => controller.abort();
1008
+ if (request.signal) {
1009
+ if (request.signal.aborted) controller.abort();
1010
+ else request.signal.addEventListener("abort", forwardAbort);
1011
+ }
1012
+ init.signal = controller.signal;
1013
+ let response;
1014
+ try {
1015
+ response = await fetch(request.url, init);
1016
+ } catch (error) {
1017
+ if (timedOut && error instanceof Error && error.name === "AbortError") {
1018
+ throw new DOMException(`Request timed out after ${timeoutMs}ms`, "TimeoutError");
1019
+ }
1020
+ throw error;
1021
+ } finally {
1022
+ clearTimeout(timer);
1023
+ request.signal?.removeEventListener("abort", forwardAbort);
1024
+ }
1025
+ const bodyText = await response.text();
1026
+ let data = null;
1027
+ if (bodyText.length > 0) {
1028
+ try {
1029
+ data = JSON.parse(bodyText);
1030
+ } catch {
1031
+ data = null;
1032
+ }
1033
+ }
1034
+ return {
1035
+ status: response.status,
1036
+ ok: response.ok,
1037
+ data,
1038
+ text: async () => bodyText
1039
+ };
1040
+ }
1041
+ };
1042
+
1043
+ // src/infrastructure/query/query-builder.ts
1044
+ var QueryBuilder = class {
1045
+ params;
1046
+ constructor(initial) {
1047
+ this.params = new URLSearchParams();
1048
+ if (initial) this.setMany(initial);
1049
+ }
1050
+ set(key, value) {
1051
+ if (value === void 0 || value === null) return this;
1052
+ this.params.set(key, String(value));
1053
+ return this;
1054
+ }
1055
+ setMany(values) {
1056
+ for (const [key, value] of Object.entries(values)) {
1057
+ this.set(key, value);
1058
+ }
1059
+ return this;
1060
+ }
1061
+ toString() {
1062
+ return this.params.toString();
1063
+ }
1064
+ };
1065
+
1066
+ // src/infrastructure/pagination/page-iterator.ts
1067
+ var PageIterator = class {
1068
+ fetcher;
1069
+ limit;
1070
+ signal;
1071
+ constructor(fetcher, options = {}) {
1072
+ this.fetcher = fetcher;
1073
+ this.limit = options.limit ?? 100;
1074
+ this.signal = options.signal;
1075
+ }
1076
+ async *[Symbol.asyncIterator]() {
1077
+ let page = 1;
1078
+ while (true) {
1079
+ if (this.signal?.aborted) return;
1080
+ const current = await this.fetcher.fetchPage(page, this.limit);
1081
+ for (const item of current.items) {
1082
+ if (this.signal?.aborted) return;
1083
+ yield item;
1084
+ }
1085
+ if (current.isLast || page >= current.maxPage) return;
1086
+ page++;
1087
+ }
1088
+ }
1089
+ };
1090
+
1091
+ // src/infrastructure/utils/pagination-clamp.ts
1092
+ var MIN_PAGE_SIZE = 1;
1093
+ var PaginationClamp = class {
1094
+ static pageSize(raw, max = API_MAX_PAGE_SIZE) {
1095
+ if (raw === void 0) return max;
1096
+ return Math.min(Math.max(MIN_PAGE_SIZE, raw), max);
1097
+ }
1098
+ };
1099
+
1100
+ // src/infrastructure/services/product.repository.ts
1101
+ var BillingApiError = class _BillingApiError extends Error {
1102
+ constructor(message, status, body) {
1103
+ super(message);
1104
+ this.status = status;
1105
+ this.body = body;
1106
+ this.name = "BillingApiError";
1107
+ Object.setPrototypeOf(this, _BillingApiError.prototype);
1108
+ }
1109
+ };
1110
+ var ProductRepository = class {
1111
+ http;
1112
+ apiToken;
1113
+ baseUrl;
1114
+ constructor(config, http = new FetchHttpClient()) {
1115
+ this.http = http;
1116
+ this.apiToken = config.apiToken;
1117
+ this.baseUrl = config.baseUrl ?? API_BASE_URL;
1118
+ }
1119
+ async list(params, options) {
1120
+ const query = new QueryBuilder({
1121
+ organization_id: params?.organizationId,
1122
+ is_recurring: params?.isRecurring,
1123
+ page: params?.page,
1124
+ limit: this.clampLimit(params?.limit)
1125
+ });
1126
+ const url = `${this.baseUrl}/products?${query.toString()}`;
1127
+ const response = await this.http.request({
1128
+ method: "GET",
1129
+ url,
1130
+ headers: this.authHeaders(),
1131
+ signal: options?.signal,
1132
+ timeoutMs: options?.timeoutMs
1133
+ });
1134
+ if (!response.ok) {
1135
+ const body = await response.text();
1136
+ throw new BillingApiError(
1137
+ `Billing API error: ${response.status}`,
1138
+ response.status,
1139
+ body
1140
+ );
1141
+ }
1142
+ return mapProductList(response.data);
1143
+ }
1144
+ async findById(productId, options) {
1145
+ const url = `${this.baseUrl}/products/${encodeURIComponent(productId)}`;
1146
+ const response = await this.http.request({
1147
+ method: "GET",
1148
+ url,
1149
+ headers: this.authHeaders(),
1150
+ signal: options?.signal,
1151
+ timeoutMs: options?.timeoutMs
1152
+ });
1153
+ if (response.status === 404) return null;
1154
+ if (!response.ok) {
1155
+ const body = await response.text();
1156
+ throw new BillingApiError(
1157
+ `Billing API error: ${response.status}`,
1158
+ response.status,
1159
+ body
1160
+ );
1161
+ }
1162
+ if (!response.data) {
1163
+ throw new BillingApiError(
1164
+ "Billing API returned an empty product body",
1165
+ response.status
1166
+ );
1167
+ }
1168
+ return mapProduct(response.data);
1169
+ }
1170
+ iterateAll(params, options = {}) {
1171
+ const limit = this.clampLimit(options.limit);
1172
+ const fetcher = {
1173
+ fetchPage: async (page, pageLimit) => {
1174
+ const response = await this.list(
1175
+ { ...params, page, limit: pageLimit },
1176
+ { signal: options.signal }
1177
+ );
1178
+ const pageData = {
1179
+ items: response.items,
1180
+ page,
1181
+ totalCount: response.pagination.totalCount,
1182
+ maxPage: response.pagination.maxPage,
1183
+ isLast: page >= response.pagination.maxPage
1184
+ };
1185
+ return pageData;
1186
+ }
1187
+ };
1188
+ return new PageIterator(fetcher, { limit, signal: options.signal });
1189
+ }
1190
+ async listAll(params, options = {}) {
1191
+ const out = [];
1192
+ for await (const product of this.iterateAll(params, options)) {
1193
+ out.push(product);
1194
+ }
1195
+ return out;
1196
+ }
1197
+ authHeaders() {
1198
+ return { Authorization: `Bearer ${this.apiToken}` };
1199
+ }
1200
+ clampLimit(limit) {
1201
+ return PaginationClamp.pageSize(limit, API_MAX_PAGE_SIZE);
1202
+ }
1203
+ };
1204
+ var defaultClient = new FetchHttpClient();
1205
+ async function getAllProducts(config, params, options = {}) {
1206
+ return new ProductRepository(config, defaultClient).listAll(params, options);
1207
+ }
1208
+
1209
+ // src/presentation/hooks/useProducts.ts
1210
+ function useProducts(options) {
1211
+ const { apiToken, organizationId, isRecurring = true, enabled = true } = options;
1212
+ const [products, setProducts] = (0, import_react7.useState)([]);
1213
+ const [loading, setLoading] = (0, import_react7.useState)(enabled);
1214
+ const [error, setError] = (0, import_react7.useState)(null);
1215
+ const optionsRef = (0, import_react7.useRef)({ apiToken, organizationId, isRecurring, enabled });
1216
+ optionsRef.current = { apiToken, organizationId, isRecurring, enabled };
1217
+ const abortRef = (0, import_react7.useRef)(null);
1218
+ const fetchProducts = (0, import_react7.useCallback)(async () => {
1219
+ const { apiToken: token, organizationId: org, isRecurring: recurring, enabled: isEnabled } = optionsRef.current;
1220
+ abortRef.current?.abort();
1221
+ if (!isEnabled) {
1222
+ setLoading(false);
1223
+ return;
1224
+ }
1225
+ const controller = new AbortController();
1226
+ abortRef.current = controller;
1227
+ setLoading(true);
1228
+ setError(null);
1229
+ try {
1230
+ const params = { isRecurring: recurring };
1231
+ if (org) params.organizationId = org;
1232
+ const data = await getAllProducts({ apiToken: token }, params, {
1233
+ signal: controller.signal
1234
+ });
1235
+ if (controller.signal.aborted) return;
1236
+ setProducts(data);
1237
+ } catch (err) {
1238
+ if (controller.signal.aborted) return;
1239
+ setError(err instanceof Error ? err : new Error("Failed to fetch products"));
1240
+ } finally {
1241
+ if (!controller.signal.aborted) {
1242
+ setLoading(false);
1243
+ }
1244
+ }
1245
+ }, []);
1246
+ (0, import_react7.useEffect)(() => {
1247
+ void fetchProducts();
1248
+ return () => {
1249
+ abortRef.current?.abort();
1250
+ };
1251
+ }, [apiToken, organizationId, isRecurring, enabled, fetchProducts]);
1252
+ return { products, loading, error, refetch: fetchProducts };
1253
+ }
1254
+
1255
+ // src/presentation/utils/product-pricing.ts
1256
+ var FALLBACK_PRICE_LABEL = "Custom";
1257
+ var ProductPricing = class _ProductPricing {
1258
+ static recurringPrice(product, cycle) {
1259
+ const recurring = product.prices.filter(
1260
+ (p) => p.recurringInterval !== void 0
1261
+ );
1262
+ if (recurring.length === 0) {
1263
+ return product.prices[0] ?? null;
1264
+ }
1265
+ if (cycle) {
1266
+ const match = recurring.find((p) => p.recurringInterval === cycle);
1267
+ if (match) return match;
1268
+ }
1269
+ return recurring[0] ?? null;
1270
+ }
1271
+ static displayLine(product, formatter, cycle) {
1272
+ const price = _ProductPricing.recurringPrice(product, cycle);
1273
+ if (!price) {
1274
+ return { amount: FALLBACK_PRICE_LABEL, interval: null };
1275
+ }
1276
+ const amount = formatter(price.amount, price.currency);
1277
+ return { amount, interval: price.recurringInterval ?? null };
1278
+ }
1279
+ };
1280
+
1281
+ // src/infrastructure/utils/money-formatter.ts
1282
+ var DEFAULT_LOCALE2 = "en-US";
1283
+ var DEFAULT_CURRENCY = "USD";
1284
+ var MoneyFormatter = class {
1285
+ locale;
1286
+ constructor(locale = DEFAULT_LOCALE2) {
1287
+ this.locale = locale;
1288
+ }
1289
+ format(amount, currency, options = {}) {
1290
+ if (amount === void 0 || amount === null) {
1291
+ return "Custom";
1292
+ }
1293
+ const code = (currency ?? options.fallbackCurrency ?? DEFAULT_CURRENCY).toUpperCase();
1294
+ try {
1295
+ return new Intl.NumberFormat(this.locale, {
1296
+ style: "currency",
1297
+ currency: code,
1298
+ maximumFractionDigits: options.maximumFractionDigits ?? 2
1299
+ }).format(amount / minorUnitDivisor(code));
1300
+ } catch {
1301
+ return `${amount} ${code}`;
1302
+ }
1303
+ }
1304
+ };
1305
+
1306
+ // src/presentation/components/PricingTable.tsx
1307
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1308
+ var DEFAULT_LABELS3 = Object.freeze({
1309
+ loading: "Loading pricing\u2026",
1310
+ empty: "No products available.",
1311
+ errorPrefix: "Failed to load pricing:",
1312
+ retry: "Retry"
1313
+ });
1314
+ function PricingTable({
1315
+ products: productsOverride,
1316
+ apiToken,
1317
+ organizationId,
1318
+ isRecurring = true,
1319
+ ctaLabel = "Subscribe",
1320
+ onError,
1321
+ filterInterval = "monthly",
1322
+ locale,
1323
+ labels
1324
+ }) {
1325
+ const enabled = productsOverride === void 0 && apiToken !== void 0;
1326
+ const { products: fetchedProducts, loading, error, refetch } = useProducts({
1327
+ apiToken: apiToken ?? "",
1328
+ organizationId,
1329
+ isRecurring,
1330
+ enabled
1331
+ });
1332
+ const products = productsOverride ?? fetchedProducts;
1333
+ const moneyFormatter = (0, import_react8.useMemo)(() => new MoneyFormatter(locale), [locale]);
1334
+ const text = (0, import_react8.useMemo)(
1335
+ () => ({ ...DEFAULT_LABELS3, ...labels }),
1336
+ [labels]
1337
+ );
1338
+ const visible = (0, import_react8.useMemo)(() => {
1339
+ if (filterInterval === null) return products;
1340
+ return products.filter((p) => p.recurringInterval === filterInterval);
1341
+ }, [products, filterInterval]);
1342
+ if (enabled && loading) {
1343
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1344
+ "div",
1345
+ {
1346
+ role: "status",
1347
+ "aria-live": "polite",
1348
+ style: {
1349
+ padding: VisualTokens.spacing.section,
1350
+ textAlign: "center"
1351
+ },
1352
+ children: text.loading
1353
+ }
1354
+ );
1355
+ }
1356
+ if (error) {
1357
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1358
+ "div",
1359
+ {
1360
+ role: "alert",
1361
+ style: {
1362
+ padding: VisualTokens.spacing.xl,
1363
+ background: VisualTokens.color.surfaceDanger,
1364
+ borderRadius: VisualTokens.radius.md
1365
+ },
1366
+ children: [
1367
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: { margin: 0, color: VisualTokens.color.brandDangerText }, children: [
1368
+ text.errorPrefix,
1369
+ " ",
1370
+ error.message
1371
+ ] }),
1372
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1373
+ "button",
1374
+ {
1375
+ type: "button",
1376
+ onClick: () => void refetch(),
1377
+ style: { marginTop: VisualTokens.spacing.md, padding: "6px 12px" },
1378
+ children: text.retry
1379
+ }
1380
+ )
1381
+ ]
1382
+ }
1383
+ );
1384
+ }
1385
+ if (visible.length === 0) {
1386
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1387
+ "div",
1388
+ {
1389
+ style: {
1390
+ padding: VisualTokens.spacing.section,
1391
+ textAlign: "center",
1392
+ color: VisualTokens.color.textMuted
1393
+ },
1394
+ children: text.empty
1395
+ }
1396
+ );
1397
+ }
1398
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1399
+ "div",
1400
+ {
1401
+ style: {
1402
+ display: "grid",
1403
+ gridTemplateColumns: `repeat(auto-fit, minmax(${VisualTokens.layout.cardMinWidth}px, 1fr))`,
1404
+ gap: VisualTokens.spacing.xl
1405
+ },
1406
+ children: visible.map((product) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1407
+ ProductCard,
1408
+ {
1409
+ product,
1410
+ moneyFormatter,
1411
+ ctaLabel,
1412
+ onError
1413
+ },
1414
+ product.id
1415
+ ))
1416
+ }
1417
+ );
1418
+ }
1419
+ function ProductCard({ product, moneyFormatter, ctaLabel, onError }) {
1420
+ const { amount, interval } = (0, import_react8.useMemo)(
1421
+ () => ProductPricing.displayLine(
1422
+ product,
1423
+ (amt, cur) => moneyFormatter.format(amt, cur)
1424
+ ),
1425
+ [product, moneyFormatter]
1426
+ );
1427
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1428
+ "article",
1429
+ {
1430
+ style: {
1431
+ border: `1px solid ${VisualTokens.color.borderSubtle}`,
1432
+ borderRadius: VisualTokens.radius.lg,
1433
+ padding: VisualTokens.spacing.xxl,
1434
+ display: "flex",
1435
+ flexDirection: "column",
1436
+ gap: VisualTokens.spacing.lg
1437
+ },
1438
+ children: [
1439
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("header", { children: [
1440
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { style: { margin: 0, fontSize: VisualTokens.fontSize.title }, children: product.name }),
1441
+ product.description ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1442
+ "p",
1443
+ {
1444
+ style: {
1445
+ margin: `${VisualTokens.spacing.xs}px 0 0`,
1446
+ color: VisualTokens.color.textMuted,
1447
+ fontSize: VisualTokens.fontSize.body
1448
+ },
1449
+ children: product.description
1450
+ }
1451
+ ) : null
1452
+ ] }),
1453
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: VisualTokens.fontSize.display, fontWeight: VisualTokens.fontWeight.strong }, children: [
1454
+ amount,
1455
+ interval ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1456
+ "span",
1457
+ {
1458
+ style: {
1459
+ fontSize: VisualTokens.fontSize.body,
1460
+ fontWeight: VisualTokens.fontWeight.regular,
1461
+ color: VisualTokens.color.textMuted
1462
+ },
1463
+ children: [
1464
+ " ",
1465
+ "/ ",
1466
+ interval
1467
+ ]
1468
+ }
1469
+ ) : null
1470
+ ] }),
1471
+ product.benefits.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1472
+ "ul",
1473
+ {
1474
+ style: {
1475
+ paddingLeft: 18,
1476
+ margin: 0,
1477
+ color: "#374151",
1478
+ fontSize: VisualTokens.fontSize.body
1479
+ },
1480
+ children: product.benefits.slice(0, VisualTokens.layout.benefitListMax).map((b) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: b.description ?? b.type }, b.id))
1481
+ }
1482
+ ) : null,
1483
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { marginTop: "auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1484
+ CheckoutButton,
1485
+ {
1486
+ productId: product.id,
1487
+ billingCycle: product.recurringInterval ?? void 0,
1488
+ label: ctaLabel,
1489
+ onError
1490
+ }
1491
+ ) })
1492
+ ]
1493
+ }
1494
+ );
1495
+ }
1496
+
1497
+ // src/presentation/components/BillingHistoryList.tsx
1498
+ var import_react9 = require("react");
1499
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1500
+ var DEFAULT_LABELS4 = Object.freeze({
1501
+ loading: "Loading billing history\u2026",
1502
+ empty: "No invoices yet.",
1503
+ refresh: "Refresh",
1504
+ retry: "Retry",
1505
+ paid: "Paid",
1506
+ invoiceLink: "View",
1507
+ invoiceFallback: "\u2014",
1508
+ colDate: "Date",
1509
+ colProduct: "Product",
1510
+ colStatus: "Status",
1511
+ colAmount: "Amount",
1512
+ colInvoice: "Invoice"
1513
+ });
1514
+ function BillingHistoryList({
1515
+ autoLoad = true,
1516
+ orders: ordersOverride,
1517
+ loading: loadingOverride,
1518
+ locale,
1519
+ labels
1520
+ }) {
1521
+ const { getBillingHistory } = useBilling();
1522
+ const [orders, setOrders] = (0, import_react9.useState)(ordersOverride ?? []);
1523
+ const [loading, setLoading] = (0, import_react9.useState)(autoLoad && ordersOverride === void 0);
1524
+ const [error, setError] = (0, import_react9.useState)(null);
1525
+ const moneyFormatter = (0, import_react9.useMemo)(() => new MoneyFormatter(locale), [locale]);
1526
+ const dateRenderer = (0, import_react9.useMemo)(() => new DateRenderer(locale), [locale]);
1527
+ const text = (0, import_react9.useMemo)(
1528
+ () => ({ ...DEFAULT_LABELS4, ...labels }),
1529
+ [labels]
1530
+ );
1531
+ const refresh = (0, import_react9.useCallback)(async () => {
1532
+ setLoading(true);
1533
+ setError(null);
1534
+ try {
1535
+ const next = await getBillingHistory();
1536
+ setOrders(next);
1537
+ } catch (err) {
1538
+ setError(err instanceof Error ? err.message : "Failed to load billing history");
1539
+ } finally {
1540
+ setLoading(false);
1541
+ }
1542
+ }, [getBillingHistory]);
1543
+ (0, import_react9.useEffect)(() => {
1544
+ if (autoLoad && ordersOverride === void 0) {
1545
+ void refresh();
1546
+ }
1547
+ }, [autoLoad, ordersOverride, refresh]);
1548
+ if (ordersOverride !== void 0 && ordersOverride.length > 0) {
1549
+ return renderList(ordersOverride, moneyFormatter, dateRenderer, text);
1550
+ }
1551
+ if (loadingOverride !== void 0 && loadingOverride) {
1552
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { role: "status", children: text.loading });
1553
+ }
1554
+ if (loading) {
1555
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { role: "status", children: text.loading });
1556
+ }
1557
+ if (error) {
1558
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1559
+ "div",
1560
+ {
1561
+ role: "alert",
1562
+ style: {
1563
+ background: VisualTokens.color.surfaceDanger,
1564
+ padding: VisualTokens.spacing.lg,
1565
+ borderRadius: VisualTokens.radius.md
1566
+ },
1567
+ children: [
1568
+ error,
1569
+ " ",
1570
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: () => void refresh(), children: text.retry })
1571
+ ]
1572
+ }
1573
+ );
1574
+ }
1575
+ if (orders.length === 0) {
1576
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { color: VisualTokens.color.textMuted }, children: text.empty });
1577
+ }
1578
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
1579
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { marginBottom: VisualTokens.spacing.md }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: () => void refresh(), children: text.refresh }) }),
1580
+ renderList(orders, moneyFormatter, dateRenderer, text)
1581
+ ] });
1582
+ }
1583
+ function renderList(orders, moneyFormatter, dateRenderer, text) {
1584
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
1585
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("tr", { style: { textAlign: "left", borderBottom: `1px solid ${VisualTokens.color.borderSubtle}` }, children: [
1586
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: { padding: VisualTokens.spacing.md }, children: text.colDate }),
1587
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: { padding: VisualTokens.spacing.md }, children: text.colProduct }),
1588
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: { padding: VisualTokens.spacing.md }, children: text.colStatus }),
1589
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: { padding: VisualTokens.spacing.md }, children: text.colAmount }),
1590
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: { padding: VisualTokens.spacing.md }, children: text.colInvoice })
1591
+ ] }) }),
1592
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tbody", { children: orders.map((order) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("tr", { style: { borderBottom: `1px solid ${VisualTokens.color.surfaceSubtle}` }, children: [
1593
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { style: { padding: VisualTokens.spacing.md }, children: dateRenderer.short(order.createdAt) }),
1594
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { style: { padding: VisualTokens.spacing.md }, children: order.productName }),
1595
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { style: { padding: VisualTokens.spacing.md }, children: order.paid ? text.paid : order.status }),
1596
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { style: { padding: VisualTokens.spacing.md }, children: moneyFormatter.format(order.amount, order.currency) }),
1597
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { style: { padding: VisualTokens.spacing.md }, children: order.invoiceUrl ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("a", { href: order.invoiceUrl, target: "_blank", rel: "noreferrer", children: text.invoiceLink }) : text.invoiceFallback })
1598
+ ] }, order.id)) })
1599
+ ] });
1600
+ }
1601
+
1602
+ // src/presentation/components/CustomerPortalLink.tsx
1603
+ var import_react10 = require("react");
1604
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1605
+ var DEFAULT_LABEL = "Manage subscription";
1606
+ var DEFAULT_LOADING_LABEL = "Opening\u2026";
1607
+ function CustomerPortalLink({
1608
+ label = DEFAULT_LABEL,
1609
+ target = "open",
1610
+ className,
1611
+ labels
1612
+ }) {
1613
+ const { getPortalUrl } = useBilling();
1614
+ const [loading, setLoading] = (0, import_react10.useState)(false);
1615
+ const [error, setError] = (0, import_react10.useState)(null);
1616
+ const loadingLabel = labels?.loading ?? DEFAULT_LOADING_LABEL;
1617
+ const open = async () => {
1618
+ setLoading(true);
1619
+ setError(null);
1620
+ try {
1621
+ const url = await getPortalUrl();
1622
+ if (target === "open") {
1623
+ window.open(url, "_blank", "noopener,noreferrer");
1624
+ } else {
1625
+ window.location.href = url;
1626
+ }
1627
+ } catch (err) {
1628
+ setError(err instanceof Error ? err.message : "Failed to open portal");
1629
+ } finally {
1630
+ setLoading(false);
1631
+ }
1632
+ };
1633
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: { display: "inline-flex", flexDirection: "column", gap: VisualTokens.spacing.xs }, children: [
1634
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1635
+ "button",
1636
+ {
1637
+ type: "button",
1638
+ onClick: () => void open(),
1639
+ disabled: loading,
1640
+ className,
1641
+ style: {
1642
+ background: "transparent",
1643
+ color: VisualTokens.color.brandPrimary,
1644
+ border: "none",
1645
+ padding: 0,
1646
+ textDecoration: "underline",
1647
+ cursor: loading ? "wait" : "pointer"
1648
+ },
1649
+ children: loading ? loadingLabel : label
1650
+ }
1651
+ ),
1652
+ InlineErrorVisual.element(error)
1653
+ ] });
1654
+ }
1655
+
1656
+ // src/presentation/components/CancelSubscriptionButton.tsx
1657
+ var import_react12 = require("react");
1658
+
1659
+ // src/presentation/hooks/useCancelSubscription.ts
1660
+ var import_react11 = require("react");
1661
+ function useCancelSubscription(options = {}) {
1662
+ const { cancelSubscription } = useBilling();
1663
+ const [loading, setLoading] = (0, import_react11.useState)(false);
1664
+ const [error, setError] = (0, import_react11.useState)(null);
1665
+ const { onSuccess, onError } = options;
1666
+ const cancel = (0, import_react11.useCallback)(
1667
+ async (reason) => {
1668
+ setLoading(true);
1669
+ setError(null);
1670
+ try {
1671
+ const result = await cancelSubscription(reason);
1672
+ onSuccess?.(result);
1673
+ return result;
1674
+ } catch (err) {
1675
+ const normalized = err instanceof Error ? err : new Error(String(err));
1676
+ setError(normalized);
1677
+ onError?.(normalized);
1678
+ return null;
1679
+ } finally {
1680
+ setLoading(false);
1681
+ }
1682
+ },
1683
+ [cancelSubscription, onSuccess, onError]
1684
+ );
1685
+ return { cancel, loading, error };
1686
+ }
1687
+
1688
+ // src/presentation/utils/cancellation-reason-catalog.ts
1689
+ var CANCELLATION_REASON_CATALOG = Object.freeze([
1690
+ { value: "too_expensive", label: "Too expensive" },
1691
+ { value: "missing_features", label: "Missing features" },
1692
+ { value: "switched_service", label: "Switched to a different service" },
1693
+ { value: "unused", label: "Not using it" },
1694
+ { value: "customer_service", label: "Poor customer service" },
1695
+ { value: "low_quality", label: "Quality is too low" },
1696
+ { value: "too_complex", label: "Too complex" },
1697
+ { value: "other", label: "Other" }
1698
+ ]);
1699
+
1700
+ // src/presentation/components/CancelSubscriptionButton.tsx
1701
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1702
+ var DEFAULT_LABELS5 = Object.freeze({
1703
+ action: "Cancel subscription",
1704
+ confirm: "Confirm cancel",
1705
+ keep: "Keep subscription",
1706
+ canceling: "Canceling\u2026",
1707
+ promptTitle: "Why are you canceling? (optional)",
1708
+ noReason: "Prefer not to say"
1709
+ });
1710
+ function CancelSubscriptionButton({
1711
+ label,
1712
+ renderPrompt,
1713
+ onSuccess,
1714
+ reasonCatalog = CANCELLATION_REASON_CATALOG,
1715
+ labels
1716
+ }) {
1717
+ const { cancel, loading, error } = useCancelSubscription({
1718
+ onSuccess: () => onSuccess?.()
1719
+ });
1720
+ const [promptOpen, setPromptOpen] = (0, import_react12.useState)(false);
1721
+ const [reason, setReason] = (0, import_react12.useState)("");
1722
+ const text = { ...DEFAULT_LABELS5, ...labels };
1723
+ const triggerLabel = label ?? text.action;
1724
+ const onConfirm = async (selected) => {
1725
+ setPromptOpen(false);
1726
+ const result = await cancel(selected);
1727
+ if (result?.success) {
1728
+ setReason("");
1729
+ }
1730
+ };
1731
+ if (!promptOpen) {
1732
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { style: { display: "inline-flex", flexDirection: "column", gap: VisualTokens.spacing.xs }, children: [
1733
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1734
+ "button",
1735
+ {
1736
+ type: "button",
1737
+ onClick: () => setPromptOpen(true),
1738
+ style: {
1739
+ background: "transparent",
1740
+ color: VisualTokens.color.brandDangerText,
1741
+ border: `1px solid ${VisualTokens.color.borderWarning}`,
1742
+ borderRadius: VisualTokens.radius.md,
1743
+ padding: `${VisualTokens.spacing.md}px ${VisualTokens.spacing.lg}px`,
1744
+ cursor: "pointer",
1745
+ fontWeight: VisualTokens.fontWeight.strong
1746
+ },
1747
+ children: triggerLabel
1748
+ }
1749
+ ),
1750
+ InlineErrorVisual.element(error?.message)
1751
+ ] });
1752
+ }
1753
+ if (renderPrompt) {
1754
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: renderPrompt((selected) => void onConfirm(selected)) });
1755
+ }
1756
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1757
+ "div",
1758
+ {
1759
+ role: "dialog",
1760
+ "aria-label": "Confirm cancellation",
1761
+ style: {
1762
+ border: `1px solid ${VisualTokens.color.borderWarning}`,
1763
+ background: VisualTokens.color.surfaceDangerMuted,
1764
+ borderRadius: VisualTokens.radius.md,
1765
+ padding: VisualTokens.spacing.lg,
1766
+ display: "flex",
1767
+ flexDirection: "column",
1768
+ gap: VisualTokens.spacing.md
1769
+ },
1770
+ children: [
1771
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { style: { fontSize: VisualTokens.fontSize.body, fontWeight: VisualTokens.fontWeight.strong }, children: [
1772
+ text.promptTitle,
1773
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1774
+ "select",
1775
+ {
1776
+ value: reason,
1777
+ onChange: (e) => setReason(e.target.value),
1778
+ style: {
1779
+ display: "block",
1780
+ marginTop: VisualTokens.spacing.xs,
1781
+ width: "100%",
1782
+ padding: VisualTokens.spacing.sm
1783
+ },
1784
+ children: [
1785
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "", children: text.noReason }),
1786
+ reasonCatalog.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: opt.value, children: opt.label }, opt.value))
1787
+ ]
1788
+ }
1789
+ )
1790
+ ] }),
1791
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", gap: VisualTokens.spacing.md }, children: [
1792
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1793
+ "button",
1794
+ {
1795
+ type: "button",
1796
+ onClick: () => void onConfirm(reason || void 0),
1797
+ disabled: loading,
1798
+ style: {
1799
+ background: VisualTokens.color.brandDanger,
1800
+ color: VisualTokens.color.textOnPrimary,
1801
+ border: "none",
1802
+ padding: `${VisualTokens.spacing.sm}px ${VisualTokens.spacing.lg}px`,
1803
+ borderRadius: VisualTokens.radius.sm
1804
+ },
1805
+ children: loading ? text.canceling : text.confirm
1806
+ }
1807
+ ),
1808
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1809
+ "button",
1810
+ {
1811
+ type: "button",
1812
+ onClick: () => setPromptOpen(false),
1813
+ disabled: loading,
1814
+ style: {
1815
+ background: "transparent",
1816
+ border: `1px solid ${VisualTokens.color.borderStrong}`,
1817
+ padding: `${VisualTokens.spacing.sm}px ${VisualTokens.spacing.lg}px`,
1818
+ borderRadius: VisualTokens.radius.sm
1819
+ },
1820
+ children: text.keep
1821
+ }
1822
+ )
1823
+ ] }),
1824
+ InlineErrorVisual.element(error?.message)
1825
+ ]
1826
+ }
1827
+ );
1828
+ }
1829
+ // Annotate the CommonJS export names for ESM import in node:
1830
+ 0 && (module.exports = {
1831
+ BILLING_CYCLE,
1832
+ BILLING_CYCLE_VALUES,
1833
+ BillingContext,
1834
+ BillingHistoryList,
1835
+ BillingProvider,
1836
+ CANCELLATION_REASON,
1837
+ CANCELLATION_REASON_CATALOG,
1838
+ CANCELLATION_REASON_VALUES,
1839
+ CancelSubscriptionButton,
1840
+ CheckoutButton,
1841
+ CustomerPortalLink,
1842
+ FREE_SUBSCRIPTION_STATUS,
1843
+ NoopNavigationStrategy,
1844
+ PAYMENT_PROVIDER,
1845
+ PAYMENT_PROVIDER_VALUES,
1846
+ POLAR_AMOUNT_TYPE,
1847
+ PricingTable,
1848
+ SUBSCRIPTION_STATUS,
1849
+ SUBSCRIPTION_STATUS_VALUES,
1850
+ SubscriptionStatusBadge,
1851
+ TONE_PAIRS,
1852
+ VisualTokens,
1853
+ WindowNavigationStrategy,
1854
+ applyPercentDiscount,
1855
+ buildCheckoutMetadata,
1856
+ createMemoryBillingAdapter,
1857
+ formatReceiptNumber,
1858
+ isActiveSubscription,
1859
+ isKnownBillingCycle,
1860
+ isKnownPaymentProvider,
1861
+ isKnownSubscriptionStatus,
1862
+ mapProduct,
1863
+ mapProductList,
1864
+ minorUnitDivisor,
1865
+ normalizeCouponCode,
1866
+ parseBillingCycle,
1867
+ readCheckoutMetadata,
1868
+ toMajorUnits,
1869
+ toMinorUnits,
1870
+ useBilling,
1871
+ useCancelSubscription,
1872
+ useCheckoutButton,
1873
+ useProducts
1874
+ });