@startsimpli/billing 0.1.4 → 0.1.5

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.
package/dist/index.js DELETED
@@ -1,959 +0,0 @@
1
- 'use strict';
2
-
3
- var react = require('react');
4
- var jsxRuntime = require('react/jsx-runtime');
5
-
6
- // src/utils/api.ts
7
- var BillingApiClient = class {
8
- constructor(config) {
9
- this.config = config;
10
- }
11
- get fetcher() {
12
- return this.config.fetcher ?? fetch.bind(globalThis);
13
- }
14
- get headers() {
15
- return { "Content-Type": "application/json" };
16
- }
17
- /** Fetch a public billing product by slug. */
18
- async getProduct(slug) {
19
- const url = `${this.config.apiBaseUrl}/billing/products/${slug}/`;
20
- const resp = await this.fetcher(url, {
21
- headers: this.headers
22
- });
23
- if (!resp.ok) {
24
- throw new Error(`Failed to fetch product "${slug}": ${resp.status}`);
25
- }
26
- return resp.json();
27
- }
28
- /** Create a checkout session for an offer. */
29
- async createCheckout(params) {
30
- const url = `${this.config.apiBaseUrl}/billing/offer-checkout/`;
31
- const resp = await this.fetcher(url, {
32
- method: "POST",
33
- headers: this.headers,
34
- body: JSON.stringify({
35
- offer_id: params.offerId,
36
- success_url: params.successUrl,
37
- cancel_url: params.cancelUrl,
38
- quantity: params.quantity ?? 1
39
- })
40
- });
41
- if (!resp.ok) {
42
- const data = await resp.json().catch(() => ({}));
43
- throw new Error(
44
- data.detail ?? `Checkout failed: ${resp.status}`
45
- );
46
- }
47
- return resp.json();
48
- }
49
- /** Subscribe to a free offer (no billing provider). */
50
- async subscribeFree(offerId) {
51
- const url = `${this.config.apiBaseUrl}/billing/subscribe-free/`;
52
- const resp = await this.fetcher(url, {
53
- method: "POST",
54
- headers: this.headers,
55
- body: JSON.stringify({ offer_id: offerId })
56
- });
57
- if (!resp.ok) {
58
- const data = await resp.json().catch(() => ({}));
59
- throw new Error(data.detail ?? `Free signup failed: ${resp.status}`);
60
- }
61
- return resp.json();
62
- }
63
- /** Create a customer portal session. */
64
- async createPortal(returnUrl) {
65
- const url = `${this.config.apiBaseUrl}/billing/offer-portal/`;
66
- const resp = await this.fetcher(url, {
67
- method: "POST",
68
- headers: this.headers,
69
- body: JSON.stringify({ return_url: returnUrl })
70
- });
71
- if (!resp.ok) {
72
- const data = await resp.json().catch(() => ({}));
73
- throw new Error(
74
- data.detail ?? `Portal session failed: ${resp.status}`
75
- );
76
- }
77
- return resp.json();
78
- }
79
- /**
80
- * Eagerly sync subscription state after Stripe checkout redirect.
81
- * Call this from your success_url page to avoid waiting for the webhook.
82
- */
83
- async successSync() {
84
- const url = `${this.config.apiBaseUrl}/billing/success-sync/`;
85
- const resp = await this.fetcher(url, {
86
- method: "POST",
87
- headers: this.headers
88
- });
89
- if (resp.status === 404) {
90
- return null;
91
- }
92
- if (!resp.ok) {
93
- const data = await resp.json().catch(() => ({}));
94
- throw new Error(data.detail ?? `Success sync failed: ${resp.status}`);
95
- }
96
- return resp.json();
97
- }
98
- /** Get current user's subscription. */
99
- async getSubscription() {
100
- const url = `${this.config.apiBaseUrl}/billing/subscription/current/`;
101
- const resp = await this.fetcher(url, {
102
- headers: this.headers
103
- });
104
- if (resp.status === 404) {
105
- return null;
106
- }
107
- if (!resp.ok) {
108
- const data = await resp.json().catch(() => ({}));
109
- throw new Error(
110
- data.detail ?? `Failed to fetch subscription: ${resp.status}`
111
- );
112
- }
113
- return resp.json();
114
- }
115
- };
116
- var BillingContext = react.createContext(null);
117
- function BillingProvider({
118
- children,
119
- ...config
120
- }) {
121
- const value = react.useMemo(() => {
122
- const client = new BillingApiClient(config);
123
- return { client, config };
124
- }, [config.apiBaseUrl]);
125
- return /* @__PURE__ */ jsxRuntime.jsx(BillingContext.Provider, { value, children });
126
- }
127
- function useBillingContext() {
128
- const ctx = react.useContext(BillingContext);
129
- if (!ctx) {
130
- throw new Error(
131
- "useBillingContext must be used within a <BillingProvider>"
132
- );
133
- }
134
- return ctx;
135
- }
136
- function useProduct(slug) {
137
- const { client } = useBillingContext();
138
- const [product, setProduct] = react.useState(null);
139
- const [loading, setLoading] = react.useState(true);
140
- const [error, setError] = react.useState(null);
141
- const fetchProduct = react.useCallback(async () => {
142
- setLoading(true);
143
- setError(null);
144
- try {
145
- const data = await client.getProduct(slug);
146
- setProduct(data);
147
- } catch (e) {
148
- setError(e instanceof Error ? e : new Error(String(e)));
149
- } finally {
150
- setLoading(false);
151
- }
152
- }, [client, slug]);
153
- react.useEffect(() => {
154
- fetchProduct();
155
- }, [fetchProduct]);
156
- return { product, loading, error, refetch: fetchProduct };
157
- }
158
- function useCheckout() {
159
- const { client } = useBillingContext();
160
- const [loading, setLoading] = react.useState(false);
161
- const [error, setError] = react.useState(null);
162
- const checkout = react.useCallback(
163
- async (params) => {
164
- setLoading(true);
165
- setError(null);
166
- try {
167
- const result = await client.createCheckout(params);
168
- return result;
169
- } catch (e) {
170
- const err = e instanceof Error ? e : new Error(String(e));
171
- setError(err);
172
- throw err;
173
- } finally {
174
- setLoading(false);
175
- }
176
- },
177
- [client]
178
- );
179
- const subscribeFree = react.useCallback(
180
- async (offerId) => {
181
- setLoading(true);
182
- setError(null);
183
- try {
184
- const result = await client.subscribeFree(offerId);
185
- return result;
186
- } catch (e) {
187
- const err = e instanceof Error ? e : new Error(String(e));
188
- setError(err);
189
- throw err;
190
- } finally {
191
- setLoading(false);
192
- }
193
- },
194
- [client]
195
- );
196
- return { checkout, subscribeFree, loading, error };
197
- }
198
- function usePortal() {
199
- const { client } = useBillingContext();
200
- const [loading, setLoading] = react.useState(false);
201
- const [error, setError] = react.useState(null);
202
- const openPortal = react.useCallback(
203
- async (returnUrl) => {
204
- setLoading(true);
205
- setError(null);
206
- try {
207
- const result = await client.createPortal(returnUrl);
208
- return result;
209
- } catch (e) {
210
- const err = e instanceof Error ? e : new Error(String(e));
211
- setError(err);
212
- throw err;
213
- } finally {
214
- setLoading(false);
215
- }
216
- },
217
- [client]
218
- );
219
- return { openPortal, loading, error };
220
- }
221
- function useSubscription() {
222
- const { client } = useBillingContext();
223
- const [subscription, setSubscription] = react.useState(null);
224
- const [loading, setLoading] = react.useState(true);
225
- const [error, setError] = react.useState(null);
226
- const fetchSubscription = react.useCallback(async () => {
227
- setLoading(true);
228
- setError(null);
229
- try {
230
- const data = await client.getSubscription();
231
- setSubscription(data);
232
- } catch (e) {
233
- setError(e instanceof Error ? e : new Error(String(e)));
234
- } finally {
235
- setLoading(false);
236
- }
237
- }, [client]);
238
- react.useEffect(() => {
239
- fetchSubscription();
240
- }, [fetchSubscription]);
241
- return { subscription, loading, error, refetch: fetchSubscription };
242
- }
243
- function useSuccessSync() {
244
- const { client } = useBillingContext();
245
- const [data, setData] = react.useState(null);
246
- const [loading, setLoading] = react.useState(false);
247
- const [error, setError] = react.useState(null);
248
- const sync = react.useCallback(async () => {
249
- setLoading(true);
250
- setError(null);
251
- try {
252
- const result = await client.successSync();
253
- setData(result);
254
- return result;
255
- } catch (e) {
256
- const err = e instanceof Error ? e : new Error(String(e));
257
- setError(err);
258
- throw err;
259
- } finally {
260
- setLoading(false);
261
- }
262
- }, [client]);
263
- return { sync, data, loading, error };
264
- }
265
- function PricingPage({ productId, onSelectOffer }) {
266
- const { product, loading, error } = useProduct(productId);
267
- if (loading) {
268
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "pricing-loading", className: "text-center py-16 text-gray-500", children: "Loading pricing..." });
269
- }
270
- if (error || !product) {
271
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "pricing-error", className: "text-center py-16 text-red-600", children: "Unable to load pricing. Please try again." });
272
- }
273
- if (product.offers.length === 0) {
274
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-16 text-gray-500", children: "No pricing plans available at this time." });
275
- }
276
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "pricing-page", className: "max-w-5xl mx-auto px-6 py-12", children: [
277
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center mb-12", children: [
278
- /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-4xl font-extrabold tracking-tight text-gray-900 mb-3", children: product.name }),
279
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-lg text-gray-500 max-w-xl mx-auto", children: product.description })
280
- ] }),
281
- /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "pricing-cards", className: "flex flex-wrap justify-center gap-6 items-stretch", children: product.offers.map((offer) => /* @__PURE__ */ jsxRuntime.jsx(
282
- PricingCard,
283
- {
284
- offer,
285
- onSelect: onSelectOffer
286
- },
287
- offer.id
288
- )) })
289
- ] });
290
- }
291
- function PricingCard({ offer, onSelect }) {
292
- const isFeatured = offer.is_featured;
293
- return /* @__PURE__ */ jsxRuntime.jsxs(
294
- "div",
295
- {
296
- "data-testid": `pricing-card-${offer.slug}`,
297
- "data-featured": isFeatured || void 0,
298
- className: `
299
- flex flex-col relative rounded-2xl p-8 w-full max-w-sm flex-1 min-w-[280px]
300
- ${isFeatured ? "border-2 border-blue-600 shadow-lg scale-[1.03]" : "border border-gray-200 bg-white shadow-sm"}
301
- `,
302
- children: [
303
- isFeatured && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -top-3.5 left-1/2 -translate-x-1/2 bg-blue-600 text-white text-xs font-bold px-4 py-1 rounded-full uppercase tracking-wide whitespace-nowrap", children: "Most Popular" }),
304
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-bold text-gray-900 mb-4", children: offer.name }),
305
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2", children: offer.is_free ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-4xl font-extrabold text-gray-900", children: "Free" }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
306
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-4xl font-extrabold text-gray-900", children: [
307
- "$",
308
- Math.floor(parseFloat(offer.unit_price) || 0)
309
- ] }),
310
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-base text-gray-500 ml-1", children: [
311
- " ",
312
- offer.interval_display
313
- ] })
314
- ] }) }),
315
- offer.trial_days > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
316
- "span",
317
- {
318
- "data-testid": "trial-badge",
319
- className: "inline-block bg-blue-50 text-blue-700 text-sm font-semibold px-3 py-1 rounded-lg mb-5 w-fit",
320
- children: [
321
- offer.trial_days,
322
- "-day free trial"
323
- ]
324
- }
325
- ),
326
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "list-none p-0 m-0 mb-7 flex-1 space-y-0", children: offer.features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs(
327
- "li",
328
- {
329
- className: "flex items-center gap-2.5 py-2.5 text-sm text-gray-600 border-b border-gray-100 last:border-0",
330
- children: [
331
- /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4 text-green-500 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }),
332
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: feature.name })
333
- ]
334
- },
335
- feature.key
336
- )) }),
337
- /* @__PURE__ */ jsxRuntime.jsx(
338
- "button",
339
- {
340
- onClick: () => onSelect?.(offer),
341
- "data-testid": `cta-${offer.slug}`,
342
- className: `
343
- w-full py-3.5 px-6 text-base font-bold rounded-xl cursor-pointer transition-colors
344
- ${isFeatured ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-gray-100 text-gray-800 hover:bg-gray-200"}
345
- `,
346
- children: offer.cta_text
347
- }
348
- )
349
- ]
350
- }
351
- );
352
- }
353
- function PricingSection({
354
- productId,
355
- onSelectOffer,
356
- maxFeatures = 4
357
- }) {
358
- const { product, loading, error } = useProduct(productId);
359
- if (loading) {
360
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-10 text-gray-500", children: "Loading pricing..." });
361
- }
362
- if (error || !product) {
363
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-10 text-red-600", children: "Unable to load pricing. Please try again." });
364
- }
365
- if (product.offers.length === 0) {
366
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-16 text-gray-500", children: "No pricing plans available at this time." });
367
- }
368
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "pricing-section", className: "w-full px-4 py-8", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-row flex-wrap justify-center gap-4 items-stretch", children: product.offers.map((offer) => /* @__PURE__ */ jsxRuntime.jsx(
369
- SectionCard,
370
- {
371
- offer,
372
- onSelect: onSelectOffer,
373
- maxFeatures
374
- },
375
- offer.id
376
- )) }) });
377
- }
378
- function SectionCard({ offer, onSelect, maxFeatures }) {
379
- const isFeatured = offer.is_featured;
380
- const visibleFeatures = offer.features.slice(0, maxFeatures);
381
- return /* @__PURE__ */ jsxRuntime.jsxs(
382
- "div",
383
- {
384
- "data-testid": `section-card-${offer.slug}`,
385
- "data-featured": isFeatured ? "true" : void 0,
386
- className: `
387
- flex flex-col relative rounded-xl p-6 flex-1 min-w-[220px] max-w-xs
388
- ${isFeatured ? "border-2 border-blue-600 shadow-md" : "border border-gray-200 bg-white shadow-sm"}
389
- `,
390
- children: [
391
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-bold text-gray-900 mb-2", children: offer.name }),
392
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2", children: offer.is_free ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-2xl font-extrabold text-gray-900", children: "Free" }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
393
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-2xl font-extrabold text-gray-900", children: [
394
- "$",
395
- Math.floor(parseFloat(offer.unit_price) || 0)
396
- ] }),
397
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-500 ml-1", children: [
398
- " ",
399
- offer.interval_display
400
- ] })
401
- ] }) }),
402
- offer.trial_days > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
403
- "span",
404
- {
405
- "data-testid": "trial-badge",
406
- className: "inline-block bg-blue-50 text-blue-700 text-xs font-semibold px-2 py-0.5 rounded-md mb-3 w-fit",
407
- children: [
408
- offer.trial_days,
409
- "-day free trial"
410
- ]
411
- }
412
- ),
413
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "list-none p-0 m-0 mb-4 flex-1 space-y-0", children: visibleFeatures.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs(
414
- "li",
415
- {
416
- className: "flex items-center gap-2 py-1.5 text-sm text-gray-600 border-b border-gray-100 last:border-0",
417
- children: [
418
- /* @__PURE__ */ jsxRuntime.jsx(
419
- "svg",
420
- {
421
- className: "w-3.5 h-3.5 text-green-500 shrink-0",
422
- fill: "none",
423
- viewBox: "0 0 24 24",
424
- stroke: "currentColor",
425
- strokeWidth: 3,
426
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" })
427
- }
428
- ),
429
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: feature.name })
430
- ]
431
- },
432
- feature.key
433
- )) }),
434
- /* @__PURE__ */ jsxRuntime.jsx(
435
- "button",
436
- {
437
- onClick: () => onSelect?.(offer),
438
- "data-testid": `section-cta-${offer.slug}`,
439
- className: `
440
- w-full py-2.5 px-4 text-sm font-bold rounded-lg cursor-pointer transition-colors
441
- ${isFeatured ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-gray-100 text-gray-800 hover:bg-gray-200"}
442
- `,
443
- children: offer.cta_text
444
- }
445
- )
446
- ]
447
- }
448
- );
449
- }
450
- function UpgradeModal({
451
- productId,
452
- open,
453
- onClose,
454
- onSelectOffer
455
- }) {
456
- const { product, loading, error } = useProduct(productId);
457
- const modalRef = react.useRef(null);
458
- const previousFocusRef = react.useRef(null);
459
- const handleKeyDown = react.useCallback(
460
- (e) => {
461
- if (e.key === "Escape") {
462
- onClose();
463
- return;
464
- }
465
- if (e.key === "Tab" && modalRef.current) {
466
- const focusable = modalRef.current.querySelectorAll(
467
- 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
468
- );
469
- if (focusable.length === 0) return;
470
- const first = focusable[0];
471
- const last = focusable[focusable.length - 1];
472
- if (e.shiftKey) {
473
- if (document.activeElement === first) {
474
- e.preventDefault();
475
- last.focus();
476
- }
477
- } else {
478
- if (document.activeElement === last) {
479
- e.preventDefault();
480
- first.focus();
481
- }
482
- }
483
- }
484
- },
485
- [onClose]
486
- );
487
- react.useEffect(() => {
488
- if (open) {
489
- previousFocusRef.current = document.activeElement;
490
- const timer = setTimeout(() => {
491
- if (modalRef.current) {
492
- const focusable = modalRef.current.querySelector(
493
- 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
494
- );
495
- focusable?.focus();
496
- }
497
- }, 0);
498
- return () => clearTimeout(timer);
499
- } else {
500
- previousFocusRef.current?.focus();
501
- }
502
- }, [open]);
503
- if (!open) {
504
- return null;
505
- }
506
- return /* @__PURE__ */ jsxRuntime.jsx(
507
- "div",
508
- {
509
- "data-testid": "upgrade-modal",
510
- role: "dialog",
511
- "aria-modal": "true",
512
- "aria-labelledby": "upgrade-modal-title",
513
- onKeyDown: handleKeyDown,
514
- ref: modalRef,
515
- className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-6",
516
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white rounded-2xl max-w-3xl w-full max-h-[90vh] overflow-auto p-8 relative", children: [
517
- /* @__PURE__ */ jsxRuntime.jsx(
518
- "button",
519
- {
520
- onClick: onClose,
521
- "aria-label": "Close",
522
- className: "absolute top-4 right-4 text-gray-400 hover:text-gray-600 text-2xl leading-none p-1 cursor-pointer",
523
- children: "\xD7"
524
- }
525
- ),
526
- /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "upgrade-modal-title", className: "text-2xl font-extrabold text-gray-900 text-center mb-8", children: "Choose a plan" }),
527
- loading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-8 text-gray-500", children: "Loading..." }),
528
- error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-8 text-red-600", children: "Unable to load pricing. Please try again." }),
529
- product && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "upgrade-offers", className: "flex flex-wrap justify-center gap-5", children: product.offers.map((offer) => {
530
- const isFeatured = offer.is_featured;
531
- return /* @__PURE__ */ jsxRuntime.jsxs(
532
- "div",
533
- {
534
- "data-testid": `upgrade-offer-${offer.slug}`,
535
- className: `
536
- flex flex-col flex-1 min-w-[200px] max-w-[240px] rounded-xl p-6
537
- ${isFeatured ? "border-2 border-blue-600 shadow-md" : "border border-gray-200"}
538
- `,
539
- children: [
540
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-bold text-gray-900 mb-2", children: offer.name }),
541
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2", children: offer.is_free ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-label": "Free", children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "text-2xl font-extrabold text-gray-900", children: "Free" }) }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-label": `${Math.floor(parseFloat(offer.unit_price) || 0)} dollars ${offer.interval_display}`, children: [
542
- /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-hidden": "true", className: "text-2xl font-extrabold text-gray-900", children: [
543
- "$",
544
- Math.floor(parseFloat(offer.unit_price) || 0)
545
- ] }),
546
- /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-hidden": "true", className: "text-sm text-gray-500", children: [
547
- " ",
548
- offer.interval_display
549
- ] })
550
- ] }) }),
551
- offer.trial_days > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-block bg-blue-50 text-blue-700 text-xs font-semibold px-2.5 py-0.5 rounded-md mb-3 w-fit", children: [
552
- offer.trial_days,
553
- "-day free trial"
554
- ] }),
555
- offer.features.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "list-none p-0 m-0 mb-4 flex-1 space-y-0", children: offer.features.map((f) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex items-center gap-1.5 py-1 text-sm text-gray-600", children: [
556
- /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-3.5 h-3.5 text-green-500 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }),
557
- f.name
558
- ] }, f.key)) }),
559
- /* @__PURE__ */ jsxRuntime.jsx(
560
- "button",
561
- {
562
- onClick: () => onSelectOffer?.(offer),
563
- "data-testid": `upgrade-cta-${offer.slug}`,
564
- className: `
565
- w-full py-2.5 px-4 text-sm font-bold rounded-lg cursor-pointer transition-colors
566
- ${isFeatured ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-gray-100 text-gray-800 hover:bg-gray-200"}
567
- `,
568
- children: offer.cta_text
569
- }
570
- )
571
- ]
572
- },
573
- offer.id
574
- );
575
- }) })
576
- ] })
577
- }
578
- );
579
- }
580
- function PricingDetailPage({
581
- productId,
582
- onSelectOffer,
583
- showComparison = true
584
- }) {
585
- const { product, loading, error } = useProduct(productId);
586
- if (loading) {
587
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-16 text-gray-500", children: "Loading pricing..." });
588
- }
589
- if (error || !product) {
590
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-16 text-red-600", children: "Unable to load pricing. Please try again." });
591
- }
592
- if (product.offers.length === 0) {
593
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-16 text-gray-500", children: "No pricing plans available at this time." });
594
- }
595
- const allFeatureKeys = [];
596
- const featureNameMap = {};
597
- for (const offer of product.offers) {
598
- for (const feature of offer.features) {
599
- if (!allFeatureKeys.includes(feature.key)) {
600
- allFeatureKeys.push(feature.key);
601
- featureNameMap[feature.key] = feature.name;
602
- }
603
- }
604
- }
605
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "pricing-detail", className: "max-w-6xl mx-auto px-6 py-12", children: [
606
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center mb-12", children: [
607
- /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-4xl font-extrabold tracking-tight text-gray-900 mb-3", children: product.name }),
608
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-lg text-gray-500 max-w-xl mx-auto", children: product.description })
609
- ] }),
610
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap justify-center gap-6 items-stretch mb-16", children: product.offers.map((offer) => /* @__PURE__ */ jsxRuntime.jsx(
611
- DetailCard,
612
- {
613
- offer,
614
- onSelect: onSelectOffer
615
- },
616
- offer.id
617
- )) }),
618
- showComparison && /* @__PURE__ */ jsxRuntime.jsx(
619
- ComparisonTable,
620
- {
621
- offers: product.offers,
622
- featureKeys: allFeatureKeys,
623
- featureNameMap
624
- }
625
- )
626
- ] });
627
- }
628
- function DetailCard({ offer, onSelect }) {
629
- const isFeatured = offer.is_featured;
630
- return /* @__PURE__ */ jsxRuntime.jsxs(
631
- "div",
632
- {
633
- "data-testid": `detail-card-${offer.slug}`,
634
- className: `
635
- flex flex-col relative rounded-2xl p-8 w-full max-w-sm flex-1 min-w-[280px]
636
- ${isFeatured ? "border-2 border-blue-600 shadow-lg scale-[1.03]" : "border border-gray-200 bg-white shadow-sm"}
637
- `,
638
- children: [
639
- isFeatured && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -top-3.5 left-1/2 -translate-x-1/2 bg-blue-600 text-white text-xs font-bold px-4 py-1 rounded-full uppercase tracking-wide whitespace-nowrap", children: "Most Popular" }),
640
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-bold text-gray-900 mb-4", children: offer.name }),
641
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2", children: offer.is_free ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-label": "Free", children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "text-4xl font-extrabold text-gray-900", children: "Free" }) }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-label": `${Math.floor(parseFloat(offer.unit_price) || 0)} dollars ${offer.interval_display}`, children: [
642
- /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-hidden": "true", className: "text-4xl font-extrabold text-gray-900", children: [
643
- "$",
644
- Math.floor(parseFloat(offer.unit_price) || 0)
645
- ] }),
646
- /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-hidden": "true", className: "text-base text-gray-500 ml-1", children: [
647
- " ",
648
- offer.interval_display
649
- ] })
650
- ] }) }),
651
- offer.trial_days > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-block bg-blue-50 text-blue-700 text-sm font-semibold px-3 py-1 rounded-lg mb-5 w-fit", children: [
652
- offer.trial_days,
653
- "-day free trial"
654
- ] }),
655
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "list-none p-0 m-0 mb-7 flex-1 space-y-0", children: offer.features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs(
656
- "li",
657
- {
658
- className: "flex items-center gap-2.5 py-2.5 text-sm text-gray-600 border-b border-gray-100 last:border-0",
659
- children: [
660
- /* @__PURE__ */ jsxRuntime.jsx(FeatureIcon, { value: feature.value }),
661
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: feature.name }),
662
- typeof feature.value !== "boolean" && feature.value !== null && feature.value !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto text-gray-900 font-medium", children: feature.value === -1 ? "Unlimited" : String(feature.value) })
663
- ]
664
- },
665
- feature.key
666
- )) }),
667
- /* @__PURE__ */ jsxRuntime.jsx(
668
- "button",
669
- {
670
- onClick: () => onSelect?.(offer),
671
- "data-testid": `detail-cta-${offer.slug}`,
672
- className: `
673
- w-full py-3.5 px-6 text-base font-bold rounded-xl cursor-pointer transition-colors
674
- ${isFeatured ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-gray-100 text-gray-800 hover:bg-gray-200"}
675
- `,
676
- children: offer.cta_text
677
- }
678
- )
679
- ]
680
- }
681
- );
682
- }
683
- function FeatureIcon({ value }) {
684
- if (value === false) {
685
- return /* @__PURE__ */ jsxRuntime.jsx("svg", { role: "img", "aria-label": "Not included", className: "w-4 h-4 text-gray-300 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) });
686
- }
687
- return /* @__PURE__ */ jsxRuntime.jsx("svg", { role: "img", "aria-label": "Included", className: "w-4 h-4 text-green-500 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) });
688
- }
689
- function ComparisonTable({ offers, featureKeys, featureNameMap }) {
690
- const featureLookup = {};
691
- for (const offer of offers) {
692
- featureLookup[offer.slug] = {};
693
- for (const feature of offer.features) {
694
- featureLookup[offer.slug][feature.key] = feature;
695
- }
696
- }
697
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "comparison-table", className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse", children: [
698
- /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "border-b-2 border-gray-200", children: [
699
- /* @__PURE__ */ jsxRuntime.jsx("th", { scope: "col", className: "text-left py-4 px-4 text-sm font-semibold text-gray-500 uppercase tracking-wider", children: "Feature" }),
700
- offers.map((offer) => /* @__PURE__ */ jsxRuntime.jsx(
701
- "th",
702
- {
703
- scope: "col",
704
- "aria-label": offer.name,
705
- className: "text-center py-4 px-4 text-sm font-semibold text-gray-900",
706
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: offer.name })
707
- },
708
- offer.slug
709
- ))
710
- ] }) }),
711
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: featureKeys.map((key) => /* @__PURE__ */ jsxRuntime.jsxs(
712
- "tr",
713
- {
714
- "data-testid": `comparison-row-${key}`,
715
- className: "border-b border-gray-100",
716
- children: [
717
- /* @__PURE__ */ jsxRuntime.jsx("td", { scope: "row", className: "py-3 px-4 text-sm text-gray-700", children: featureNameMap[key] }),
718
- offers.map((offer) => {
719
- const feature = featureLookup[offer.slug]?.[key];
720
- return /* @__PURE__ */ jsxRuntime.jsx(
721
- "td",
722
- {
723
- className: "text-center py-3 px-4 text-sm",
724
- children: /* @__PURE__ */ jsxRuntime.jsx(ComparisonCellValue, { feature })
725
- },
726
- offer.slug
727
- );
728
- })
729
- ]
730
- },
731
- key
732
- )) })
733
- ] }) });
734
- }
735
- function ComparisonCellValue({ feature }) {
736
- if (!feature) {
737
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2014" });
738
- }
739
- const { value } = feature;
740
- if (value === true) {
741
- return /* @__PURE__ */ jsxRuntime.jsx("svg", { role: "img", "aria-label": "Included", className: "w-5 h-5 text-green-500 mx-auto", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) });
742
- }
743
- if (value === false) {
744
- return /* @__PURE__ */ jsxRuntime.jsx("svg", { role: "img", "aria-label": "Not included", className: "w-5 h-5 text-gray-300 mx-auto", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) });
745
- }
746
- if (typeof value === "string") {
747
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-900 font-medium", children: value });
748
- }
749
- if (typeof value === "number") {
750
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-900 font-medium", children: value === -1 ? "Unlimited" : value });
751
- }
752
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-500", children: String(value) });
753
- }
754
- function ManageSubscription({
755
- returnUrl,
756
- buttonText = "Manage Billing"
757
- }) {
758
- const { openPortal, loading, error } = usePortal();
759
- const handleClick = async () => {
760
- try {
761
- const result = await openPortal(returnUrl);
762
- window.location.href = result.url;
763
- } catch {
764
- }
765
- };
766
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "manage-subscription", children: [
767
- /* @__PURE__ */ jsxRuntime.jsx(
768
- "button",
769
- {
770
- onClick: handleClick,
771
- disabled: loading,
772
- className: `
773
- inline-flex items-center gap-2 px-6 py-3 text-sm font-semibold rounded-xl border transition-colors
774
- ${loading ? "bg-gray-50 text-gray-400 border-gray-200 cursor-not-allowed" : "bg-white text-gray-700 border-gray-200 hover:bg-gray-50 hover:border-gray-300 cursor-pointer"}
775
- `,
776
- children: loading ? "Loading..." : buttonText
777
- }
778
- ),
779
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": "portal-error", className: "mt-2 text-sm text-red-600", children: error.message })
780
- ] });
781
- }
782
- function formatDate(iso) {
783
- const d = new Date(iso);
784
- const year = d.getUTCFullYear();
785
- const month = String(d.getUTCMonth() + 1).padStart(2, "0");
786
- const day = String(d.getUTCDate()).padStart(2, "0");
787
- return `${year}-${month}-${day}`;
788
- }
789
- function StatusBadge({ status, trialEnd }) {
790
- let className = "inline-block text-xs font-semibold px-3 py-1 rounded-full ";
791
- switch (status) {
792
- case "active":
793
- className += "bg-green-50 text-green-700";
794
- break;
795
- case "trialing":
796
- className += "bg-blue-50 text-blue-700";
797
- break;
798
- case "past_due":
799
- className += "bg-yellow-50 text-yellow-700 warning";
800
- break;
801
- case "cancelled":
802
- className += "bg-gray-100 text-gray-600";
803
- break;
804
- case "incomplete":
805
- className += "bg-yellow-50 text-yellow-700";
806
- break;
807
- case "incomplete_expired":
808
- className += "bg-red-50 text-red-700";
809
- break;
810
- case "unpaid":
811
- className += "bg-red-50 text-red-700";
812
- break;
813
- default:
814
- className += "bg-gray-100 text-gray-600";
815
- }
816
- const labelMap = {
817
- active: "Active",
818
- trialing: "Trialing",
819
- past_due: "Past due",
820
- cancelled: "Cancelled",
821
- incomplete: "Incomplete",
822
- incomplete_expired: "Expired",
823
- unpaid: "Unpaid"
824
- };
825
- const readableStatus = labelMap[status] ?? status;
826
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-testid": "plan-status", role: "status", "aria-label": `Subscription status: ${readableStatus}`, className, children: [
827
- labelMap[status] ?? status,
828
- status === "trialing" && trialEnd && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1", children: [
829
- "until ",
830
- formatDate(trialEnd)
831
- ] })
832
- ] });
833
- }
834
- function SubscriptionManager({
835
- productId,
836
- returnUrl,
837
- onPlanChange,
838
- statusMessage
839
- }) {
840
- const { subscription, loading: subLoading, error: subError } = useSubscription();
841
- const { product, loading: productLoading } = useProduct(productId);
842
- const { openPortal, loading: portalLoading } = usePortal();
843
- const loading = subLoading || productLoading;
844
- if (loading) {
845
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "subscription-manager", className: "max-w-3xl mx-auto px-6 py-12", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-center text-gray-500", children: "Loading subscription..." }) });
846
- }
847
- if (subError) {
848
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "subscription-manager", className: "max-w-3xl mx-auto px-6 py-12", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center text-red-600", children: [
849
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-semibold", children: "Something went wrong" }),
850
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: subError.message })
851
- ] }) });
852
- }
853
- const currentOfferId = subscription?.offer?.id ?? null;
854
- const availableOffers = product?.offers.filter((o) => o.id !== currentOfferId) ?? [];
855
- const handlePortalClick = async () => {
856
- try {
857
- const result = await openPortal(returnUrl);
858
- window.location.href = result.url;
859
- } catch {
860
- }
861
- };
862
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "subscription-manager", className: "max-w-3xl mx-auto px-6 py-12 space-y-8", children: [
863
- statusMessage === "success" && /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "status-banner-success", className: "rounded-xl bg-green-50 border border-green-200 p-4 flex items-center gap-3", children: [
864
- /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5 text-green-600 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }),
865
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-green-800", children: "Subscription activated successfully!" })
866
- ] }),
867
- statusMessage === "cancelled" && /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "status-banner-cancelled", className: "rounded-xl bg-gray-50 border border-gray-200 p-4 flex items-center gap-3", children: [
868
- /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5 text-gray-500 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }),
869
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-gray-700", children: "Checkout was cancelled. You can try again whenever you're ready." })
870
- ] }),
871
- statusMessage === "activating" && /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "status-banner-activating", className: "rounded-xl bg-blue-50 border border-blue-200 p-4 flex items-center gap-3", children: [
872
- /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "w-5 h-5 text-blue-600 shrink-0 animate-spin", fill: "none", viewBox: "0 0 24 24", children: [
873
- /* @__PURE__ */ jsxRuntime.jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
874
- /* @__PURE__ */ jsxRuntime.jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" })
875
- ] }),
876
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-blue-800", children: "Activating your subscription..." })
877
- ] }),
878
- subscription ? /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "current-plan", className: "rounded-2xl border border-gray-200 bg-white shadow-sm p-8", children: [
879
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
880
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-bold text-gray-900", children: subscription.offer.name }),
881
- /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: subscription.status, trialEnd: subscription.trial_end })
882
- ] }),
883
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4", children: subscription.offer.is_free ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-label": "Free", children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "text-3xl font-extrabold text-gray-900", children: "Free" }) }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-label": `${Math.floor(parseFloat(subscription.offer.unit_price) || 0)} dollars ${subscription.offer.interval_display}`, children: [
884
- /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-hidden": "true", className: "text-3xl font-extrabold text-gray-900", children: [
885
- "$",
886
- Math.floor(parseFloat(subscription.offer.unit_price) || 0)
887
- ] }),
888
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "text-base text-gray-500 ml-1", children: subscription.offer.interval_display })
889
- ] }) }),
890
- subscription.current_period_start && subscription.current_period_end && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-gray-500", children: [
891
- "Billing period: ",
892
- formatDate(subscription.current_period_start),
893
- " to",
894
- " ",
895
- formatDate(subscription.current_period_end)
896
- ] })
897
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-8", children: [
898
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-lg text-gray-600", children: "No active subscription" }),
899
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-400 mt-1", children: "Choose a plan below to get started." })
900
- ] }),
901
- availableOffers.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "available-plans", className: "space-y-4", children: [
902
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: subscription ? "Change Plan" : "Available Plans" }),
903
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: availableOffers.map((offer) => /* @__PURE__ */ jsxRuntime.jsxs(
904
- "div",
905
- {
906
- className: `rounded-xl border p-6 ${offer.is_featured ? "border-blue-600 shadow-md" : "border-gray-200 bg-white shadow-sm"}`,
907
- children: [
908
- /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-lg font-bold text-gray-900 mb-2", children: offer.name }),
909
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-3", children: offer.is_free ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-label": "Free", children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "text-2xl font-extrabold text-gray-900", children: "Free" }) }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-label": `${Math.floor(parseFloat(offer.unit_price) || 0)} dollars ${offer.interval_display}`, children: [
910
- /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-hidden": "true", className: "text-2xl font-extrabold text-gray-900", children: [
911
- "$",
912
- Math.floor(parseFloat(offer.unit_price) || 0)
913
- ] }),
914
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "text-sm text-gray-500 ml-1", children: offer.interval_display })
915
- ] }) }),
916
- /* @__PURE__ */ jsxRuntime.jsx(
917
- "button",
918
- {
919
- "data-testid": `upgrade-cta-${offer.slug}`,
920
- onClick: () => onPlanChange?.(offer),
921
- className: `w-full py-2.5 px-4 text-sm font-bold rounded-xl cursor-pointer transition-colors ${offer.is_featured ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-gray-100 text-gray-800 hover:bg-gray-200"}`,
922
- children: offer.cta_text
923
- }
924
- )
925
- ]
926
- },
927
- offer.id
928
- )) })
929
- ] }),
930
- subscription && subscription.status !== "cancelled" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-4 border-t border-gray-100", children: /* @__PURE__ */ jsxRuntime.jsx(
931
- "button",
932
- {
933
- "data-testid": "portal-button",
934
- onClick: handlePortalClick,
935
- disabled: portalLoading,
936
- "aria-busy": portalLoading || void 0,
937
- className: `inline-flex items-center gap-2 px-6 py-3 text-sm font-semibold rounded-xl border transition-colors ${portalLoading ? "bg-gray-50 text-gray-400 border-gray-200 cursor-not-allowed" : "bg-white text-gray-700 border-gray-200 hover:bg-gray-50 hover:border-gray-300 cursor-pointer"}`,
938
- children: portalLoading ? "Loading..." : "Manage Billing"
939
- }
940
- ) })
941
- ] });
942
- }
943
-
944
- exports.BillingApiClient = BillingApiClient;
945
- exports.BillingProvider = BillingProvider;
946
- exports.ManageSubscription = ManageSubscription;
947
- exports.PricingDetailPage = PricingDetailPage;
948
- exports.PricingPage = PricingPage;
949
- exports.PricingSection = PricingSection;
950
- exports.SubscriptionManager = SubscriptionManager;
951
- exports.UpgradeModal = UpgradeModal;
952
- exports.useBillingContext = useBillingContext;
953
- exports.useCheckout = useCheckout;
954
- exports.usePortal = usePortal;
955
- exports.useProduct = useProduct;
956
- exports.useSubscription = useSubscription;
957
- exports.useSuccessSync = useSuccessSync;
958
- //# sourceMappingURL=index.js.map
959
- //# sourceMappingURL=index.js.map