@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
@@ -0,0 +1,1128 @@
1
+ import {
2
+ FREE_SUBSCRIPTION_STATUS
3
+ } from "./chunk-3AXPZOCV.mjs";
4
+ import {
5
+ NoopNavigationStrategy,
6
+ TONE_PAIRS,
7
+ VisualTokens
8
+ } from "./chunk-2NH5DOLT.mjs";
9
+ import {
10
+ minorUnitDivisor
11
+ } from "./chunk-PIWNUAKW.mjs";
12
+ import {
13
+ getAllProducts
14
+ } from "./chunk-XQ2HJTTA.mjs";
15
+
16
+ // src/presentation/components/BillingProvider.tsx
17
+ import {
18
+ useCallback,
19
+ useEffect,
20
+ useMemo,
21
+ useRef,
22
+ useState
23
+ } from "react";
24
+
25
+ // src/infrastructure/utils/security.util.ts
26
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "[::1]"]);
27
+ function isValidCheckoutUrl(url) {
28
+ return typeof url === "string" && (url.startsWith("https://") || url.startsWith("http://"));
29
+ }
30
+ function isInsecureUrl(url) {
31
+ return typeof url === "string" && url.startsWith("http://");
32
+ }
33
+ function isLoopbackHost(url) {
34
+ try {
35
+ const host = new URL(url).hostname.toLowerCase();
36
+ return LOOPBACK_HOSTS.has(host);
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ function isProductionInsecureUrl(url) {
42
+ return isInsecureUrl(url) && !isLoopbackHost(url);
43
+ }
44
+
45
+ // src/infrastructure/utils/validation.util.ts
46
+ function normalizeUserId(userId) {
47
+ if (typeof userId !== "string") return void 0;
48
+ const trimmed = userId.trim();
49
+ return trimmed.length > 0 ? trimmed : void 0;
50
+ }
51
+ function isValidProductId(productId) {
52
+ return typeof productId === "string" && productId.trim().length > 0;
53
+ }
54
+
55
+ // src/presentation/context/billing-context.ts
56
+ import { createContext, useContext } from "react";
57
+ var BillingContext = createContext(void 0);
58
+ function useBilling() {
59
+ const ctx = useContext(BillingContext);
60
+ if (!ctx) {
61
+ throw new Error("useBilling must be used within <BillingProvider>");
62
+ }
63
+ return ctx;
64
+ }
65
+
66
+ // src/presentation/components/BillingProvider.tsx
67
+ import { jsx } from "react/jsx-runtime";
68
+ function defaultNavigation() {
69
+ if (typeof window === "undefined") return new NoopNavigationStrategy();
70
+ return {
71
+ redirect(url) {
72
+ window.location.href = url;
73
+ }
74
+ };
75
+ }
76
+ function BillingProvider({
77
+ adapter,
78
+ userId,
79
+ navigation,
80
+ children
81
+ }) {
82
+ const [status, setStatus] = useState(FREE_SUBSCRIPTION_STATUS);
83
+ const [loading, setLoading] = useState(true);
84
+ const adapterRef = useRef(adapter);
85
+ const userIdRef = useRef(normalizeUserId(userId));
86
+ const navigationRef = useRef(navigation ?? defaultNavigation());
87
+ const refreshGenerationRef = useRef(0);
88
+ adapterRef.current = adapter;
89
+ userIdRef.current = normalizeUserId(userId);
90
+ navigationRef.current = navigation ?? navigationRef.current;
91
+ const refresh = useCallback(async () => {
92
+ const generation = ++refreshGenerationRef.current;
93
+ const uid = userIdRef.current;
94
+ if (!uid) {
95
+ setStatus(FREE_SUBSCRIPTION_STATUS);
96
+ setLoading(false);
97
+ return;
98
+ }
99
+ setLoading(true);
100
+ try {
101
+ const next = await adapterRef.current.getStatus(uid);
102
+ if (generation !== refreshGenerationRef.current) return;
103
+ setStatus(next);
104
+ } catch {
105
+ if (generation !== refreshGenerationRef.current) return;
106
+ setStatus(FREE_SUBSCRIPTION_STATUS);
107
+ } finally {
108
+ if (generation === refreshGenerationRef.current) {
109
+ setLoading(false);
110
+ }
111
+ }
112
+ }, []);
113
+ useEffect(() => {
114
+ void refresh();
115
+ }, [userId, refresh]);
116
+ const startCheckout = useCallback(async (params) => {
117
+ if (!isValidProductId(params.productId)) {
118
+ throw new Error("[billing] Invalid productId: must be a non-empty string");
119
+ }
120
+ const uid = userIdRef.current;
121
+ if (!uid) {
122
+ throw new Error("[billing] Cannot start checkout: no authenticated user");
123
+ }
124
+ const result = await adapterRef.current.createCheckout({ ...params, userId: uid });
125
+ if (!isValidCheckoutUrl(result.url)) {
126
+ throw new Error("[billing] Invalid checkout URL: must start with https:// or http://");
127
+ }
128
+ if (isProductionInsecureUrl(result.url)) {
129
+ throw new Error("[billing] Insecure http:// checkout URL in production");
130
+ }
131
+ navigationRef.current.redirect(result.url);
132
+ }, []);
133
+ const syncSubscription = useCallback(async () => {
134
+ const uid = userIdRef.current;
135
+ if (!uid) return { synced: false };
136
+ const checkoutId = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("checkout_id") ?? void 0 : void 0;
137
+ const result = await adapterRef.current.syncSubscription(uid, checkoutId);
138
+ if (result.synced) {
139
+ await refresh();
140
+ }
141
+ return result;
142
+ }, [refresh]);
143
+ const getBillingHistory = useCallback(async () => {
144
+ const uid = userIdRef.current;
145
+ if (!uid) return [];
146
+ return adapterRef.current.getBillingHistory(uid);
147
+ }, []);
148
+ const cancelSubscription = useCallback(
149
+ async (reason) => {
150
+ const result = await adapterRef.current.cancelSubscription(reason);
151
+ if (result.success) {
152
+ await refresh();
153
+ }
154
+ return result;
155
+ },
156
+ [refresh]
157
+ );
158
+ const getPortalUrl = useCallback(async () => {
159
+ const uid = userIdRef.current;
160
+ if (!uid) {
161
+ throw new Error("[billing] Cannot get portal URL: no authenticated user");
162
+ }
163
+ return adapterRef.current.getPortalUrl(uid);
164
+ }, []);
165
+ const value = useMemo(
166
+ () => ({
167
+ status,
168
+ loading,
169
+ refresh,
170
+ startCheckout,
171
+ syncSubscription,
172
+ getBillingHistory,
173
+ cancelSubscription,
174
+ getPortalUrl
175
+ }),
176
+ [
177
+ status,
178
+ loading,
179
+ refresh,
180
+ startCheckout,
181
+ syncSubscription,
182
+ getBillingHistory,
183
+ cancelSubscription,
184
+ getPortalUrl
185
+ ]
186
+ );
187
+ return /* @__PURE__ */ jsx(BillingContext.Provider, { value, children });
188
+ }
189
+
190
+ // src/presentation/components/CheckoutButton.tsx
191
+ import { useMemo as useMemo2 } from "react";
192
+
193
+ // src/presentation/hooks/useCheckoutButton.ts
194
+ import { useCallback as useCallback2, useState as useState2 } from "react";
195
+ function useCheckoutButton(options) {
196
+ const { startCheckout } = useBilling();
197
+ const [loading, setLoading] = useState2(false);
198
+ const [error, setError] = useState2(null);
199
+ const { productId, billingCycle, successUrl, planKey, onError } = options;
200
+ const start = useCallback2(async () => {
201
+ setLoading(true);
202
+ setError(null);
203
+ try {
204
+ await startCheckout({
205
+ productId,
206
+ billingCycle,
207
+ successUrl,
208
+ planKey
209
+ });
210
+ } catch (err) {
211
+ const normalized = err instanceof Error ? err : new Error(String(err));
212
+ setError(normalized);
213
+ onError?.(normalized);
214
+ } finally {
215
+ setLoading(false);
216
+ }
217
+ }, [startCheckout, productId, billingCycle, successUrl, planKey, onError]);
218
+ return { start, loading, error };
219
+ }
220
+
221
+ // src/presentation/utils/checkout-button-visual.ts
222
+ var VARIANT_COLOR_SCHEMES = Object.freeze({
223
+ primary: Object.freeze({
224
+ backgroundColor: VisualTokens.color.brandPrimary,
225
+ color: VisualTokens.color.textOnPrimary,
226
+ border: `1px solid ${VisualTokens.color.brandPrimary}`
227
+ }),
228
+ secondary: Object.freeze({
229
+ backgroundColor: VisualTokens.color.surfacePrimary,
230
+ color: VisualTokens.color.textPrimary,
231
+ border: `1px solid ${VisualTokens.color.borderStrong}`
232
+ }),
233
+ danger: Object.freeze({
234
+ backgroundColor: VisualTokens.color.brandDanger,
235
+ color: VisualTokens.color.textOnPrimary,
236
+ border: `1px solid ${VisualTokens.color.brandDanger}`
237
+ })
238
+ });
239
+ var CheckoutButtonVisual = class {
240
+ static resolve({ variant, disabled, loading }) {
241
+ const scheme = VARIANT_COLOR_SCHEMES[variant];
242
+ const isInactive = disabled || loading;
243
+ return {
244
+ ...scheme,
245
+ padding: `${VisualTokens.spacing.lg}px ${VisualTokens.spacing.xl}px`,
246
+ fontSize: VisualTokens.fontSize.body,
247
+ fontWeight: VisualTokens.fontWeight.strong,
248
+ borderRadius: VisualTokens.radius.md,
249
+ cursor: isInactive ? "not-allowed" : "pointer",
250
+ opacity: isInactive ? VisualTokens.opacity.disabled : 1,
251
+ transition: VisualTokens.transition.instant
252
+ };
253
+ }
254
+ };
255
+
256
+ // src/presentation/utils/inline-error-visual.ts
257
+ import { createElement } from "react";
258
+ var InlineErrorVisual = class _InlineErrorVisual {
259
+ static style() {
260
+ return {
261
+ color: VisualTokens.color.textError,
262
+ fontSize: VisualTokens.fontSize.caption
263
+ };
264
+ }
265
+ static element(message) {
266
+ if (!message) return null;
267
+ return createElement(
268
+ "span",
269
+ { role: "alert", style: _InlineErrorVisual.style() },
270
+ message
271
+ );
272
+ }
273
+ };
274
+
275
+ // src/presentation/components/CheckoutButton.tsx
276
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
277
+ var DEFAULT_LABELS = Object.freeze({
278
+ loading: "Redirecting\u2026"
279
+ });
280
+ function CheckoutButton({
281
+ productId,
282
+ billingCycle,
283
+ successUrl,
284
+ planKey,
285
+ label,
286
+ variant = "primary",
287
+ disabled = false,
288
+ onError,
289
+ className,
290
+ labels
291
+ }) {
292
+ const { start, loading, error } = useCheckoutButton({
293
+ productId,
294
+ billingCycle,
295
+ successUrl,
296
+ planKey,
297
+ onError
298
+ });
299
+ const style = useMemo2(
300
+ () => CheckoutButtonVisual.resolve({ variant, disabled, loading }),
301
+ [variant, disabled, loading]
302
+ );
303
+ const text = useMemo2(
304
+ () => ({ ...DEFAULT_LABELS, ...labels }),
305
+ [labels]
306
+ );
307
+ return /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", flexDirection: "column", gap: 4 }, children: [
308
+ /* @__PURE__ */ jsx2(
309
+ "button",
310
+ {
311
+ type: "button",
312
+ style,
313
+ className,
314
+ onClick: () => void start(),
315
+ disabled: disabled || loading,
316
+ "aria-busy": loading,
317
+ children: loading ? text.loading : label
318
+ }
319
+ ),
320
+ InlineErrorVisual.element(error?.message)
321
+ ] });
322
+ }
323
+
324
+ // src/presentation/components/SubscriptionStatusBadge.tsx
325
+ import { useMemo as useMemo3 } from "react";
326
+
327
+ // src/presentation/utils/status-tone.ts
328
+ var StatusTone = class _StatusTone {
329
+ static MAP = Object.freeze({
330
+ active: "active",
331
+ trialing: "active",
332
+ past_due: "warning",
333
+ incomplete: "warning",
334
+ canceled: "danger",
335
+ revoked: "danger",
336
+ unpaid: "danger",
337
+ incomplete_expired: "danger",
338
+ none: "neutral"
339
+ });
340
+ static forStatus(status) {
341
+ return _StatusTone.MAP[status] ?? "neutral";
342
+ }
343
+ static pairFor(status) {
344
+ return TONE_PAIRS[_StatusTone.forStatus(status)];
345
+ }
346
+ };
347
+
348
+ // src/presentation/utils/status-label.ts
349
+ var StatusLabel = class _StatusLabel {
350
+ static MAP = Object.freeze({
351
+ active: "Active",
352
+ trialing: "Trial",
353
+ past_due: "Past due",
354
+ incomplete: "Incomplete",
355
+ canceled: "Canceled",
356
+ revoked: "Revoked",
357
+ unpaid: "Unpaid",
358
+ incomplete_expired: "Expired",
359
+ none: "Free"
360
+ });
361
+ static forStatus(status, overrides) {
362
+ return overrides?.[status] ?? _StatusLabel.MAP[status] ?? "Free";
363
+ }
364
+ };
365
+
366
+ // src/infrastructure/utils/date-renderer.ts
367
+ var DEFAULT_LOCALE = "en-US";
368
+ var ISO_DATE_DISPLAY_LENGTH = 10;
369
+ var FALLBACK_DATE_LABEL = "soon";
370
+ var FALLBACK_LONG_DATE = "\u2014";
371
+ var DateRenderer = class _DateRenderer {
372
+ dateLength;
373
+ shortFormatter;
374
+ longFormatter;
375
+ constructor(locale = DEFAULT_LOCALE, dateLength = ISO_DATE_DISPLAY_LENGTH) {
376
+ this.dateLength = dateLength;
377
+ this.shortFormatter = new Intl.DateTimeFormat(locale, {
378
+ year: "numeric",
379
+ month: "short",
380
+ day: "numeric"
381
+ });
382
+ this.longFormatter = new Intl.DateTimeFormat(locale, {
383
+ year: "numeric",
384
+ month: "long",
385
+ day: "numeric"
386
+ });
387
+ }
388
+ short(iso) {
389
+ if (!iso) return FALLBACK_LONG_DATE;
390
+ return iso.slice(0, this.dateLength);
391
+ }
392
+ renewal(iso, fallback = FALLBACK_DATE_LABEL) {
393
+ const date = _DateRenderer.parse(iso);
394
+ if (!date) return fallback;
395
+ return this.shortFormatter.format(date);
396
+ }
397
+ long(iso) {
398
+ if (!iso) return FALLBACK_LONG_DATE;
399
+ const date = _DateRenderer.parse(iso);
400
+ if (!date) return iso;
401
+ return this.longFormatter.format(date);
402
+ }
403
+ static parse(iso) {
404
+ if (!iso) return null;
405
+ const date = new Date(iso);
406
+ return Number.isNaN(date.getTime()) ? null : date;
407
+ }
408
+ };
409
+
410
+ // src/presentation/components/SubscriptionStatusBadge.tsx
411
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
412
+ var DEFAULT_LABELS2 = Object.freeze({
413
+ loading: "Loading\u2026",
414
+ endsPrefix: "ends"
415
+ });
416
+ function SubscriptionStatusBadge({
417
+ status: statusOverride,
418
+ loading: loadingOverride,
419
+ locale,
420
+ labels
421
+ }) {
422
+ const ctx = useBilling();
423
+ const status = statusOverride ?? ctx.status;
424
+ const loading = loadingOverride ?? ctx.loading;
425
+ const dateRenderer = useMemo3(() => new DateRenderer(locale), [locale]);
426
+ const text = useMemo3(
427
+ () => ({ ...DEFAULT_LABELS2, ...labels }),
428
+ [labels]
429
+ );
430
+ const pillStyle = useMemo3(
431
+ () => ({
432
+ padding: `${VisualTokens.spacing.xs}px ${VisualTokens.spacing.lg - 2}px`,
433
+ borderRadius: VisualTokens.radius.pill,
434
+ fontSize: VisualTokens.fontSize.caption,
435
+ fontWeight: VisualTokens.fontWeight.strong
436
+ }),
437
+ []
438
+ );
439
+ if (loading) {
440
+ return /* @__PURE__ */ jsx3(
441
+ "span",
442
+ {
443
+ style: {
444
+ ...pillStyle,
445
+ ...TONE_PAIRS.neutral
446
+ },
447
+ children: text.loading
448
+ }
449
+ );
450
+ }
451
+ const tone = StatusTone.forStatus(status.subscriptionStatus);
452
+ const color = TONE_PAIRS[tone];
453
+ const label = StatusLabel.forStatus(status.subscriptionStatus, labels?.statuses);
454
+ const showPlan = status.plan && status.plan !== "free";
455
+ const showEnds = status.cancelAtPeriodEnd;
456
+ const endsLabel = showEnds ? `${text.endsPrefix} ${dateRenderer.renewal(status.currentPeriodEnd)}` : null;
457
+ return /* @__PURE__ */ jsxs2(
458
+ "span",
459
+ {
460
+ "aria-label": `Subscription ${label}`,
461
+ style: {
462
+ backgroundColor: color.bg,
463
+ color: color.fg,
464
+ padding: pillStyle.padding,
465
+ borderRadius: VisualTokens.radius.pill,
466
+ fontSize: VisualTokens.fontSize.caption,
467
+ fontWeight: VisualTokens.fontWeight.strong,
468
+ display: "inline-flex",
469
+ alignItems: "center",
470
+ gap: VisualTokens.spacing.xs + 2
471
+ },
472
+ children: [
473
+ /* @__PURE__ */ jsx3("span", { style: { textTransform: "capitalize" }, children: label }),
474
+ showPlan ? /* @__PURE__ */ jsxs2("span", { style: { opacity: VisualTokens.opacity.muted }, children: [
475
+ "\xB7 ",
476
+ status.plan
477
+ ] }) : null,
478
+ endsLabel ? /* @__PURE__ */ jsxs2("span", { style: { opacity: VisualTokens.opacity.muted }, children: [
479
+ "\xB7 ",
480
+ endsLabel
481
+ ] }) : null
482
+ ]
483
+ }
484
+ );
485
+ }
486
+
487
+ // src/presentation/components/PricingTable.tsx
488
+ import { useMemo as useMemo4 } from "react";
489
+
490
+ // src/presentation/hooks/useProducts.ts
491
+ import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
492
+ function useProducts(options) {
493
+ const { apiToken, organizationId, isRecurring = true, enabled = true } = options;
494
+ const [products, setProducts] = useState3([]);
495
+ const [loading, setLoading] = useState3(enabled);
496
+ const [error, setError] = useState3(null);
497
+ const optionsRef = useRef2({ apiToken, organizationId, isRecurring, enabled });
498
+ optionsRef.current = { apiToken, organizationId, isRecurring, enabled };
499
+ const abortRef = useRef2(null);
500
+ const fetchProducts = useCallback3(async () => {
501
+ const { apiToken: token, organizationId: org, isRecurring: recurring, enabled: isEnabled } = optionsRef.current;
502
+ abortRef.current?.abort();
503
+ if (!isEnabled) {
504
+ setLoading(false);
505
+ return;
506
+ }
507
+ const controller = new AbortController();
508
+ abortRef.current = controller;
509
+ setLoading(true);
510
+ setError(null);
511
+ try {
512
+ const params = { isRecurring: recurring };
513
+ if (org) params.organizationId = org;
514
+ const data = await getAllProducts({ apiToken: token }, params, {
515
+ signal: controller.signal
516
+ });
517
+ if (controller.signal.aborted) return;
518
+ setProducts(data);
519
+ } catch (err) {
520
+ if (controller.signal.aborted) return;
521
+ setError(err instanceof Error ? err : new Error("Failed to fetch products"));
522
+ } finally {
523
+ if (!controller.signal.aborted) {
524
+ setLoading(false);
525
+ }
526
+ }
527
+ }, []);
528
+ useEffect2(() => {
529
+ void fetchProducts();
530
+ return () => {
531
+ abortRef.current?.abort();
532
+ };
533
+ }, [apiToken, organizationId, isRecurring, enabled, fetchProducts]);
534
+ return { products, loading, error, refetch: fetchProducts };
535
+ }
536
+
537
+ // src/presentation/utils/product-pricing.ts
538
+ var FALLBACK_PRICE_LABEL = "Custom";
539
+ var ProductPricing = class _ProductPricing {
540
+ static recurringPrice(product, cycle) {
541
+ const recurring = product.prices.filter(
542
+ (p) => p.recurringInterval !== void 0
543
+ );
544
+ if (recurring.length === 0) {
545
+ return product.prices[0] ?? null;
546
+ }
547
+ if (cycle) {
548
+ const match = recurring.find((p) => p.recurringInterval === cycle);
549
+ if (match) return match;
550
+ }
551
+ return recurring[0] ?? null;
552
+ }
553
+ static displayLine(product, formatter, cycle) {
554
+ const price = _ProductPricing.recurringPrice(product, cycle);
555
+ if (!price) {
556
+ return { amount: FALLBACK_PRICE_LABEL, interval: null };
557
+ }
558
+ const amount = formatter(price.amount, price.currency);
559
+ return { amount, interval: price.recurringInterval ?? null };
560
+ }
561
+ };
562
+
563
+ // src/infrastructure/utils/money-formatter.ts
564
+ var DEFAULT_LOCALE2 = "en-US";
565
+ var DEFAULT_CURRENCY = "USD";
566
+ var MoneyFormatter = class {
567
+ locale;
568
+ constructor(locale = DEFAULT_LOCALE2) {
569
+ this.locale = locale;
570
+ }
571
+ format(amount, currency, options = {}) {
572
+ if (amount === void 0 || amount === null) {
573
+ return "Custom";
574
+ }
575
+ const code = (currency ?? options.fallbackCurrency ?? DEFAULT_CURRENCY).toUpperCase();
576
+ try {
577
+ return new Intl.NumberFormat(this.locale, {
578
+ style: "currency",
579
+ currency: code,
580
+ maximumFractionDigits: options.maximumFractionDigits ?? 2
581
+ }).format(amount / minorUnitDivisor(code));
582
+ } catch {
583
+ return `${amount} ${code}`;
584
+ }
585
+ }
586
+ };
587
+
588
+ // src/presentation/components/PricingTable.tsx
589
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
590
+ var DEFAULT_LABELS3 = Object.freeze({
591
+ loading: "Loading pricing\u2026",
592
+ empty: "No products available.",
593
+ errorPrefix: "Failed to load pricing:",
594
+ retry: "Retry"
595
+ });
596
+ function PricingTable({
597
+ products: productsOverride,
598
+ apiToken,
599
+ organizationId,
600
+ isRecurring = true,
601
+ ctaLabel = "Subscribe",
602
+ onError,
603
+ filterInterval = "monthly",
604
+ locale,
605
+ labels
606
+ }) {
607
+ const enabled = productsOverride === void 0 && apiToken !== void 0;
608
+ const { products: fetchedProducts, loading, error, refetch } = useProducts({
609
+ apiToken: apiToken ?? "",
610
+ organizationId,
611
+ isRecurring,
612
+ enabled
613
+ });
614
+ const products = productsOverride ?? fetchedProducts;
615
+ const moneyFormatter = useMemo4(() => new MoneyFormatter(locale), [locale]);
616
+ const text = useMemo4(
617
+ () => ({ ...DEFAULT_LABELS3, ...labels }),
618
+ [labels]
619
+ );
620
+ const visible = useMemo4(() => {
621
+ if (filterInterval === null) return products;
622
+ return products.filter((p) => p.recurringInterval === filterInterval);
623
+ }, [products, filterInterval]);
624
+ if (enabled && loading) {
625
+ return /* @__PURE__ */ jsx4(
626
+ "div",
627
+ {
628
+ role: "status",
629
+ "aria-live": "polite",
630
+ style: {
631
+ padding: VisualTokens.spacing.section,
632
+ textAlign: "center"
633
+ },
634
+ children: text.loading
635
+ }
636
+ );
637
+ }
638
+ if (error) {
639
+ return /* @__PURE__ */ jsxs3(
640
+ "div",
641
+ {
642
+ role: "alert",
643
+ style: {
644
+ padding: VisualTokens.spacing.xl,
645
+ background: VisualTokens.color.surfaceDanger,
646
+ borderRadius: VisualTokens.radius.md
647
+ },
648
+ children: [
649
+ /* @__PURE__ */ jsxs3("p", { style: { margin: 0, color: VisualTokens.color.brandDangerText }, children: [
650
+ text.errorPrefix,
651
+ " ",
652
+ error.message
653
+ ] }),
654
+ /* @__PURE__ */ jsx4(
655
+ "button",
656
+ {
657
+ type: "button",
658
+ onClick: () => void refetch(),
659
+ style: { marginTop: VisualTokens.spacing.md, padding: "6px 12px" },
660
+ children: text.retry
661
+ }
662
+ )
663
+ ]
664
+ }
665
+ );
666
+ }
667
+ if (visible.length === 0) {
668
+ return /* @__PURE__ */ jsx4(
669
+ "div",
670
+ {
671
+ style: {
672
+ padding: VisualTokens.spacing.section,
673
+ textAlign: "center",
674
+ color: VisualTokens.color.textMuted
675
+ },
676
+ children: text.empty
677
+ }
678
+ );
679
+ }
680
+ return /* @__PURE__ */ jsx4(
681
+ "div",
682
+ {
683
+ style: {
684
+ display: "grid",
685
+ gridTemplateColumns: `repeat(auto-fit, minmax(${VisualTokens.layout.cardMinWidth}px, 1fr))`,
686
+ gap: VisualTokens.spacing.xl
687
+ },
688
+ children: visible.map((product) => /* @__PURE__ */ jsx4(
689
+ ProductCard,
690
+ {
691
+ product,
692
+ moneyFormatter,
693
+ ctaLabel,
694
+ onError
695
+ },
696
+ product.id
697
+ ))
698
+ }
699
+ );
700
+ }
701
+ function ProductCard({ product, moneyFormatter, ctaLabel, onError }) {
702
+ const { amount, interval } = useMemo4(
703
+ () => ProductPricing.displayLine(
704
+ product,
705
+ (amt, cur) => moneyFormatter.format(amt, cur)
706
+ ),
707
+ [product, moneyFormatter]
708
+ );
709
+ return /* @__PURE__ */ jsxs3(
710
+ "article",
711
+ {
712
+ style: {
713
+ border: `1px solid ${VisualTokens.color.borderSubtle}`,
714
+ borderRadius: VisualTokens.radius.lg,
715
+ padding: VisualTokens.spacing.xxl,
716
+ display: "flex",
717
+ flexDirection: "column",
718
+ gap: VisualTokens.spacing.lg
719
+ },
720
+ children: [
721
+ /* @__PURE__ */ jsxs3("header", { children: [
722
+ /* @__PURE__ */ jsx4("h3", { style: { margin: 0, fontSize: VisualTokens.fontSize.title }, children: product.name }),
723
+ product.description ? /* @__PURE__ */ jsx4(
724
+ "p",
725
+ {
726
+ style: {
727
+ margin: `${VisualTokens.spacing.xs}px 0 0`,
728
+ color: VisualTokens.color.textMuted,
729
+ fontSize: VisualTokens.fontSize.body
730
+ },
731
+ children: product.description
732
+ }
733
+ ) : null
734
+ ] }),
735
+ /* @__PURE__ */ jsxs3("div", { style: { fontSize: VisualTokens.fontSize.display, fontWeight: VisualTokens.fontWeight.strong }, children: [
736
+ amount,
737
+ interval ? /* @__PURE__ */ jsxs3(
738
+ "span",
739
+ {
740
+ style: {
741
+ fontSize: VisualTokens.fontSize.body,
742
+ fontWeight: VisualTokens.fontWeight.regular,
743
+ color: VisualTokens.color.textMuted
744
+ },
745
+ children: [
746
+ " ",
747
+ "/ ",
748
+ interval
749
+ ]
750
+ }
751
+ ) : null
752
+ ] }),
753
+ product.benefits.length > 0 ? /* @__PURE__ */ jsx4(
754
+ "ul",
755
+ {
756
+ style: {
757
+ paddingLeft: 18,
758
+ margin: 0,
759
+ color: "#374151",
760
+ fontSize: VisualTokens.fontSize.body
761
+ },
762
+ children: product.benefits.slice(0, VisualTokens.layout.benefitListMax).map((b) => /* @__PURE__ */ jsx4("li", { children: b.description ?? b.type }, b.id))
763
+ }
764
+ ) : null,
765
+ /* @__PURE__ */ jsx4("div", { style: { marginTop: "auto" }, children: /* @__PURE__ */ jsx4(
766
+ CheckoutButton,
767
+ {
768
+ productId: product.id,
769
+ billingCycle: product.recurringInterval ?? void 0,
770
+ label: ctaLabel,
771
+ onError
772
+ }
773
+ ) })
774
+ ]
775
+ }
776
+ );
777
+ }
778
+
779
+ // src/presentation/components/BillingHistoryList.tsx
780
+ import { useEffect as useEffect3, useState as useState4, useCallback as useCallback4, useMemo as useMemo5 } from "react";
781
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
782
+ var DEFAULT_LABELS4 = Object.freeze({
783
+ loading: "Loading billing history\u2026",
784
+ empty: "No invoices yet.",
785
+ refresh: "Refresh",
786
+ retry: "Retry",
787
+ paid: "Paid",
788
+ invoiceLink: "View",
789
+ invoiceFallback: "\u2014",
790
+ colDate: "Date",
791
+ colProduct: "Product",
792
+ colStatus: "Status",
793
+ colAmount: "Amount",
794
+ colInvoice: "Invoice"
795
+ });
796
+ function BillingHistoryList({
797
+ autoLoad = true,
798
+ orders: ordersOverride,
799
+ loading: loadingOverride,
800
+ locale,
801
+ labels
802
+ }) {
803
+ const { getBillingHistory } = useBilling();
804
+ const [orders, setOrders] = useState4(ordersOverride ?? []);
805
+ const [loading, setLoading] = useState4(autoLoad && ordersOverride === void 0);
806
+ const [error, setError] = useState4(null);
807
+ const moneyFormatter = useMemo5(() => new MoneyFormatter(locale), [locale]);
808
+ const dateRenderer = useMemo5(() => new DateRenderer(locale), [locale]);
809
+ const text = useMemo5(
810
+ () => ({ ...DEFAULT_LABELS4, ...labels }),
811
+ [labels]
812
+ );
813
+ const refresh = useCallback4(async () => {
814
+ setLoading(true);
815
+ setError(null);
816
+ try {
817
+ const next = await getBillingHistory();
818
+ setOrders(next);
819
+ } catch (err) {
820
+ setError(err instanceof Error ? err.message : "Failed to load billing history");
821
+ } finally {
822
+ setLoading(false);
823
+ }
824
+ }, [getBillingHistory]);
825
+ useEffect3(() => {
826
+ if (autoLoad && ordersOverride === void 0) {
827
+ void refresh();
828
+ }
829
+ }, [autoLoad, ordersOverride, refresh]);
830
+ if (ordersOverride !== void 0 && ordersOverride.length > 0) {
831
+ return renderList(ordersOverride, moneyFormatter, dateRenderer, text);
832
+ }
833
+ if (loadingOverride !== void 0 && loadingOverride) {
834
+ return /* @__PURE__ */ jsx5("div", { role: "status", children: text.loading });
835
+ }
836
+ if (loading) {
837
+ return /* @__PURE__ */ jsx5("div", { role: "status", children: text.loading });
838
+ }
839
+ if (error) {
840
+ return /* @__PURE__ */ jsxs4(
841
+ "div",
842
+ {
843
+ role: "alert",
844
+ style: {
845
+ background: VisualTokens.color.surfaceDanger,
846
+ padding: VisualTokens.spacing.lg,
847
+ borderRadius: VisualTokens.radius.md
848
+ },
849
+ children: [
850
+ error,
851
+ " ",
852
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => void refresh(), children: text.retry })
853
+ ]
854
+ }
855
+ );
856
+ }
857
+ if (orders.length === 0) {
858
+ return /* @__PURE__ */ jsx5("div", { style: { color: VisualTokens.color.textMuted }, children: text.empty });
859
+ }
860
+ return /* @__PURE__ */ jsxs4("div", { children: [
861
+ /* @__PURE__ */ jsx5("div", { style: { marginBottom: VisualTokens.spacing.md }, children: /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => void refresh(), children: text.refresh }) }),
862
+ renderList(orders, moneyFormatter, dateRenderer, text)
863
+ ] });
864
+ }
865
+ function renderList(orders, moneyFormatter, dateRenderer, text) {
866
+ return /* @__PURE__ */ jsxs4("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
867
+ /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsxs4("tr", { style: { textAlign: "left", borderBottom: `1px solid ${VisualTokens.color.borderSubtle}` }, children: [
868
+ /* @__PURE__ */ jsx5("th", { style: { padding: VisualTokens.spacing.md }, children: text.colDate }),
869
+ /* @__PURE__ */ jsx5("th", { style: { padding: VisualTokens.spacing.md }, children: text.colProduct }),
870
+ /* @__PURE__ */ jsx5("th", { style: { padding: VisualTokens.spacing.md }, children: text.colStatus }),
871
+ /* @__PURE__ */ jsx5("th", { style: { padding: VisualTokens.spacing.md }, children: text.colAmount }),
872
+ /* @__PURE__ */ jsx5("th", { style: { padding: VisualTokens.spacing.md }, children: text.colInvoice })
873
+ ] }) }),
874
+ /* @__PURE__ */ jsx5("tbody", { children: orders.map((order) => /* @__PURE__ */ jsxs4("tr", { style: { borderBottom: `1px solid ${VisualTokens.color.surfaceSubtle}` }, children: [
875
+ /* @__PURE__ */ jsx5("td", { style: { padding: VisualTokens.spacing.md }, children: dateRenderer.short(order.createdAt) }),
876
+ /* @__PURE__ */ jsx5("td", { style: { padding: VisualTokens.spacing.md }, children: order.productName }),
877
+ /* @__PURE__ */ jsx5("td", { style: { padding: VisualTokens.spacing.md }, children: order.paid ? text.paid : order.status }),
878
+ /* @__PURE__ */ jsx5("td", { style: { padding: VisualTokens.spacing.md }, children: moneyFormatter.format(order.amount, order.currency) }),
879
+ /* @__PURE__ */ jsx5("td", { style: { padding: VisualTokens.spacing.md }, children: order.invoiceUrl ? /* @__PURE__ */ jsx5("a", { href: order.invoiceUrl, target: "_blank", rel: "noreferrer", children: text.invoiceLink }) : text.invoiceFallback })
880
+ ] }, order.id)) })
881
+ ] });
882
+ }
883
+
884
+ // src/presentation/components/CustomerPortalLink.tsx
885
+ import { useState as useState5 } from "react";
886
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
887
+ var DEFAULT_LABEL = "Manage subscription";
888
+ var DEFAULT_LOADING_LABEL = "Opening\u2026";
889
+ function CustomerPortalLink({
890
+ label = DEFAULT_LABEL,
891
+ target = "open",
892
+ className,
893
+ labels
894
+ }) {
895
+ const { getPortalUrl } = useBilling();
896
+ const [loading, setLoading] = useState5(false);
897
+ const [error, setError] = useState5(null);
898
+ const loadingLabel = labels?.loading ?? DEFAULT_LOADING_LABEL;
899
+ const open = async () => {
900
+ setLoading(true);
901
+ setError(null);
902
+ try {
903
+ const url = await getPortalUrl();
904
+ if (target === "open") {
905
+ window.open(url, "_blank", "noopener,noreferrer");
906
+ } else {
907
+ window.location.href = url;
908
+ }
909
+ } catch (err) {
910
+ setError(err instanceof Error ? err.message : "Failed to open portal");
911
+ } finally {
912
+ setLoading(false);
913
+ }
914
+ };
915
+ return /* @__PURE__ */ jsxs5("span", { style: { display: "inline-flex", flexDirection: "column", gap: VisualTokens.spacing.xs }, children: [
916
+ /* @__PURE__ */ jsx6(
917
+ "button",
918
+ {
919
+ type: "button",
920
+ onClick: () => void open(),
921
+ disabled: loading,
922
+ className,
923
+ style: {
924
+ background: "transparent",
925
+ color: VisualTokens.color.brandPrimary,
926
+ border: "none",
927
+ padding: 0,
928
+ textDecoration: "underline",
929
+ cursor: loading ? "wait" : "pointer"
930
+ },
931
+ children: loading ? loadingLabel : label
932
+ }
933
+ ),
934
+ InlineErrorVisual.element(error)
935
+ ] });
936
+ }
937
+
938
+ // src/presentation/components/CancelSubscriptionButton.tsx
939
+ import { useState as useState7 } from "react";
940
+
941
+ // src/presentation/hooks/useCancelSubscription.ts
942
+ import { useCallback as useCallback5, useState as useState6 } from "react";
943
+ function useCancelSubscription(options = {}) {
944
+ const { cancelSubscription } = useBilling();
945
+ const [loading, setLoading] = useState6(false);
946
+ const [error, setError] = useState6(null);
947
+ const { onSuccess, onError } = options;
948
+ const cancel = useCallback5(
949
+ async (reason) => {
950
+ setLoading(true);
951
+ setError(null);
952
+ try {
953
+ const result = await cancelSubscription(reason);
954
+ onSuccess?.(result);
955
+ return result;
956
+ } catch (err) {
957
+ const normalized = err instanceof Error ? err : new Error(String(err));
958
+ setError(normalized);
959
+ onError?.(normalized);
960
+ return null;
961
+ } finally {
962
+ setLoading(false);
963
+ }
964
+ },
965
+ [cancelSubscription, onSuccess, onError]
966
+ );
967
+ return { cancel, loading, error };
968
+ }
969
+
970
+ // src/presentation/utils/cancellation-reason-catalog.ts
971
+ var CANCELLATION_REASON_CATALOG = Object.freeze([
972
+ { value: "too_expensive", label: "Too expensive" },
973
+ { value: "missing_features", label: "Missing features" },
974
+ { value: "switched_service", label: "Switched to a different service" },
975
+ { value: "unused", label: "Not using it" },
976
+ { value: "customer_service", label: "Poor customer service" },
977
+ { value: "low_quality", label: "Quality is too low" },
978
+ { value: "too_complex", label: "Too complex" },
979
+ { value: "other", label: "Other" }
980
+ ]);
981
+
982
+ // src/presentation/components/CancelSubscriptionButton.tsx
983
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
984
+ var DEFAULT_LABELS5 = Object.freeze({
985
+ action: "Cancel subscription",
986
+ confirm: "Confirm cancel",
987
+ keep: "Keep subscription",
988
+ canceling: "Canceling\u2026",
989
+ promptTitle: "Why are you canceling? (optional)",
990
+ noReason: "Prefer not to say"
991
+ });
992
+ function CancelSubscriptionButton({
993
+ label,
994
+ renderPrompt,
995
+ onSuccess,
996
+ reasonCatalog = CANCELLATION_REASON_CATALOG,
997
+ labels
998
+ }) {
999
+ const { cancel, loading, error } = useCancelSubscription({
1000
+ onSuccess: () => onSuccess?.()
1001
+ });
1002
+ const [promptOpen, setPromptOpen] = useState7(false);
1003
+ const [reason, setReason] = useState7("");
1004
+ const text = { ...DEFAULT_LABELS5, ...labels };
1005
+ const triggerLabel = label ?? text.action;
1006
+ const onConfirm = async (selected) => {
1007
+ setPromptOpen(false);
1008
+ const result = await cancel(selected);
1009
+ if (result?.success) {
1010
+ setReason("");
1011
+ }
1012
+ };
1013
+ if (!promptOpen) {
1014
+ return /* @__PURE__ */ jsxs6("span", { style: { display: "inline-flex", flexDirection: "column", gap: VisualTokens.spacing.xs }, children: [
1015
+ /* @__PURE__ */ jsx7(
1016
+ "button",
1017
+ {
1018
+ type: "button",
1019
+ onClick: () => setPromptOpen(true),
1020
+ style: {
1021
+ background: "transparent",
1022
+ color: VisualTokens.color.brandDangerText,
1023
+ border: `1px solid ${VisualTokens.color.borderWarning}`,
1024
+ borderRadius: VisualTokens.radius.md,
1025
+ padding: `${VisualTokens.spacing.md}px ${VisualTokens.spacing.lg}px`,
1026
+ cursor: "pointer",
1027
+ fontWeight: VisualTokens.fontWeight.strong
1028
+ },
1029
+ children: triggerLabel
1030
+ }
1031
+ ),
1032
+ InlineErrorVisual.element(error?.message)
1033
+ ] });
1034
+ }
1035
+ if (renderPrompt) {
1036
+ return /* @__PURE__ */ jsx7(Fragment, { children: renderPrompt((selected) => void onConfirm(selected)) });
1037
+ }
1038
+ return /* @__PURE__ */ jsxs6(
1039
+ "div",
1040
+ {
1041
+ role: "dialog",
1042
+ "aria-label": "Confirm cancellation",
1043
+ style: {
1044
+ border: `1px solid ${VisualTokens.color.borderWarning}`,
1045
+ background: VisualTokens.color.surfaceDangerMuted,
1046
+ borderRadius: VisualTokens.radius.md,
1047
+ padding: VisualTokens.spacing.lg,
1048
+ display: "flex",
1049
+ flexDirection: "column",
1050
+ gap: VisualTokens.spacing.md
1051
+ },
1052
+ children: [
1053
+ /* @__PURE__ */ jsxs6("label", { style: { fontSize: VisualTokens.fontSize.body, fontWeight: VisualTokens.fontWeight.strong }, children: [
1054
+ text.promptTitle,
1055
+ /* @__PURE__ */ jsxs6(
1056
+ "select",
1057
+ {
1058
+ value: reason,
1059
+ onChange: (e) => setReason(e.target.value),
1060
+ style: {
1061
+ display: "block",
1062
+ marginTop: VisualTokens.spacing.xs,
1063
+ width: "100%",
1064
+ padding: VisualTokens.spacing.sm
1065
+ },
1066
+ children: [
1067
+ /* @__PURE__ */ jsx7("option", { value: "", children: text.noReason }),
1068
+ reasonCatalog.map((opt) => /* @__PURE__ */ jsx7("option", { value: opt.value, children: opt.label }, opt.value))
1069
+ ]
1070
+ }
1071
+ )
1072
+ ] }),
1073
+ /* @__PURE__ */ jsxs6("div", { style: { display: "flex", gap: VisualTokens.spacing.md }, children: [
1074
+ /* @__PURE__ */ jsx7(
1075
+ "button",
1076
+ {
1077
+ type: "button",
1078
+ onClick: () => void onConfirm(reason || void 0),
1079
+ disabled: loading,
1080
+ style: {
1081
+ background: VisualTokens.color.brandDanger,
1082
+ color: VisualTokens.color.textOnPrimary,
1083
+ border: "none",
1084
+ padding: `${VisualTokens.spacing.sm}px ${VisualTokens.spacing.lg}px`,
1085
+ borderRadius: VisualTokens.radius.sm
1086
+ },
1087
+ children: loading ? text.canceling : text.confirm
1088
+ }
1089
+ ),
1090
+ /* @__PURE__ */ jsx7(
1091
+ "button",
1092
+ {
1093
+ type: "button",
1094
+ onClick: () => setPromptOpen(false),
1095
+ disabled: loading,
1096
+ style: {
1097
+ background: "transparent",
1098
+ border: `1px solid ${VisualTokens.color.borderStrong}`,
1099
+ padding: `${VisualTokens.spacing.sm}px ${VisualTokens.spacing.lg}px`,
1100
+ borderRadius: VisualTokens.radius.sm
1101
+ },
1102
+ children: text.keep
1103
+ }
1104
+ )
1105
+ ] }),
1106
+ InlineErrorVisual.element(error?.message)
1107
+ ]
1108
+ }
1109
+ );
1110
+ }
1111
+
1112
+ export {
1113
+ isInsecureUrl,
1114
+ isLoopbackHost,
1115
+ BillingContext,
1116
+ useBilling,
1117
+ BillingProvider,
1118
+ useCheckoutButton,
1119
+ CheckoutButton,
1120
+ SubscriptionStatusBadge,
1121
+ useProducts,
1122
+ PricingTable,
1123
+ BillingHistoryList,
1124
+ CustomerPortalLink,
1125
+ useCancelSubscription,
1126
+ CANCELLATION_REASON_CATALOG,
1127
+ CancelSubscriptionButton
1128
+ };