@umituz/web-payment 0.0.0-stage → 1.0.0

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