@solvapay/react 1.0.9-preview.1 → 1.0.9

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.
@@ -0,0 +1,4824 @@
1
+ import {
2
+ defaultAuthAdapter
3
+ } from "./chunk-OUSEQRCT.js";
4
+
5
+ // src/utils/purchases.ts
6
+ function filterPurchases(purchases) {
7
+ return purchases.filter((purchase) => purchase.status === "active");
8
+ }
9
+ function getActivePurchases(purchases) {
10
+ return purchases.filter((purchase) => purchase.status === "active");
11
+ }
12
+ function getCancelledPurchasesWithEndDate(purchases) {
13
+ const now = /* @__PURE__ */ new Date();
14
+ return purchases.filter((purchase) => {
15
+ return purchase.status === "active" && purchase.cancelledAt && purchase.endDate && new Date(purchase.endDate) > now;
16
+ });
17
+ }
18
+ function getMostRecentPurchase(purchases) {
19
+ if (purchases.length === 0) return null;
20
+ return purchases.reduce((latest, current) => {
21
+ return new Date(current.startDate) > new Date(latest.startDate) ? current : latest;
22
+ });
23
+ }
24
+ function getPrimaryPurchase(purchases) {
25
+ const filtered = filterPurchases(purchases);
26
+ if (filtered.length > 0) {
27
+ return getMostRecentPurchase(filtered);
28
+ }
29
+ return null;
30
+ }
31
+ function isPaidPurchase(purchase) {
32
+ return (purchase.amount ?? 0) > 0;
33
+ }
34
+ function isPlanPurchase(purchase) {
35
+ return !!purchase.planSnapshot && purchase.metadata?.purpose !== "credit_topup";
36
+ }
37
+ function isTopupPurchase(purchase) {
38
+ return !isPlanPurchase(purchase);
39
+ }
40
+
41
+ // src/utils/headers.ts
42
+ var CUSTOMER_REF_KEY = "solvapay_customerRef";
43
+ var CUSTOMER_REF_EXPIRY = "solvapay_customerRef_expiry";
44
+ var CUSTOMER_REF_USER_ID_KEY = "solvapay_customerRef_userId";
45
+ function getAuthAdapter(config) {
46
+ if (config?.auth?.adapter) {
47
+ return config.auth.adapter;
48
+ }
49
+ return defaultAuthAdapter;
50
+ }
51
+ function getCachedCustomerRef(userId) {
52
+ if (typeof window === "undefined") return null;
53
+ const cached = localStorage.getItem(CUSTOMER_REF_KEY);
54
+ const expiry = localStorage.getItem(CUSTOMER_REF_EXPIRY);
55
+ const cachedUserId = localStorage.getItem(CUSTOMER_REF_USER_ID_KEY);
56
+ if (!cached || !expiry) return null;
57
+ if (Date.now() > parseInt(expiry)) {
58
+ localStorage.removeItem(CUSTOMER_REF_KEY);
59
+ localStorage.removeItem(CUSTOMER_REF_EXPIRY);
60
+ localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
61
+ return null;
62
+ }
63
+ if (userId !== void 0 && userId !== null) {
64
+ if (cachedUserId !== userId) {
65
+ clearCachedCustomerRef();
66
+ return null;
67
+ }
68
+ }
69
+ return cached;
70
+ }
71
+ var CACHE_DURATION = 24 * 60 * 60 * 1e3;
72
+ function setCachedCustomerRef(customerRef, userId) {
73
+ if (typeof window === "undefined") return;
74
+ if (userId === void 0 || userId === null) {
75
+ return;
76
+ }
77
+ localStorage.setItem(CUSTOMER_REF_KEY, customerRef);
78
+ localStorage.setItem(CUSTOMER_REF_EXPIRY, String(Date.now() + CACHE_DURATION));
79
+ localStorage.setItem(CUSTOMER_REF_USER_ID_KEY, userId);
80
+ }
81
+ function clearCachedCustomerRef() {
82
+ if (typeof window === "undefined") return;
83
+ localStorage.removeItem(CUSTOMER_REF_KEY);
84
+ localStorage.removeItem(CUSTOMER_REF_EXPIRY);
85
+ localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
86
+ }
87
+ async function buildRequestHeaders(config) {
88
+ const adapter = getAuthAdapter(config);
89
+ const token = await adapter.getToken();
90
+ const userId = await adapter.getUserId();
91
+ const cachedRef = getCachedCustomerRef(userId);
92
+ const headers = {
93
+ "Content-Type": "application/json"
94
+ };
95
+ if (token) {
96
+ headers["Authorization"] = `Bearer ${token}`;
97
+ }
98
+ if (cachedRef) {
99
+ headers["x-solvapay-customer-ref"] = cachedRef;
100
+ }
101
+ if (config?.headers) {
102
+ const custom = typeof config.headers === "function" ? await config.headers() : config.headers;
103
+ Object.assign(headers, custom);
104
+ }
105
+ return { headers, userId };
106
+ }
107
+
108
+ // src/transport/http.ts
109
+ async function request(config, url, opts) {
110
+ const { headers } = await buildRequestHeaders(config);
111
+ const fetchFn = config?.fetch || fetch;
112
+ const init = { method: opts.method, headers };
113
+ if (opts.body !== void 0) {
114
+ init.body = JSON.stringify(opts.body);
115
+ }
116
+ const res = await fetchFn(url, init);
117
+ if (!res.ok) {
118
+ let serverMessage;
119
+ try {
120
+ const data = await res.clone().json();
121
+ serverMessage = data?.error;
122
+ } catch {
123
+ }
124
+ const error = new Error(serverMessage || `${opts.errorPrefix}: ${res.statusText || res.status}`);
125
+ config?.onError?.(error, opts.onErrorContext);
126
+ throw error;
127
+ }
128
+ return await res.json();
129
+ }
130
+ var DEFAULT_ROUTES = {
131
+ checkPurchase: "/api/check-purchase",
132
+ createPayment: "/api/create-payment-intent",
133
+ processPayment: "/api/process-payment",
134
+ createTopupPayment: "/api/create-topup-payment-intent",
135
+ customerBalance: "/api/customer-balance",
136
+ cancelRenewal: "/api/cancel-renewal",
137
+ reactivateRenewal: "/api/reactivate-renewal",
138
+ activatePlan: "/api/activate-plan",
139
+ createCheckoutSession: "/api/create-checkout-session",
140
+ createCustomerSession: "/api/create-customer-session",
141
+ getMerchant: "/api/merchant",
142
+ getProduct: "/api/get-product",
143
+ listPlans: "/api/list-plans",
144
+ getPaymentMethod: "/api/payment-method",
145
+ getUsage: "/api/usage"
146
+ };
147
+ function routeFor(config, key) {
148
+ const configured = config?.api?.[key];
149
+ return configured || DEFAULT_ROUTES[key];
150
+ }
151
+ function createHttpTransport(config) {
152
+ return {
153
+ checkPurchase: () => request(config, routeFor(config, "checkPurchase"), {
154
+ method: "GET",
155
+ onErrorContext: "checkPurchase",
156
+ errorPrefix: "Failed to check purchase"
157
+ }),
158
+ createPayment: (params) => {
159
+ const body = {};
160
+ if (params.planRef) body.planRef = params.planRef;
161
+ if (params.productRef) body.productRef = params.productRef;
162
+ if (params.customer && (params.customer.name || params.customer.email)) {
163
+ body.customer = params.customer;
164
+ }
165
+ return request(config, routeFor(config, "createPayment"), {
166
+ method: "POST",
167
+ body,
168
+ onErrorContext: "createPayment",
169
+ errorPrefix: "Failed to create payment"
170
+ });
171
+ },
172
+ processPayment: (params) => request(config, routeFor(config, "processPayment"), {
173
+ method: "POST",
174
+ body: params,
175
+ onErrorContext: "processPayment",
176
+ errorPrefix: "Failed to process payment"
177
+ }),
178
+ createTopupPayment: (params) => request(config, routeFor(config, "createTopupPayment"), {
179
+ method: "POST",
180
+ body: { amount: params.amount, currency: params.currency },
181
+ onErrorContext: "createTopupPayment",
182
+ errorPrefix: "Failed to create topup payment"
183
+ }),
184
+ getBalance: () => request(config, routeFor(config, "customerBalance"), {
185
+ method: "GET",
186
+ onErrorContext: "getBalance",
187
+ errorPrefix: "Failed to fetch balance"
188
+ }),
189
+ cancelRenewal: (params) => request(config, routeFor(config, "cancelRenewal"), {
190
+ method: "POST",
191
+ body: params,
192
+ onErrorContext: "cancelRenewal",
193
+ errorPrefix: "Failed to cancel renewal"
194
+ }),
195
+ reactivateRenewal: (params) => request(config, routeFor(config, "reactivateRenewal"), {
196
+ method: "POST",
197
+ body: params,
198
+ onErrorContext: "reactivateRenewal",
199
+ errorPrefix: "Failed to reactivate renewal"
200
+ }),
201
+ activatePlan: (params) => request(config, routeFor(config, "activatePlan"), {
202
+ method: "POST",
203
+ body: params,
204
+ onErrorContext: "activatePlan",
205
+ errorPrefix: "Failed to activate plan"
206
+ }),
207
+ createCheckoutSession: (params) => {
208
+ const body = {};
209
+ if (params?.planRef) body.planRef = params.planRef;
210
+ if (params?.productRef) body.productRef = params.productRef;
211
+ if (params?.returnUrl) body.returnUrl = params.returnUrl;
212
+ return request(
213
+ config,
214
+ routeFor(config, "createCheckoutSession"),
215
+ {
216
+ method: "POST",
217
+ body,
218
+ onErrorContext: "createCheckoutSession",
219
+ errorPrefix: "Failed to create checkout session"
220
+ }
221
+ );
222
+ },
223
+ createCustomerSession: () => request(
224
+ config,
225
+ routeFor(config, "createCustomerSession"),
226
+ {
227
+ method: "POST",
228
+ body: {},
229
+ onErrorContext: "createCustomerSession",
230
+ errorPrefix: "Failed to create customer session"
231
+ }
232
+ ),
233
+ getMerchant: () => request(config, routeFor(config, "getMerchant"), {
234
+ method: "GET",
235
+ onErrorContext: "getMerchant",
236
+ errorPrefix: "Failed to fetch merchant"
237
+ }),
238
+ getProduct: (productRef) => {
239
+ const base = routeFor(config, "getProduct");
240
+ const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
241
+ return request(config, url, {
242
+ method: "GET",
243
+ onErrorContext: "getProduct",
244
+ errorPrefix: "Failed to fetch product"
245
+ });
246
+ },
247
+ listPlans: (productRef) => {
248
+ const base = routeFor(config, "listPlans");
249
+ const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
250
+ return request(config, url, {
251
+ method: "GET",
252
+ onErrorContext: "listPlans",
253
+ errorPrefix: "Failed to list plans"
254
+ });
255
+ },
256
+ getPaymentMethod: () => request(config, routeFor(config, "getPaymentMethod"), {
257
+ method: "GET",
258
+ onErrorContext: "getPaymentMethod",
259
+ errorPrefix: "Failed to load payment method"
260
+ }),
261
+ getUsage: () => request(config, routeFor(config, "getUsage"), {
262
+ method: "GET",
263
+ onErrorContext: "getUsage",
264
+ errorPrefix: "Failed to load usage"
265
+ })
266
+ };
267
+ }
268
+
269
+ // src/i18n/en.ts
270
+ function intervalPhrase(ctx) {
271
+ const interval = ctx.plan?.interval;
272
+ const count = ctx.plan?.intervalCount ?? 1;
273
+ if (!interval) return "";
274
+ return count > 1 ? `${count} ${interval}s` : interval;
275
+ }
276
+ function trialPhrase(ctx) {
277
+ const trialDays = ctx.plan?.trialDays ?? ctx.trialDays;
278
+ return trialDays ? ` after your ${trialDays}-day free trial` : "";
279
+ }
280
+ function termsSentence(ctx) {
281
+ const { termsUrl, privacyUrl } = ctx.merchant;
282
+ if (termsUrl && privacyUrl) {
283
+ return ` See ${termsUrl} and ${privacyUrl}.`;
284
+ }
285
+ if (termsUrl) return ` See ${termsUrl}.`;
286
+ if (privacyUrl) return ` See ${privacyUrl}.`;
287
+ return "";
288
+ }
289
+ var enCopy = {
290
+ mandate: {
291
+ recurring: (ctx) => {
292
+ const period = intervalPhrase(ctx);
293
+ const trial = trialPhrase(ctx);
294
+ const every = period ? ` every ${period}` : "";
295
+ return `By subscribing, you authorize ${ctx.merchant.legalName} to charge ${ctx.amountFormatted}${every}${trial} until you cancel. You can cancel any time. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
296
+ },
297
+ oneTime: (ctx) => {
298
+ const product = ctx.product?.name ? ` for ${ctx.product.name}` : "";
299
+ return `By confirming, you authorize ${ctx.merchant.legalName} to charge ${ctx.amountFormatted}${product}. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
300
+ },
301
+ topup: (ctx) => {
302
+ const product = ctx.product?.name ? ` to add credits to your ${ctx.product.name} balance` : " to add credits to your balance";
303
+ return `By confirming, you authorize ${ctx.merchant.legalName} to charge ${ctx.amountFormatted}${product}. Credits are non-refundable once used. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
304
+ },
305
+ usageMetered: (ctx) => {
306
+ const measures = ctx.plan?.measures ?? "unit";
307
+ const cycle = ctx.plan?.billingCycle ?? "monthly";
308
+ const product = ctx.product?.name ?? "the service";
309
+ return `By confirming, you authorize ${ctx.merchant.legalName} to charge your payment method for metered usage of ${product} at ${ctx.amountFormatted} per ${measures}, billed ${cycle}. You can cancel any time. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
310
+ },
311
+ freeTier: (ctx) => {
312
+ const product = ctx.product?.name ?? "this plan";
313
+ const planPhrase = ctx.plan?.name ? ` on ${ctx.plan.name}` : "";
314
+ return `By confirming, you activate ${product}${planPhrase}. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
315
+ }
316
+ },
317
+ cta: {
318
+ payNow: "Pay Now",
319
+ topUp: "Top Up",
320
+ subscribe: "Subscribe",
321
+ trialStart: "Start {trialDays}-day free trial",
322
+ payAmount: "Pay {amount}",
323
+ addAmount: "Add {amount}",
324
+ startUsing: "Start using {product}",
325
+ processing: "Processing..."
326
+ },
327
+ interval: {
328
+ day: "day",
329
+ week: "week",
330
+ month: "month",
331
+ year: "year",
332
+ every: "every {n} {unit}",
333
+ free: "Free",
334
+ trial: "{trialDays}-day free trial"
335
+ },
336
+ terms: {
337
+ checkboxLabel: "I agree to the terms and privacy policy"
338
+ },
339
+ customer: {
340
+ chargingTo: "Charging to {email}",
341
+ emailLabel: "Email",
342
+ nameLabel: "Name"
343
+ },
344
+ balance: {
345
+ credits: " credits",
346
+ currencyEquivalent: " (~{amount})"
347
+ },
348
+ product: {
349
+ currentProductLabel: "Current product: {name}"
350
+ },
351
+ topup: {
352
+ selectOrEnterAmount: "Please select or enter an amount",
353
+ minAmount: "Minimum amount is {amount}",
354
+ maxAmount: "Maximum amount is {amount}"
355
+ },
356
+ activation: {
357
+ paymentRequired: "This plan requires payment. Please select a different plan.",
358
+ invalidConfiguration: "Invalid plan configuration.",
359
+ unexpectedResponse: "Unexpected response from server.",
360
+ failed: "Activation failed"
361
+ },
362
+ planSelector: {
363
+ heading: "Choose your pricing",
364
+ currentBadge: "Current",
365
+ popularBadge: "Popular",
366
+ freeBadge: "Free",
367
+ perIntervalShort: "/{interval}",
368
+ continueButton: "Continue",
369
+ backButton: "\u2190 Back to plans",
370
+ trialBadge: "{trialDays}-day free trial"
371
+ },
372
+ amountPicker: {
373
+ selectAmountLabel: "Select an amount",
374
+ customAmountLabel: "Or enter a custom amount",
375
+ creditEstimateExact: "= {credits} credits",
376
+ creditEstimateApprox: "~ {credits} credits"
377
+ },
378
+ activationFlow: {
379
+ heading: "Confirm your plan",
380
+ activateButton: "Activate",
381
+ activatingLabel: "Activating...",
382
+ topupHeading: "Add credits",
383
+ topupSubheading: "Top up your credits to activate this plan.",
384
+ continueToPayment: "Continue to payment",
385
+ changeAmountButton: "Change amount",
386
+ retryingHeading: "Activating your plan...",
387
+ retryingSubheading: "Payment received. Setting up your plan.",
388
+ activatedHeading: "Plan selected",
389
+ activatedSubheading: "Your plan is now active.",
390
+ tryAgainButton: "Try Again",
391
+ backButton: "\u2190 Back to plan selection"
392
+ },
393
+ cancelPlan: {
394
+ button: "Cancel plan",
395
+ buttonLoading: "Cancelling...",
396
+ confirmRecurring: "Are you sure you want to cancel your subscription?",
397
+ confirmUsageBased: "Are you sure you want to deactivate your plan? This will take effect immediately."
398
+ },
399
+ cancelledNotice: {
400
+ heading: "Your purchase has been cancelled",
401
+ expiresLabel: "Purchase Expires: {date}",
402
+ daysRemaining: "{days} days remaining",
403
+ dayRemaining: "1 day remaining",
404
+ accessUntil: "You'll continue to have access to {product} features until this date",
405
+ accessEnded: "Your purchase access has ended",
406
+ cancelledOn: "Cancelled on {date}",
407
+ reactivateButton: "Undo Cancellation",
408
+ reactivateButtonLoading: "Reactivating..."
409
+ },
410
+ creditGate: {
411
+ lowBalanceHeading: "You're out of credits",
412
+ lowBalanceSubheading: "Top up to continue using {product}",
413
+ topUpCta: "Top up now"
414
+ },
415
+ currentPlan: {
416
+ heading: "Your plan",
417
+ nextBilling: "Next billing: {date}",
418
+ expiresOn: "Expires {date}",
419
+ validIndefinitely: "Valid indefinitely",
420
+ paymentMethod: "{brand} \u2022\u2022\u2022\u2022 {last4}",
421
+ paymentMethodExpires: "expires {month}/{year}",
422
+ noPaymentMethod: "No payment method on file",
423
+ updatePaymentButton: "Update card",
424
+ cycleUnit: {
425
+ weekly: "week",
426
+ monthly: "month",
427
+ quarterly: "3 months",
428
+ yearly: "year"
429
+ }
430
+ },
431
+ customerPortal: {
432
+ launchButton: "Manage billing",
433
+ loadingLabel: "Loading portal\u2026"
434
+ },
435
+ errors: {
436
+ paymentInitFailed: "Payment initialization failed",
437
+ topupInitFailed: "Top-up initialization failed",
438
+ configMissingPlanOrProduct: "PaymentForm: either planRef or productRef is required",
439
+ configMissingAmount: "TopupForm: amount must be a positive number",
440
+ unknownError: "Unknown error",
441
+ stripeUnavailable: "Stripe is not available. Please refresh the page.",
442
+ paymentIntentUnavailable: "Payment intent not available. Please refresh the page.",
443
+ cardElementMissing: "Card element not found",
444
+ paymentUnexpected: "An unexpected error occurred.",
445
+ paymentProcessingFailed: "Payment processing failed. Please try again or contact support.",
446
+ paymentRequires3ds: "Payment requires additional authentication. Please complete the verification.",
447
+ paymentProcessingTimeout: "Payment processing timed out \u2014 webhooks may not be configured",
448
+ paymentConfirmationDelayed: "Payment succeeded but confirmation is taking longer than usual. Refresh in a moment to see your purchase.",
449
+ paymentStatusPrefix: "Payment status: {status}",
450
+ paywallInvalidContent: "Paywall content is missing or malformed.",
451
+ usageLoadFailed: "Failed to load usage"
452
+ },
453
+ paywall: {
454
+ header: "Unlock access",
455
+ paymentRequiredHeading: "Upgrade to continue",
456
+ activationRequiredHeading: "Add credits to continue",
457
+ resolvedHeading: "You can continue",
458
+ productContext: "For {product}",
459
+ balanceLine: "You have {available} credits, need {required}.",
460
+ paymentRequiredMessage: "You've used all your included calls{forProduct}. Choose a plan below to keep going.",
461
+ paymentRequiredMessageRemaining: "Only {remaining} call{pluralSuffix} left{forProduct}. Choose a plan below to keep going.",
462
+ paymentRequiredProductSuffix: " for {product}",
463
+ retryButton: "Continue",
464
+ hostedCheckoutButton: "Open checkout",
465
+ hostedCheckoutLoading: "Loading checkout\u2026"
466
+ },
467
+ usage: {
468
+ header: "Usage",
469
+ percentUsedLabel: "{percent}% used",
470
+ usedLabel: "{used} / {total} {unit}",
471
+ remainingLabel: "{remaining} {unit} remaining",
472
+ unlimitedLabel: "Unlimited",
473
+ resetsInLabel: "Resets in {days} days",
474
+ resetsOnLabel: "Resets {date}",
475
+ loadingLabel: "Loading usage\u2026",
476
+ emptyLabel: "No usage-based plan is active.",
477
+ approachingLimit: "You're approaching your usage limit.",
478
+ atLimit: "You've reached your usage limit.",
479
+ topUpCta: "Add credits",
480
+ upgradeCta: "Upgrade plan",
481
+ refreshCta: "Refresh"
482
+ }
483
+ };
484
+
485
+ // src/i18n/merge.ts
486
+ function mergeCopy(defaults, overrides) {
487
+ if (!overrides) return defaults;
488
+ const merged = { ...defaults };
489
+ for (const sectionKey of Object.keys(overrides)) {
490
+ const defaultSection = defaults[sectionKey];
491
+ const overrideSection = overrides[sectionKey];
492
+ if (!overrideSection) continue;
493
+ const combined = { ...defaultSection, ...overrideSection };
494
+ merged[sectionKey] = combined;
495
+ }
496
+ return merged;
497
+ }
498
+
499
+ // src/i18n/context.tsx
500
+ import { createContext, useContext, useMemo } from "react";
501
+ import { jsx } from "react/jsx-runtime";
502
+ var CopyContext = createContext({
503
+ locale: void 0,
504
+ copy: enCopy
505
+ });
506
+ var CopyProvider = ({
507
+ locale,
508
+ copy: overrides,
509
+ children
510
+ }) => {
511
+ const value = useMemo(
512
+ () => ({
513
+ locale,
514
+ copy: mergeCopy(enCopy, overrides)
515
+ }),
516
+ [locale, overrides]
517
+ );
518
+ return /* @__PURE__ */ jsx(CopyContext.Provider, { value, children });
519
+ };
520
+ function useCopyContext() {
521
+ return useContext(CopyContext);
522
+ }
523
+
524
+ // src/SolvaPayProvider.tsx
525
+ import { createContext as createContext2, useState, useEffect, useCallback, useMemo as useMemo2, useRef } from "react";
526
+ import { jsx as jsx2 } from "react/jsx-runtime";
527
+ var SolvaPayContext = createContext2(null);
528
+ function resolveTransport(config) {
529
+ return config?.transport ?? createHttpTransport(config);
530
+ }
531
+ var SolvaPayProvider = ({ config, children }) => {
532
+ const initial = config?.initial;
533
+ const [purchaseData, setPurchaseData] = useState(() => {
534
+ if (!initial) return { purchases: [] };
535
+ return {
536
+ // The server-side `PurchaseCheckResult.purchases` shape has looser
537
+ // optional fields than `PurchaseInfo`, but the runtime data is
538
+ // identical — the same rows flow through `transport.checkPurchase()`
539
+ // today. Cast once here to unify the two entry points, and run
540
+ // the same `filterPurchases` the HTTP path applies in
541
+ // `fetchPurchase` so the bootstrap-hydrated data never contains
542
+ // cancelled / expired / suspended rows the active-only derivations
543
+ // (`activePurchase`, `hasPaidPurchase`, …) wouldn't expect.
544
+ purchases: filterPurchases(
545
+ initial.purchase?.purchases ?? []
546
+ ),
547
+ customerRef: initial.customerRef ?? initial.purchase?.customerRef,
548
+ email: initial.purchase?.email,
549
+ name: initial.purchase?.name
550
+ };
551
+ });
552
+ const [loading, setLoading] = useState(false);
553
+ const [isRefetching, setIsRefetching] = useState(false);
554
+ const [purchaseError, setPurchaseError] = useState(null);
555
+ const [internalCustomerRef, setInternalCustomerRef] = useState(
556
+ initial?.customerRef ?? void 0
557
+ );
558
+ const [userId, setUserId] = useState(null);
559
+ const [isAuthenticated, setIsAuthenticated] = useState(!!initial?.customerRef);
560
+ const [creditsValue, setCreditsValue] = useState(
561
+ initial?.balance?.credits ?? null
562
+ );
563
+ const [displayCurrencyValue, setDisplayCurrencyValue] = useState(
564
+ initial?.balance?.displayCurrency ?? null
565
+ );
566
+ const [creditsPerMinorUnitValue, setCreditsPerMinorUnitValue] = useState(
567
+ initial?.balance?.creditsPerMinorUnit ?? null
568
+ );
569
+ const [displayExchangeRateValue, setDisplayExchangeRateValue] = useState(
570
+ initial?.balance?.displayExchangeRate ?? null
571
+ );
572
+ const [balanceLoading, setBalanceLoading] = useState(false);
573
+ const balanceInFlightRef = useRef(false);
574
+ const balanceLoadedRef = useRef(!!initial?.balance);
575
+ const optimisticUntilRef = useRef(0);
576
+ const optimisticTimerRef = useRef(null);
577
+ const fetchBalanceRef = useRef(null);
578
+ const inFlightRef = useRef(null);
579
+ const loadedCacheKeysRef = useRef(
580
+ new Set(initial?.customerRef ? [initial.customerRef] : [])
581
+ );
582
+ const configRef = useRef(config);
583
+ const transportRef = useRef(resolveTransport(config));
584
+ useEffect(() => {
585
+ configRef.current = config;
586
+ transportRef.current = resolveTransport(config);
587
+ }, [config]);
588
+ const fetchBalanceImpl = useCallback(async () => {
589
+ if (optimisticUntilRef.current > Date.now()) return;
590
+ if (!isAuthenticated && !internalCustomerRef) {
591
+ setCreditsValue(null);
592
+ setDisplayCurrencyValue(null);
593
+ setCreditsPerMinorUnitValue(null);
594
+ setDisplayExchangeRateValue(null);
595
+ setBalanceLoading(false);
596
+ balanceLoadedRef.current = false;
597
+ return;
598
+ }
599
+ if (balanceInFlightRef.current) return;
600
+ balanceInFlightRef.current = true;
601
+ if (!balanceLoadedRef.current) {
602
+ setBalanceLoading(true);
603
+ }
604
+ try {
605
+ if (!transportRef.current.getBalance) {
606
+ setBalanceLoading(false);
607
+ balanceInFlightRef.current = false;
608
+ return;
609
+ }
610
+ const data = await transportRef.current.getBalance();
611
+ setCreditsValue(data.credits ?? null);
612
+ setDisplayCurrencyValue(data.displayCurrency ?? null);
613
+ setCreditsPerMinorUnitValue(data.creditsPerMinorUnit ?? null);
614
+ setDisplayExchangeRateValue(data.displayExchangeRate ?? null);
615
+ balanceLoadedRef.current = true;
616
+ } catch (error) {
617
+ console.error("[SolvaPayProvider] Failed to fetch balance:", error);
618
+ } finally {
619
+ setBalanceLoading(false);
620
+ balanceInFlightRef.current = false;
621
+ }
622
+ }, [isAuthenticated, internalCustomerRef]);
623
+ useEffect(() => {
624
+ fetchBalanceRef.current = fetchBalanceImpl;
625
+ }, [fetchBalanceImpl]);
626
+ useEffect(() => {
627
+ return () => {
628
+ if (optimisticTimerRef.current) clearTimeout(optimisticTimerRef.current);
629
+ };
630
+ }, []);
631
+ const OPTIMISTIC_GRACE_MS = 8e3;
632
+ const adjustBalanceImpl = useCallback((credits) => {
633
+ setCreditsValue((prev) => (prev ?? 0) + credits);
634
+ optimisticUntilRef.current = Date.now() + OPTIMISTIC_GRACE_MS;
635
+ if (optimisticTimerRef.current) clearTimeout(optimisticTimerRef.current);
636
+ optimisticTimerRef.current = setTimeout(() => {
637
+ optimisticUntilRef.current = 0;
638
+ fetchBalanceRef.current?.();
639
+ }, OPTIMISTIC_GRACE_MS);
640
+ }, []);
641
+ const createPayment = useCallback(
642
+ (params) => transportRef.current.createPayment(params),
643
+ []
644
+ );
645
+ const processPayment = useCallback(
646
+ (params) => transportRef.current.processPayment(params),
647
+ []
648
+ );
649
+ const createTopupPayment = useCallback(
650
+ (params) => transportRef.current.createTopupPayment(params),
651
+ []
652
+ );
653
+ useEffect(() => {
654
+ if (configRef.current?.initial) {
655
+ setIsAuthenticated(configRef.current.initial.customerRef !== null);
656
+ return;
657
+ }
658
+ const detectAuth = async () => {
659
+ const currentConfig = configRef.current;
660
+ const adapter2 = getAuthAdapter(currentConfig);
661
+ const token = await adapter2.getToken();
662
+ const detectedUserId = await adapter2.getUserId();
663
+ const prevUserId = userId;
664
+ setIsAuthenticated(!!token);
665
+ setUserId(detectedUserId);
666
+ if (prevUserId !== null && detectedUserId !== prevUserId) {
667
+ clearCachedCustomerRef();
668
+ setInternalCustomerRef(void 0);
669
+ loadedCacheKeysRef.current.clear();
670
+ return;
671
+ }
672
+ const cachedRef = getCachedCustomerRef(detectedUserId);
673
+ if (cachedRef && token) {
674
+ setInternalCustomerRef(cachedRef);
675
+ } else if (!token) {
676
+ clearCachedCustomerRef();
677
+ setInternalCustomerRef(void 0);
678
+ loadedCacheKeysRef.current.clear();
679
+ } else if (token && !cachedRef) {
680
+ setInternalCustomerRef(void 0);
681
+ }
682
+ };
683
+ detectAuth();
684
+ const adapter = getAuthAdapter(configRef.current);
685
+ if (typeof adapter.subscribe === "function") {
686
+ const unsubscribe = adapter.subscribe(() => {
687
+ detectAuth();
688
+ });
689
+ return unsubscribe;
690
+ }
691
+ const interval = setInterval(detectAuth, 3e4);
692
+ return () => clearInterval(interval);
693
+ }, [userId]);
694
+ const fetchPurchase = useCallback(
695
+ async (force = false) => {
696
+ if (!isAuthenticated && !internalCustomerRef) {
697
+ setPurchaseData({ purchases: [] });
698
+ setPurchaseError(null);
699
+ setLoading(false);
700
+ setIsRefetching(false);
701
+ inFlightRef.current = null;
702
+ return;
703
+ }
704
+ const cacheKey = internalCustomerRef || userId || "anonymous";
705
+ if (inFlightRef.current === cacheKey && !force) {
706
+ return;
707
+ }
708
+ inFlightRef.current = cacheKey;
709
+ const hasLoadedOnce = loadedCacheKeysRef.current.has(cacheKey);
710
+ if (hasLoadedOnce) {
711
+ setIsRefetching(true);
712
+ } else {
713
+ setLoading(true);
714
+ }
715
+ try {
716
+ if (!transportRef.current.checkPurchase) {
717
+ setLoading(false);
718
+ setIsRefetching(false);
719
+ loadedCacheKeysRef.current.add(cacheKey);
720
+ inFlightRef.current = null;
721
+ return;
722
+ }
723
+ const data = await transportRef.current.checkPurchase();
724
+ if (data.customerRef) {
725
+ setInternalCustomerRef(data.customerRef);
726
+ loadedCacheKeysRef.current.add(data.customerRef);
727
+ const currentAdapter = getAuthAdapter(configRef.current);
728
+ const currentUserId = await currentAdapter.getUserId();
729
+ setCachedCustomerRef(data.customerRef, currentUserId);
730
+ }
731
+ if (inFlightRef.current === cacheKey) {
732
+ const filteredData = {
733
+ ...data,
734
+ purchases: filterPurchases(data.purchases || [])
735
+ };
736
+ setPurchaseData(filteredData);
737
+ setPurchaseError(null);
738
+ }
739
+ } catch (err) {
740
+ console.error("[SolvaPayProvider] Failed to fetch purchase:", err);
741
+ if (inFlightRef.current === cacheKey) {
742
+ setPurchaseError(err instanceof Error ? err : new Error(String(err)));
743
+ }
744
+ } finally {
745
+ if (inFlightRef.current === cacheKey) {
746
+ loadedCacheKeysRef.current.add(cacheKey);
747
+ setLoading(false);
748
+ setIsRefetching(false);
749
+ inFlightRef.current = null;
750
+ }
751
+ }
752
+ },
753
+ [isAuthenticated, internalCustomerRef, userId]
754
+ );
755
+ const fetchPurchaseRef = useRef(fetchPurchase);
756
+ useEffect(() => {
757
+ fetchPurchaseRef.current = fetchPurchase;
758
+ }, [fetchPurchase]);
759
+ const applyInitialRef = useRef(null);
760
+ const refetchPurchase = useCallback(async () => {
761
+ inFlightRef.current = null;
762
+ const refresh = configRef.current?.refreshInitial;
763
+ const hasCheckPurchase = !!transportRef.current.checkPurchase;
764
+ if (refresh && !hasCheckPurchase) {
765
+ setIsRefetching(true);
766
+ try {
767
+ const next = await refresh();
768
+ if (next) applyInitialRef.current?.(next);
769
+ } catch (err) {
770
+ console.error("[SolvaPayProvider] refetchPurchase (MCP) failed:", err);
771
+ } finally {
772
+ setIsRefetching(false);
773
+ }
774
+ return;
775
+ }
776
+ await fetchPurchaseRef.current(true);
777
+ }, []);
778
+ const applyInitial = useCallback((next) => {
779
+ setPurchaseData({
780
+ // Mirror the HTTP path: strip non-active rows before any
781
+ // derivation (`activePurchase` etc.) reads the array.
782
+ purchases: filterPurchases(
783
+ next.purchase?.purchases ?? []
784
+ ),
785
+ customerRef: next.customerRef ?? next.purchase?.customerRef,
786
+ email: next.purchase?.email,
787
+ name: next.purchase?.name
788
+ });
789
+ if (next.customerRef) {
790
+ setInternalCustomerRef(next.customerRef);
791
+ loadedCacheKeysRef.current.add(next.customerRef);
792
+ } else {
793
+ setInternalCustomerRef(void 0);
794
+ loadedCacheKeysRef.current.clear();
795
+ }
796
+ setIsAuthenticated(next.customerRef !== null);
797
+ setCreditsValue(next.balance?.credits ?? null);
798
+ setDisplayCurrencyValue(next.balance?.displayCurrency ?? null);
799
+ setCreditsPerMinorUnitValue(next.balance?.creditsPerMinorUnit ?? null);
800
+ setDisplayExchangeRateValue(next.balance?.displayExchangeRate ?? null);
801
+ balanceLoadedRef.current = !!next.balance;
802
+ }, []);
803
+ useEffect(() => {
804
+ applyInitialRef.current = applyInitial;
805
+ }, [applyInitial]);
806
+ const refreshBootstrap = useCallback(async () => {
807
+ const refresh = configRef.current?.refreshInitial;
808
+ if (refresh) {
809
+ try {
810
+ const next = await refresh();
811
+ if (next) applyInitial(next);
812
+ } catch (err) {
813
+ console.error("[SolvaPayProvider] refreshBootstrap failed:", err);
814
+ }
815
+ return;
816
+ }
817
+ await Promise.all([refetchPurchase(), fetchBalanceRef.current?.() ?? Promise.resolve()]);
818
+ }, [refetchPurchase, applyInitial]);
819
+ const cancelRenewal = useCallback(
820
+ async (params) => {
821
+ const result = await transportRef.current.cancelRenewal(params);
822
+ await refetchPurchase();
823
+ return result;
824
+ },
825
+ [refetchPurchase]
826
+ );
827
+ const reactivateRenewal = useCallback(
828
+ async (params) => {
829
+ const result = await transportRef.current.reactivateRenewal(params);
830
+ await refetchPurchase();
831
+ return result;
832
+ },
833
+ [refetchPurchase]
834
+ );
835
+ const activatePlan = useCallback(
836
+ async (params) => {
837
+ const result = await transportRef.current.activatePlan(params);
838
+ if (result.status === "activated" || result.status === "already_active") {
839
+ await refetchPurchase();
840
+ }
841
+ return result;
842
+ },
843
+ [refetchPurchase]
844
+ );
845
+ const hydratedFromInitialRef = useRef(!!initial);
846
+ useEffect(() => {
847
+ inFlightRef.current = null;
848
+ if (hydratedFromInitialRef.current) {
849
+ hydratedFromInitialRef.current = false;
850
+ return;
851
+ }
852
+ if (isAuthenticated || internalCustomerRef) {
853
+ fetchPurchase();
854
+ } else {
855
+ setPurchaseData({ purchases: [] });
856
+ setPurchaseError(null);
857
+ setLoading(false);
858
+ setIsRefetching(false);
859
+ }
860
+ }, [isAuthenticated, internalCustomerRef, userId]);
861
+ const updateCustomerRef = useCallback(
862
+ (newCustomerRef) => {
863
+ setInternalCustomerRef(newCustomerRef);
864
+ setCachedCustomerRef(newCustomerRef, userId);
865
+ fetchPurchase(true);
866
+ },
867
+ [fetchPurchase, userId]
868
+ );
869
+ const purchase = useMemo2(() => {
870
+ const planPurchases = purchaseData.purchases.filter(isPlanPurchase);
871
+ const balanceTransactions = purchaseData.purchases.filter((p) => !isPlanPurchase(p));
872
+ const activePurchase = getPrimaryPurchase(planPurchases);
873
+ const activePaidPurchases = planPurchases.filter(
874
+ (p) => p.status === "active" && isPaidPurchase(p)
875
+ );
876
+ const activePaidPurchase = activePaidPurchases.sort(
877
+ (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
878
+ )[0] || null;
879
+ return {
880
+ loading,
881
+ isRefetching,
882
+ error: purchaseError,
883
+ customerRef: purchaseData.customerRef || internalCustomerRef,
884
+ email: purchaseData.email,
885
+ name: purchaseData.name,
886
+ purchases: purchaseData.purchases,
887
+ hasProduct: (productName) => {
888
+ return planPurchases.some(
889
+ (p) => p.productName.toLowerCase() === productName.toLowerCase() && p.status === "active"
890
+ );
891
+ },
892
+ activePurchase,
893
+ hasPaidPurchase: activePaidPurchases.length > 0,
894
+ activePaidPurchase,
895
+ balanceTransactions
896
+ };
897
+ }, [loading, isRefetching, purchaseError, purchaseData, internalCustomerRef]);
898
+ const balance = useMemo2(
899
+ () => ({
900
+ loading: balanceLoading,
901
+ credits: creditsValue,
902
+ displayCurrency: displayCurrencyValue,
903
+ creditsPerMinorUnit: creditsPerMinorUnitValue,
904
+ displayExchangeRate: displayExchangeRateValue,
905
+ refetch: fetchBalanceImpl,
906
+ adjustBalance: adjustBalanceImpl
907
+ }),
908
+ [
909
+ balanceLoading,
910
+ creditsValue,
911
+ displayCurrencyValue,
912
+ creditsPerMinorUnitValue,
913
+ displayExchangeRateValue,
914
+ fetchBalanceImpl,
915
+ adjustBalanceImpl
916
+ ]
917
+ );
918
+ const contextValue = useMemo2(
919
+ () => ({
920
+ purchase,
921
+ refetchPurchase,
922
+ createPayment,
923
+ processPayment,
924
+ createTopupPayment,
925
+ cancelRenewal,
926
+ reactivateRenewal,
927
+ activatePlan,
928
+ customerRef: purchaseData.customerRef || internalCustomerRef,
929
+ updateCustomerRef,
930
+ balance,
931
+ refreshBootstrap,
932
+ _config: configRef.current
933
+ }),
934
+ [
935
+ purchase,
936
+ refetchPurchase,
937
+ createPayment,
938
+ processPayment,
939
+ createTopupPayment,
940
+ cancelRenewal,
941
+ reactivateRenewal,
942
+ activatePlan,
943
+ purchaseData.customerRef,
944
+ internalCustomerRef,
945
+ updateCustomerRef,
946
+ balance,
947
+ refreshBootstrap
948
+ ]
949
+ );
950
+ return /* @__PURE__ */ jsx2(SolvaPayContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx2(CopyProvider, { locale: config?.locale, copy: config?.copy, children }) });
951
+ };
952
+
953
+ // src/primitives/composeRefs.ts
954
+ function setRef(ref, value) {
955
+ if (typeof ref === "function") {
956
+ ref(value);
957
+ } else if (ref !== null && ref !== void 0) {
958
+ ;
959
+ ref.current = value;
960
+ }
961
+ }
962
+ function composeRefs(...refs) {
963
+ return (node) => {
964
+ for (const ref of refs) {
965
+ setRef(ref, node);
966
+ }
967
+ };
968
+ }
969
+
970
+ // src/primitives/slot.tsx
971
+ import React3 from "react";
972
+ import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
973
+ var Slot = React3.forwardRef((props, forwardedRef) => {
974
+ const { children, ...slotProps } = props;
975
+ const childrenArray = React3.Children.toArray(children);
976
+ const slottable = childrenArray.find(isSlottable);
977
+ if (slottable) {
978
+ const newElement = slottable.props.children;
979
+ const newChildren = childrenArray.map((child) => {
980
+ if (child === slottable) {
981
+ if (React3.Children.count(newElement) > 1) return React3.Children.only(null);
982
+ return React3.isValidElement(newElement) ? newElement.props.children : null;
983
+ }
984
+ return child;
985
+ });
986
+ return /* @__PURE__ */ jsx3(SlotClone, { ...slotProps, ref: forwardedRef, children: React3.isValidElement(newElement) ? React3.cloneElement(newElement, void 0, newChildren) : null });
987
+ }
988
+ return /* @__PURE__ */ jsx3(SlotClone, { ...slotProps, ref: forwardedRef, children });
989
+ });
990
+ Slot.displayName = "Slot";
991
+ var SlotClone = React3.forwardRef(
992
+ (props, forwardedRef) => {
993
+ const { children, ...slotProps } = props;
994
+ if (React3.isValidElement(children)) {
995
+ const childProps = children.props ?? {};
996
+ const childRef = getElementRef(children);
997
+ const props2 = mergeProps(slotProps, childProps);
998
+ const cloneProps = { ...props2 };
999
+ if (forwardedRef) {
1000
+ cloneProps.ref = childRef ? composeRefs(forwardedRef, childRef) : forwardedRef;
1001
+ } else {
1002
+ cloneProps.ref = childRef;
1003
+ }
1004
+ return React3.cloneElement(children, cloneProps);
1005
+ }
1006
+ return React3.Children.count(children) > 1 ? React3.Children.only(null) : null;
1007
+ }
1008
+ );
1009
+ SlotClone.displayName = "SlotClone";
1010
+ var Slottable = ({ children }) => {
1011
+ return /* @__PURE__ */ jsx3(Fragment, { children });
1012
+ };
1013
+ function isSlottable(child) {
1014
+ return React3.isValidElement(child) && child.type === Slottable;
1015
+ }
1016
+ function mergeProps(slotProps, childProps) {
1017
+ const overrideProps = { ...childProps };
1018
+ for (const propName in slotProps) {
1019
+ const slotPropValue = slotProps[propName];
1020
+ const childPropValue = childProps[propName];
1021
+ const isHandler = /^on[A-Z]/.test(propName);
1022
+ if (isHandler) {
1023
+ if (slotPropValue && childPropValue) {
1024
+ overrideProps[propName] = (...args) => {
1025
+ ;
1026
+ childPropValue(...args);
1027
+ const first = args[0];
1028
+ if (!first?.defaultPrevented) {
1029
+ ;
1030
+ slotPropValue(...args);
1031
+ }
1032
+ };
1033
+ } else if (slotPropValue) {
1034
+ overrideProps[propName] = slotPropValue;
1035
+ }
1036
+ } else if (propName === "style") {
1037
+ overrideProps[propName] = {
1038
+ ...slotPropValue,
1039
+ ...childPropValue
1040
+ };
1041
+ } else if (propName === "className") {
1042
+ overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(" ");
1043
+ } else if (propName === "ref") {
1044
+ } else if (childPropValue === void 0) {
1045
+ overrideProps[propName] = slotPropValue;
1046
+ }
1047
+ }
1048
+ return overrideProps;
1049
+ }
1050
+ function getElementRef(element) {
1051
+ const el = element;
1052
+ return el.props?.ref ?? el.ref ?? null;
1053
+ }
1054
+
1055
+ // src/primitives/composeEventHandlers.ts
1056
+ function composeEventHandlers(originalEventHandler, ourEventHandler, { checkForDefaultPrevented = true } = {}) {
1057
+ return function handleEvent(event) {
1058
+ originalEventHandler?.(event);
1059
+ if (checkForDefaultPrevented === false || !event.defaultPrevented) {
1060
+ ourEventHandler?.(event);
1061
+ }
1062
+ };
1063
+ }
1064
+
1065
+ // src/hooks/useSolvaPay.ts
1066
+ import { useContext as useContext2 } from "react";
1067
+ function useSolvaPay() {
1068
+ const context = useContext2(SolvaPayContext);
1069
+ if (!context) {
1070
+ throw new Error(
1071
+ "useSolvaPay must be used within a SolvaPayProvider. Wrap your component tree with <SolvaPayProvider> to use this hook."
1072
+ );
1073
+ }
1074
+ return context;
1075
+ }
1076
+
1077
+ // src/hooks/useCheckout.ts
1078
+ import { useState as useState2, useCallback as useCallback2, useRef as useRef2 } from "react";
1079
+ import { loadStripe } from "@stripe/stripe-js";
1080
+ var stripePromiseCache = /* @__PURE__ */ new Map();
1081
+ function getStripeCacheKey(publishableKey, accountId) {
1082
+ return accountId ? `${publishableKey}:${accountId}` : publishableKey;
1083
+ }
1084
+ async function resolvePlanRef(productRef, config) {
1085
+ const listPlansRoute = config?.api?.listPlans || "/api/list-plans";
1086
+ const url = `${listPlansRoute}?productRef=${encodeURIComponent(productRef)}`;
1087
+ const fetchFn = config?.fetch || fetch;
1088
+ const { headers } = await buildRequestHeaders(config);
1089
+ const res = await fetchFn(url, { method: "GET", headers });
1090
+ if (!res.ok) {
1091
+ throw new Error(`Failed to fetch plans for product "${productRef}": ${res.statusText}`);
1092
+ }
1093
+ const data = await res.json();
1094
+ const allPlans = data.plans ?? [];
1095
+ const activePlans = allPlans.filter((p) => p.isActive !== false && p.status !== "inactive");
1096
+ if (activePlans.length === 0) {
1097
+ throw new Error(
1098
+ `No active plans found for product "${productRef}". Configure at least one plan in the SolvaPay Console.`
1099
+ );
1100
+ }
1101
+ if (activePlans.length === 1) {
1102
+ return activePlans[0].reference;
1103
+ }
1104
+ const defaultPlan = activePlans.find((p) => p.default === true);
1105
+ if (defaultPlan) {
1106
+ return defaultPlan.reference;
1107
+ }
1108
+ throw new Error(
1109
+ `Product "${productRef}" has ${activePlans.length} active plans but none is marked as default. Either pass planRef explicitly, use <PricingSelector> for user selection, or mark one plan as default in the SolvaPay Console.`
1110
+ );
1111
+ }
1112
+ function useCheckout(options) {
1113
+ const { planRef, productRef, customer } = options;
1114
+ const { createPayment, customerRef, updateCustomerRef, _config } = useSolvaPay();
1115
+ const [loading, setLoading] = useState2(false);
1116
+ const [error, setError] = useState2(null);
1117
+ const [stripePromise, setStripePromise] = useState2(null);
1118
+ const [clientSecret, setClientSecret] = useState2(null);
1119
+ const [resolvedPlanRef, setResolvedPlanRef] = useState2(planRef || null);
1120
+ const isStartingRef = useRef2(false);
1121
+ const startCheckout = useCallback2(async () => {
1122
+ if (isStartingRef.current || loading) {
1123
+ return;
1124
+ }
1125
+ if (!planRef && !productRef) {
1126
+ setError(
1127
+ new Error(
1128
+ "useCheckout: either planRef or productRef is required. Pass planRef directly, or pass productRef to auto-resolve the plan."
1129
+ )
1130
+ );
1131
+ return;
1132
+ }
1133
+ isStartingRef.current = true;
1134
+ setLoading(true);
1135
+ setError(null);
1136
+ try {
1137
+ let effectivePlanRef = planRef;
1138
+ if (!effectivePlanRef && productRef) {
1139
+ effectivePlanRef = await resolvePlanRef(productRef, _config);
1140
+ setResolvedPlanRef(effectivePlanRef);
1141
+ }
1142
+ if (!effectivePlanRef) {
1143
+ throw new Error("Could not determine plan reference for checkout");
1144
+ }
1145
+ const result = await createPayment({
1146
+ planRef: effectivePlanRef,
1147
+ productRef,
1148
+ customer
1149
+ });
1150
+ if (!result || typeof result !== "object") {
1151
+ throw new Error("Invalid payment intent response from server");
1152
+ }
1153
+ if (!result.clientSecret || typeof result.clientSecret !== "string") {
1154
+ throw new Error("Invalid client secret in payment intent response");
1155
+ }
1156
+ if (!result.publishableKey || typeof result.publishableKey !== "string") {
1157
+ throw new Error("Invalid publishable key in payment intent response");
1158
+ }
1159
+ if (result.customerRef && result.customerRef !== customerRef && updateCustomerRef) {
1160
+ updateCustomerRef(result.customerRef);
1161
+ }
1162
+ const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
1163
+ const cacheKey = getStripeCacheKey(result.publishableKey, result.accountId);
1164
+ let stripe = stripePromiseCache.get(cacheKey);
1165
+ if (!stripe) {
1166
+ stripe = loadStripe(result.publishableKey, stripeOptions);
1167
+ stripePromiseCache.set(cacheKey, stripe);
1168
+ }
1169
+ setStripePromise(stripe);
1170
+ setClientSecret(result.clientSecret);
1171
+ } catch (err) {
1172
+ const error2 = err instanceof Error ? err : new Error("Failed to start checkout");
1173
+ setError(error2);
1174
+ } finally {
1175
+ setLoading(false);
1176
+ isStartingRef.current = false;
1177
+ }
1178
+ }, [planRef, productRef, customer, createPayment, updateCustomerRef, loading, _config]);
1179
+ const reset = useCallback2(() => {
1180
+ isStartingRef.current = false;
1181
+ setLoading(false);
1182
+ setError(null);
1183
+ setStripePromise(null);
1184
+ setClientSecret(null);
1185
+ setResolvedPlanRef(planRef || null);
1186
+ }, [planRef]);
1187
+ return {
1188
+ loading,
1189
+ error,
1190
+ stripePromise,
1191
+ clientSecret,
1192
+ resolvedPlanRef,
1193
+ startCheckout,
1194
+ reset
1195
+ };
1196
+ }
1197
+
1198
+ // src/hooks/usePurchase.ts
1199
+ function usePurchase() {
1200
+ const { purchase, refetchPurchase } = useSolvaPay();
1201
+ return {
1202
+ ...purchase,
1203
+ refetch: refetchPurchase
1204
+ };
1205
+ }
1206
+
1207
+ // src/hooks/useCustomer.ts
1208
+ function useCustomer() {
1209
+ const { purchase, customerRef } = useSolvaPay();
1210
+ return {
1211
+ customerRef: purchase.customerRef || customerRef,
1212
+ email: purchase.email,
1213
+ name: purchase.name,
1214
+ loading: purchase.loading
1215
+ };
1216
+ }
1217
+
1218
+ // src/hooks/useCopy.ts
1219
+ function useCopy() {
1220
+ return useCopyContext().copy;
1221
+ }
1222
+ function useLocale() {
1223
+ return useCopyContext().locale;
1224
+ }
1225
+
1226
+ // src/hooks/usePlans.ts
1227
+ import { useState as useState3, useEffect as useEffect2, useCallback as useCallback3, useMemo as useMemo3, useRef as useRef3 } from "react";
1228
+ var plansCache = /* @__PURE__ */ new Map();
1229
+ var CACHE_DURATION2 = 5 * 60 * 1e3;
1230
+ function processPlans(raw, filter, sortBy) {
1231
+ let result = sortBy ? [...raw].sort(sortBy) : raw;
1232
+ if (filter) result = result.filter(filter);
1233
+ return result;
1234
+ }
1235
+ function computeInitialIndex(plans, initialPlanRef, autoSelectFirstPaid) {
1236
+ if (plans.length === 0) return 0;
1237
+ if (initialPlanRef) {
1238
+ const idx = plans.findIndex((p) => p.reference === initialPlanRef);
1239
+ if (idx >= 0) return idx;
1240
+ }
1241
+ if (autoSelectFirstPaid) {
1242
+ const idx = plans.findIndex((p) => p.requiresPayment !== false);
1243
+ return idx >= 0 ? idx : 0;
1244
+ }
1245
+ return 0;
1246
+ }
1247
+ function usePlans(options) {
1248
+ const {
1249
+ fetcher,
1250
+ productRef,
1251
+ filter,
1252
+ sortBy,
1253
+ autoSelectFirstPaid = false,
1254
+ initialPlanRef,
1255
+ selectionReady = true
1256
+ } = options;
1257
+ const fetcherRef = useRef3(fetcher);
1258
+ const filterRef = useRef3(filter);
1259
+ const sortByRef = useRef3(sortBy);
1260
+ const autoSelectFirstPaidRef = useRef3(autoSelectFirstPaid);
1261
+ const initialPlanRefRef = useRef3(initialPlanRef);
1262
+ const selectionReadyRef = useRef3(selectionReady);
1263
+ const hasAppliedInitialRef = useRef3(false);
1264
+ const userHasSelectedRef = useRef3(false);
1265
+ const [selectedPlanIndex, setSelectedPlanIndexState] = useState3(() => {
1266
+ if (!selectionReady || !productRef) return 0;
1267
+ const cached = plansCache.get(productRef);
1268
+ if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
1269
+ return 0;
1270
+ }
1271
+ const processed = processPlans(cached.plans, filter, sortBy);
1272
+ if (processed.length === 0) return 0;
1273
+ const idx = computeInitialIndex(processed, initialPlanRef, autoSelectFirstPaid);
1274
+ hasAppliedInitialRef.current = true;
1275
+ return idx;
1276
+ });
1277
+ const [plans, setPlans] = useState3(() => {
1278
+ if (!productRef) return [];
1279
+ const cached = plansCache.get(productRef);
1280
+ if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
1281
+ return [];
1282
+ }
1283
+ return processPlans(cached.plans, filter, sortBy);
1284
+ });
1285
+ const [loading, setLoading] = useState3(() => plans.length === 0);
1286
+ const [error, setError] = useState3(null);
1287
+ useEffect2(() => {
1288
+ fetcherRef.current = fetcher;
1289
+ }, [fetcher]);
1290
+ useEffect2(() => {
1291
+ filterRef.current = filter;
1292
+ }, [filter]);
1293
+ useEffect2(() => {
1294
+ sortByRef.current = sortBy;
1295
+ }, [sortBy]);
1296
+ useEffect2(() => {
1297
+ autoSelectFirstPaidRef.current = autoSelectFirstPaid;
1298
+ }, [autoSelectFirstPaid]);
1299
+ useEffect2(() => {
1300
+ initialPlanRefRef.current = initialPlanRef;
1301
+ }, [initialPlanRef]);
1302
+ useEffect2(() => {
1303
+ selectionReadyRef.current = selectionReady;
1304
+ }, [selectionReady]);
1305
+ const setSelectedPlanIndex = useCallback3((index) => {
1306
+ userHasSelectedRef.current = true;
1307
+ setSelectedPlanIndexState(index);
1308
+ }, []);
1309
+ const applyInitialSelection = useCallback3((processedPlans) => {
1310
+ if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
1311
+ if (!selectionReadyRef.current || processedPlans.length === 0) return;
1312
+ hasAppliedInitialRef.current = true;
1313
+ const idx = computeInitialIndex(
1314
+ processedPlans,
1315
+ initialPlanRefRef.current,
1316
+ autoSelectFirstPaidRef.current
1317
+ );
1318
+ setSelectedPlanIndexState(idx);
1319
+ }, []);
1320
+ const fetchPlans = useCallback3(
1321
+ async (force = false) => {
1322
+ if (!productRef) {
1323
+ setError(new Error("Product reference not configured"));
1324
+ setLoading(false);
1325
+ return;
1326
+ }
1327
+ const cached = plansCache.get(productRef);
1328
+ const now = Date.now();
1329
+ if (!force && cached && now - cached.timestamp < CACHE_DURATION2) {
1330
+ const processedPlans = processPlans(
1331
+ cached.plans,
1332
+ filterRef.current,
1333
+ sortByRef.current
1334
+ );
1335
+ setPlans(processedPlans);
1336
+ setLoading(false);
1337
+ setError(null);
1338
+ applyInitialSelection(processedPlans);
1339
+ return;
1340
+ }
1341
+ if (cached?.promise) {
1342
+ try {
1343
+ setLoading(true);
1344
+ const fetchedPlans = await cached.promise;
1345
+ const processedPlans = processPlans(
1346
+ fetchedPlans,
1347
+ filterRef.current,
1348
+ sortByRef.current
1349
+ );
1350
+ setPlans(processedPlans);
1351
+ setError(null);
1352
+ applyInitialSelection(processedPlans);
1353
+ } catch (err) {
1354
+ setError(err instanceof Error ? err : new Error("Failed to load plans"));
1355
+ } finally {
1356
+ setLoading(false);
1357
+ }
1358
+ return;
1359
+ }
1360
+ try {
1361
+ setLoading(true);
1362
+ setError(null);
1363
+ const fetchPromise = fetcherRef.current(productRef);
1364
+ plansCache.set(productRef, { plans: [], timestamp: now, promise: fetchPromise });
1365
+ const fetchedPlans = await fetchPromise;
1366
+ plansCache.set(productRef, { plans: fetchedPlans, timestamp: now, promise: null });
1367
+ const processedPlans = processPlans(
1368
+ fetchedPlans,
1369
+ filterRef.current,
1370
+ sortByRef.current
1371
+ );
1372
+ setPlans(processedPlans);
1373
+ applyInitialSelection(processedPlans);
1374
+ } catch (err) {
1375
+ plansCache.delete(productRef);
1376
+ setError(err instanceof Error ? err : new Error("Failed to load plans"));
1377
+ } finally {
1378
+ setLoading(false);
1379
+ }
1380
+ },
1381
+ [productRef, applyInitialSelection]
1382
+ );
1383
+ useEffect2(() => {
1384
+ fetchPlans();
1385
+ }, [fetchPlans]);
1386
+ useEffect2(() => {
1387
+ if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
1388
+ if (!selectionReady || plans.length === 0) return;
1389
+ applyInitialSelection(plans);
1390
+ }, [selectionReady, plans, applyInitialSelection]);
1391
+ const selectedPlan = useMemo3(() => plans[selectedPlanIndex] || null, [plans, selectedPlanIndex]);
1392
+ const selectPlan = useCallback3(
1393
+ (planRef) => {
1394
+ const index = plans.findIndex((p) => p.reference === planRef);
1395
+ if (index >= 0) {
1396
+ setSelectedPlanIndex(index);
1397
+ }
1398
+ },
1399
+ [plans, setSelectedPlanIndex]
1400
+ );
1401
+ return {
1402
+ plans,
1403
+ loading,
1404
+ error,
1405
+ selectedPlanIndex,
1406
+ selectedPlan,
1407
+ setSelectedPlanIndex,
1408
+ selectPlan,
1409
+ refetch: () => fetchPlans(true),
1410
+ isSelectionReady: hasAppliedInitialRef.current
1411
+ };
1412
+ }
1413
+
1414
+ // src/hooks/usePlan.ts
1415
+ import { useCallback as useCallback4, useEffect as useEffect3, useState as useState4 } from "react";
1416
+ async function listPlans(productRef, config) {
1417
+ const base = config?.api?.listPlans || "/api/list-plans";
1418
+ const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
1419
+ const fetchFn = config?.fetch || fetch;
1420
+ const { headers } = await buildRequestHeaders(config);
1421
+ const res = await fetchFn(url, { method: "GET", headers });
1422
+ if (!res.ok) {
1423
+ const error = new Error(`Failed to fetch plans: ${res.statusText || res.status}`);
1424
+ config?.onError?.(error, "listPlans");
1425
+ throw error;
1426
+ }
1427
+ const data = await res.json();
1428
+ return data.plans ?? [];
1429
+ }
1430
+ function usePlan(options) {
1431
+ const { planRef, productRef } = options;
1432
+ const { _config } = useSolvaPay();
1433
+ const findPlan = useCallback4(
1434
+ (plans) => {
1435
+ if (!planRef) return null;
1436
+ return plans.find((p) => p.reference === planRef) || null;
1437
+ },
1438
+ [planRef]
1439
+ );
1440
+ const [plan, setPlan] = useState4(() => {
1441
+ if (!planRef || !productRef) return null;
1442
+ const cached = plansCache.get(productRef);
1443
+ if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2) return null;
1444
+ return findPlan(cached.plans);
1445
+ });
1446
+ const [loading, setLoading] = useState4(() => !!(planRef && !plan));
1447
+ const [error, setError] = useState4(null);
1448
+ const load = useCallback4(
1449
+ async (force = false) => {
1450
+ if (!planRef) {
1451
+ setPlan(null);
1452
+ setLoading(false);
1453
+ setError(null);
1454
+ return;
1455
+ }
1456
+ if (!productRef) {
1457
+ setLoading(false);
1458
+ setError(
1459
+ new Error("usePlan: productRef is required to resolve a plan reference")
1460
+ );
1461
+ return;
1462
+ }
1463
+ const cached = plansCache.get(productRef);
1464
+ const now = Date.now();
1465
+ if (!force && cached?.plans.length && now - cached.timestamp < CACHE_DURATION2) {
1466
+ const p = findPlan(cached.plans);
1467
+ setPlan(p);
1468
+ setLoading(false);
1469
+ setError(
1470
+ p ? null : new Error(
1471
+ `Plan "${planRef}" not found in product "${productRef}"`
1472
+ )
1473
+ );
1474
+ return;
1475
+ }
1476
+ if (!force && cached?.promise) {
1477
+ try {
1478
+ setLoading(true);
1479
+ const plans = await cached.promise;
1480
+ const p = findPlan(plans);
1481
+ setPlan(p);
1482
+ setError(
1483
+ p ? null : new Error(
1484
+ `Plan "${planRef}" not found in product "${productRef}"`
1485
+ )
1486
+ );
1487
+ } catch (err) {
1488
+ setError(err instanceof Error ? err : new Error("Failed to load plan"));
1489
+ } finally {
1490
+ setLoading(false);
1491
+ }
1492
+ return;
1493
+ }
1494
+ try {
1495
+ setLoading(true);
1496
+ setError(null);
1497
+ const promise = listPlans(productRef, _config);
1498
+ plansCache.set(productRef, { plans: [], timestamp: now, promise });
1499
+ const plans = await promise;
1500
+ plansCache.set(productRef, { plans, timestamp: now, promise: null });
1501
+ const p = findPlan(plans);
1502
+ setPlan(p);
1503
+ if (!p) {
1504
+ setError(new Error(`Plan "${planRef}" not found in product "${productRef}"`));
1505
+ }
1506
+ } catch (err) {
1507
+ plansCache.delete(productRef);
1508
+ setError(err instanceof Error ? err : new Error("Failed to load plan"));
1509
+ } finally {
1510
+ setLoading(false);
1511
+ }
1512
+ },
1513
+ [planRef, productRef, _config, findPlan]
1514
+ );
1515
+ useEffect3(() => {
1516
+ load();
1517
+ }, [load]);
1518
+ return {
1519
+ plan,
1520
+ loading,
1521
+ error,
1522
+ refetch: () => load(true)
1523
+ };
1524
+ }
1525
+
1526
+ // src/hooks/useProduct.ts
1527
+ import { useCallback as useCallback5, useEffect as useEffect4, useState as useState5 } from "react";
1528
+
1529
+ // src/transport/cache-key.ts
1530
+ var transportIds = /* @__PURE__ */ new WeakMap();
1531
+ var nextTransportId = 0;
1532
+ function transportIdFor(transport) {
1533
+ let id = transportIds.get(transport);
1534
+ if (id === void 0) {
1535
+ id = ++nextTransportId;
1536
+ transportIds.set(transport, id);
1537
+ }
1538
+ return id;
1539
+ }
1540
+ function createTransportCacheKey(config, fallbackRoute, suffix) {
1541
+ const transport = config?.transport;
1542
+ if (transport) {
1543
+ const id = transportIdFor(transport);
1544
+ return suffix ? `transport:${id}:${suffix}` : `transport:${id}`;
1545
+ }
1546
+ return fallbackRoute;
1547
+ }
1548
+
1549
+ // src/hooks/useProduct.ts
1550
+ var productCache = /* @__PURE__ */ new Map();
1551
+ var CACHE_DURATION3 = 5 * 60 * 1e3;
1552
+ function cacheKeyFor(config, productRef) {
1553
+ return createTransportCacheKey(config, productRef, productRef);
1554
+ }
1555
+ async function fetchProduct(productRef, config) {
1556
+ const transport = config?.transport ?? createHttpTransport(config);
1557
+ if (!transport.getProduct) return null;
1558
+ return transport.getProduct(productRef);
1559
+ }
1560
+ function useProduct(productRef) {
1561
+ const { _config } = useSolvaPay();
1562
+ const cacheKey = productRef ? cacheKeyFor(_config, productRef) : "";
1563
+ const [product, setProduct] = useState5(
1564
+ () => productRef ? productCache.get(cacheKey)?.product ?? null : null
1565
+ );
1566
+ const [loading, setLoading] = useState5(() => {
1567
+ if (!productRef) return false;
1568
+ const cached = productCache.get(cacheKey);
1569
+ return !cached || !cached.product && !cached.promise;
1570
+ });
1571
+ const [error, setError] = useState5(null);
1572
+ const load = useCallback5(
1573
+ async (force = false) => {
1574
+ if (!productRef) {
1575
+ setProduct(null);
1576
+ setLoading(false);
1577
+ setError(null);
1578
+ return;
1579
+ }
1580
+ const key = cacheKeyFor(_config, productRef);
1581
+ const cached = productCache.get(key);
1582
+ const now = Date.now();
1583
+ if (!force && cached?.product && now - cached.timestamp < CACHE_DURATION3) {
1584
+ setProduct(cached.product);
1585
+ setLoading(false);
1586
+ setError(null);
1587
+ return;
1588
+ }
1589
+ if (!force && cached?.promise) {
1590
+ try {
1591
+ setLoading(true);
1592
+ const p = await cached.promise;
1593
+ setProduct(p);
1594
+ setError(null);
1595
+ } catch (err) {
1596
+ setError(err instanceof Error ? err : new Error("Failed to load product"));
1597
+ } finally {
1598
+ setLoading(false);
1599
+ }
1600
+ return;
1601
+ }
1602
+ try {
1603
+ setLoading(true);
1604
+ setError(null);
1605
+ const promise = fetchProduct(productRef, _config);
1606
+ productCache.set(key, {
1607
+ product: cached?.product ?? null,
1608
+ promise,
1609
+ timestamp: now
1610
+ });
1611
+ const p = await promise;
1612
+ if (p === null) {
1613
+ productCache.set(key, {
1614
+ product: cached?.product ?? null,
1615
+ promise: null,
1616
+ timestamp: cached?.timestamp ?? now
1617
+ });
1618
+ setProduct(cached?.product ?? null);
1619
+ setLoading(false);
1620
+ return;
1621
+ }
1622
+ productCache.set(key, { product: p, promise: null, timestamp: now });
1623
+ setProduct(p);
1624
+ } catch (err) {
1625
+ productCache.delete(key);
1626
+ setError(err instanceof Error ? err : new Error("Failed to load product"));
1627
+ } finally {
1628
+ setLoading(false);
1629
+ }
1630
+ },
1631
+ [productRef, _config]
1632
+ );
1633
+ useEffect4(() => {
1634
+ load();
1635
+ }, [load]);
1636
+ return {
1637
+ product,
1638
+ loading,
1639
+ error,
1640
+ refetch: () => load(true)
1641
+ };
1642
+ }
1643
+
1644
+ // src/hooks/useActivation.ts
1645
+ import { useState as useState6, useCallback as useCallback6 } from "react";
1646
+ function useActivation() {
1647
+ const { activatePlan } = useSolvaPay();
1648
+ const copy = useCopy();
1649
+ const [state, setState] = useState6("idle");
1650
+ const [error, setError] = useState6(null);
1651
+ const [result, setResult] = useState6(null);
1652
+ const activate = useCallback6(
1653
+ async (params) => {
1654
+ setState("activating");
1655
+ setError(null);
1656
+ setResult(null);
1657
+ try {
1658
+ const data = await activatePlan(params);
1659
+ setResult(data);
1660
+ switch (data.status) {
1661
+ case "activated":
1662
+ case "already_active":
1663
+ setState("activated");
1664
+ break;
1665
+ case "topup_required":
1666
+ setState("topup_required");
1667
+ break;
1668
+ case "payment_required":
1669
+ setError(copy.activation.paymentRequired);
1670
+ setState("payment_required");
1671
+ break;
1672
+ case "invalid":
1673
+ setError(data.message || copy.activation.invalidConfiguration);
1674
+ setState("error");
1675
+ break;
1676
+ default:
1677
+ setError(copy.activation.unexpectedResponse);
1678
+ setState("error");
1679
+ }
1680
+ } catch (err) {
1681
+ setError(err instanceof Error ? err.message : copy.activation.failed);
1682
+ setState("error");
1683
+ }
1684
+ },
1685
+ [activatePlan, copy]
1686
+ );
1687
+ const reset = useCallback6(() => {
1688
+ setState("idle");
1689
+ setError(null);
1690
+ setResult(null);
1691
+ }, []);
1692
+ return { activate, state, error, result, reset };
1693
+ }
1694
+
1695
+ // src/components/PaymentFormContext.tsx
1696
+ import { createContext as createContext3, useContext as useContext3 } from "react";
1697
+ import { jsx as jsx4 } from "react/jsx-runtime";
1698
+ var PaymentFormContext = createContext3(null);
1699
+ function usePaymentForm() {
1700
+ const ctx = useContext3(PaymentFormContext);
1701
+ if (!ctx) {
1702
+ throw new Error(
1703
+ "PaymentForm subcomponents must be used inside a <PaymentForm>. Wrap the slots with <PaymentForm planRef=... productRef=...>."
1704
+ );
1705
+ }
1706
+ return ctx;
1707
+ }
1708
+ var PaymentFormProvider = ({ value, children }) => /* @__PURE__ */ jsx4(PaymentFormContext.Provider, { value, children });
1709
+
1710
+ // src/utils/format.ts
1711
+ var ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
1712
+ "bif",
1713
+ "clp",
1714
+ "djf",
1715
+ "gnf",
1716
+ "jpy",
1717
+ "kmf",
1718
+ "krw",
1719
+ "mga",
1720
+ "pyg",
1721
+ "rwf",
1722
+ "ugx",
1723
+ "vnd",
1724
+ "vuv",
1725
+ "xaf",
1726
+ "xof",
1727
+ "xpf"
1728
+ ]);
1729
+ function getFractionDigits(currency) {
1730
+ return ZERO_DECIMAL_CURRENCIES.has(currency.toLowerCase()) ? 0 : 2;
1731
+ }
1732
+ function getMinorUnitsPerMajor(currency) {
1733
+ return getFractionDigits(currency) === 0 ? 1 : 100;
1734
+ }
1735
+ function toMajorUnits(amountMinor, currency) {
1736
+ const fractionDigits = getFractionDigits(currency);
1737
+ return fractionDigits === 0 ? amountMinor : amountMinor / 100;
1738
+ }
1739
+ function formatPrice(amountMinor, currency, opts = {}) {
1740
+ const { locale, interval, intervalCount = 1, free = "Free" } = opts;
1741
+ if (amountMinor === 0 && free !== "") {
1742
+ return free;
1743
+ }
1744
+ const naturalFractionDigits = getFractionDigits(currency);
1745
+ const minorPerMajor = getMinorUnitsPerMajor(currency);
1746
+ const isWhole = amountMinor % minorPerMajor === 0;
1747
+ const fractionDigits = isWhole ? 0 : naturalFractionDigits;
1748
+ const major = toMajorUnits(amountMinor, currency);
1749
+ const formatter = new Intl.NumberFormat(locale, {
1750
+ style: "currency",
1751
+ currency: currency.toUpperCase(),
1752
+ minimumFractionDigits: fractionDigits,
1753
+ maximumFractionDigits: fractionDigits
1754
+ });
1755
+ const formatted = formatter.format(major);
1756
+ if (!interval) return formatted;
1757
+ const suffix = intervalCount > 1 ? `${intervalCount} ${interval}s` : interval;
1758
+ return `${formatted} / ${suffix}`;
1759
+ }
1760
+
1761
+ // src/i18n/interpolate.ts
1762
+ function interpolate(template, vars) {
1763
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
1764
+ const value = vars[key];
1765
+ if (value === void 0 || value === null) return match;
1766
+ return String(value);
1767
+ });
1768
+ }
1769
+
1770
+ // src/primitives/CheckoutSummary.tsx
1771
+ import { createContext as createContext5, forwardRef, useContext as useContext5, useMemo as useMemo4 } from "react";
1772
+
1773
+ // src/components/PlanSelectionContext.tsx
1774
+ import { createContext as createContext4, useContext as useContext4 } from "react";
1775
+ import { jsx as jsx5 } from "react/jsx-runtime";
1776
+ var PlanSelectionContext = createContext4(null);
1777
+ var PlanSelectionProvider = ({
1778
+ value,
1779
+ children
1780
+ }) => /* @__PURE__ */ jsx5(PlanSelectionContext.Provider, { value, children });
1781
+ function usePlanSelection() {
1782
+ return useContext4(PlanSelectionContext);
1783
+ }
1784
+
1785
+ // src/utils/errors.ts
1786
+ var DOCS_BASE_URL = "https://solvapay.com/docs";
1787
+ var SolvaPayError = class extends Error {
1788
+ code;
1789
+ docsUrl;
1790
+ constructor(code, message, docsUrl) {
1791
+ super(message);
1792
+ this.name = "SolvaPayError";
1793
+ this.code = code;
1794
+ this.docsUrl = docsUrl;
1795
+ }
1796
+ };
1797
+ var MissingProviderError = class extends SolvaPayError {
1798
+ constructor(primitiveName) {
1799
+ const docsUrl = `${DOCS_BASE_URL}/troubleshooting/missing-provider`;
1800
+ super(
1801
+ "MISSING_PROVIDER",
1802
+ `${primitiveName} must be rendered inside <SolvaPayProvider>. Wrap your app (or the section using ${primitiveName}) with <SolvaPayProvider config={{ ... }}>. See ${docsUrl}`,
1803
+ docsUrl
1804
+ );
1805
+ this.name = "MissingProviderError";
1806
+ }
1807
+ };
1808
+ var MissingProductRefError = class extends SolvaPayError {
1809
+ constructor(primitiveName) {
1810
+ const docsUrl = `${DOCS_BASE_URL}/primitives/product-ref`;
1811
+ super(
1812
+ "MISSING_PRODUCT_REF",
1813
+ `${primitiveName} requires a productRef. Pass the \`productRef\` prop (or set NEXT_PUBLIC_SOLVAPAY_PRODUCT_REF). See ${docsUrl}`,
1814
+ docsUrl
1815
+ );
1816
+ this.name = "MissingProductRefError";
1817
+ }
1818
+ };
1819
+
1820
+ // src/primitives/CheckoutSummary.tsx
1821
+ import { jsx as jsx6 } from "react/jsx-runtime";
1822
+ var CheckoutSummaryContext = createContext5(null);
1823
+ function useCheckoutSummaryContext(part) {
1824
+ const ctx = useContext5(CheckoutSummaryContext);
1825
+ if (!ctx) {
1826
+ throw new Error(
1827
+ `CheckoutSummary.${part} must be rendered inside <CheckoutSummary.Root>.`
1828
+ );
1829
+ }
1830
+ return ctx;
1831
+ }
1832
+ var Root = forwardRef(function CheckoutSummaryRoot({ planRef, productRef, asChild, children, ...rest }, forwardedRef) {
1833
+ const solva = useContext5(SolvaPayContext);
1834
+ if (!solva) throw new MissingProviderError("CheckoutSummary");
1835
+ const locale = useLocale();
1836
+ const copy = useCopy();
1837
+ const planSelection = usePlanSelection();
1838
+ const resolvedPlanRef = planRef ?? planSelection?.selectedPlanRef ?? void 0;
1839
+ const resolvedProductRef = productRef ?? planSelection?.productRef;
1840
+ const { plan, loading: planLoading } = usePlan({
1841
+ planRef: resolvedPlanRef,
1842
+ productRef: resolvedProductRef
1843
+ });
1844
+ const { product, loading: productLoading } = useProduct(resolvedProductRef);
1845
+ const loading = planLoading || productLoading;
1846
+ const priceFormatted = useMemo4(() => {
1847
+ const price = plan?.price ?? 0;
1848
+ const currency = plan?.currency ?? "usd";
1849
+ return formatPrice(price, currency, {
1850
+ locale,
1851
+ interval: plan?.interval,
1852
+ intervalCount: 1,
1853
+ free: copy.interval.free
1854
+ });
1855
+ }, [plan, locale, copy.interval.free]);
1856
+ const trialBanner = useMemo4(() => {
1857
+ const trialDays = plan?.trialDays;
1858
+ if (!trialDays || trialDays <= 0) return null;
1859
+ return interpolate(copy.interval.trial, { trialDays });
1860
+ }, [plan?.trialDays, copy.interval.trial]);
1861
+ const ctx = useMemo4(
1862
+ () => ({ plan, product, priceFormatted, trialBanner, loading }),
1863
+ [plan, product, priceFormatted, trialBanner, loading]
1864
+ );
1865
+ const Comp = asChild ? Slot : "div";
1866
+ return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary": "", ...rest, children: /* @__PURE__ */ jsx6(CheckoutSummaryContext.Provider, { value: ctx, children }) });
1867
+ });
1868
+ var ProductSlot = forwardRef(function CheckoutSummaryProduct({ asChild, children, ...rest }, forwardedRef) {
1869
+ const ctx = useCheckoutSummaryContext("Product");
1870
+ const name = ctx.product?.name;
1871
+ if (!name) return null;
1872
+ const Comp = asChild ? Slot : "span";
1873
+ return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-product": "", ...rest, children: children ?? name });
1874
+ });
1875
+ var PlanSlot = forwardRef(function CheckoutSummaryPlan({ asChild, children, ...rest }, forwardedRef) {
1876
+ const ctx = useCheckoutSummaryContext("Plan");
1877
+ const name = ctx.plan?.name;
1878
+ if (!name) return null;
1879
+ const Comp = asChild ? Slot : "span";
1880
+ return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-plan": "", ...rest, children: children ?? name });
1881
+ });
1882
+ var PriceSlot = forwardRef(function CheckoutSummaryPrice({ asChild, children, ...rest }, forwardedRef) {
1883
+ const ctx = useCheckoutSummaryContext("Price");
1884
+ const Comp = asChild ? Slot : "span";
1885
+ return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-price": "", ...rest, children: children ?? ctx.priceFormatted });
1886
+ });
1887
+ var TrialSlot = forwardRef(function CheckoutSummaryTrial({ asChild, children, ...rest }, forwardedRef) {
1888
+ const ctx = useCheckoutSummaryContext("Trial");
1889
+ if (!ctx.trialBanner) return null;
1890
+ const Comp = asChild ? Slot : "span";
1891
+ return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-trial": "", ...rest, children: children ?? ctx.trialBanner });
1892
+ });
1893
+ var TaxNoteSlot = forwardRef(function CheckoutSummaryTaxNote({ asChild, children, ...rest }, forwardedRef) {
1894
+ useCheckoutSummaryContext("TaxNote");
1895
+ const Comp = asChild ? Slot : "span";
1896
+ return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-tax-note": "", ...rest, children: children ?? "Taxes calculated at checkout" });
1897
+ });
1898
+ var CheckoutSummaryRoot2 = Root;
1899
+ var CheckoutSummaryProduct2 = ProductSlot;
1900
+ var CheckoutSummaryPlan2 = PlanSlot;
1901
+ var CheckoutSummaryPrice2 = PriceSlot;
1902
+ var CheckoutSummaryTrial2 = TrialSlot;
1903
+ var CheckoutSummaryTaxNote2 = TaxNoteSlot;
1904
+ var CheckoutSummary = {
1905
+ Root,
1906
+ Product: ProductSlot,
1907
+ Plan: PlanSlot,
1908
+ Price: PriceSlot,
1909
+ Trial: TrialSlot,
1910
+ TaxNote: TaxNoteSlot
1911
+ };
1912
+ function useCheckoutSummary() {
1913
+ return useCheckoutSummaryContext("useCheckoutSummary");
1914
+ }
1915
+
1916
+ // src/components/CheckoutSummary.tsx
1917
+ import { jsx as jsx7, jsxs } from "react/jsx-runtime";
1918
+ var CheckoutSummary2 = ({
1919
+ planRef,
1920
+ productRef,
1921
+ showTrial = true,
1922
+ showTaxNote = false,
1923
+ className
1924
+ }) => {
1925
+ const rootClass = ["solvapay-checkout-summary", className].filter(Boolean).join(" ");
1926
+ return /* @__PURE__ */ jsxs(CheckoutSummary.Root, { planRef, productRef, className: rootClass, children: [
1927
+ /* @__PURE__ */ jsx7(CheckoutSummary.Product, { className: "solvapay-checkout-summary-product" }),
1928
+ /* @__PURE__ */ jsxs("div", { className: "solvapay-checkout-summary-row", children: [
1929
+ /* @__PURE__ */ jsx7(CheckoutSummary.Plan, { className: "solvapay-checkout-summary-plan" }),
1930
+ /* @__PURE__ */ jsx7(CheckoutSummary.Price, { className: "solvapay-checkout-summary-price" })
1931
+ ] }),
1932
+ showTrial && /* @__PURE__ */ jsx7(CheckoutSummary.Trial, { className: "solvapay-checkout-summary-trial" }),
1933
+ showTaxNote && /* @__PURE__ */ jsx7(CheckoutSummary.TaxNote, { className: "solvapay-checkout-summary-tax-note" })
1934
+ ] });
1935
+ };
1936
+
1937
+ // src/hooks/useMerchant.ts
1938
+ import { useCallback as useCallback7, useEffect as useEffect5, useState as useState7 } from "react";
1939
+ var merchantCache = /* @__PURE__ */ new Map();
1940
+ var CACHE_DURATION4 = 5 * 60 * 1e3;
1941
+ function cacheKeyFor2(config) {
1942
+ return createTransportCacheKey(config, config?.api?.getMerchant || "/api/merchant");
1943
+ }
1944
+ async function fetchMerchant(config) {
1945
+ const transport = config?.transport ?? createHttpTransport(config);
1946
+ if (!transport.getMerchant) return null;
1947
+ return transport.getMerchant();
1948
+ }
1949
+ function useMerchant() {
1950
+ const { _config } = useSolvaPay();
1951
+ const key = cacheKeyFor2(_config);
1952
+ const [merchant, setMerchant] = useState7(
1953
+ () => merchantCache.get(key)?.merchant ?? null
1954
+ );
1955
+ const [loading, setLoading] = useState7(() => {
1956
+ const cached = merchantCache.get(key);
1957
+ return !cached || !cached.merchant && !cached.promise;
1958
+ });
1959
+ const [error, setError] = useState7(null);
1960
+ const load = useCallback7(
1961
+ async (force = false) => {
1962
+ const cached = merchantCache.get(key);
1963
+ const now = Date.now();
1964
+ if (!force && cached?.merchant && now - cached.timestamp < CACHE_DURATION4) {
1965
+ setMerchant(cached.merchant);
1966
+ setLoading(false);
1967
+ setError(null);
1968
+ return;
1969
+ }
1970
+ if (!force && cached?.promise) {
1971
+ try {
1972
+ setLoading(true);
1973
+ const m = await cached.promise;
1974
+ setMerchant(m);
1975
+ setError(null);
1976
+ } catch (err) {
1977
+ setError(err instanceof Error ? err : new Error("Failed to load merchant"));
1978
+ } finally {
1979
+ setLoading(false);
1980
+ }
1981
+ return;
1982
+ }
1983
+ try {
1984
+ setLoading(true);
1985
+ setError(null);
1986
+ const promise = fetchMerchant(_config);
1987
+ merchantCache.set(key, {
1988
+ merchant: cached?.merchant ?? null,
1989
+ promise,
1990
+ timestamp: now
1991
+ });
1992
+ const m = await promise;
1993
+ if (m === null) {
1994
+ merchantCache.set(key, {
1995
+ merchant: cached?.merchant ?? null,
1996
+ promise: null,
1997
+ timestamp: cached?.timestamp ?? now
1998
+ });
1999
+ setMerchant(cached?.merchant ?? null);
2000
+ setLoading(false);
2001
+ return;
2002
+ }
2003
+ merchantCache.set(key, { merchant: m, promise: null, timestamp: now });
2004
+ setMerchant(m);
2005
+ } catch (err) {
2006
+ merchantCache.delete(key);
2007
+ setError(err instanceof Error ? err : new Error("Failed to load merchant"));
2008
+ } finally {
2009
+ setLoading(false);
2010
+ }
2011
+ },
2012
+ [_config, key]
2013
+ );
2014
+ useEffect5(() => {
2015
+ load();
2016
+ }, [load]);
2017
+ return {
2018
+ merchant,
2019
+ loading,
2020
+ error,
2021
+ refetch: () => load(true)
2022
+ };
2023
+ }
2024
+
2025
+ // src/utils/checkoutVariant.ts
2026
+ function deriveVariant(plan, mode) {
2027
+ if (mode === "topup") return "topup";
2028
+ if (!plan?.type) return "oneTime";
2029
+ if (plan.type === "usage-based") {
2030
+ return plan.billingModel === "post-paid" ? "usageMetered" : "topup";
2031
+ }
2032
+ if (plan.requiresPayment === false) return "freeTier";
2033
+ switch (plan.type) {
2034
+ case "recurring":
2035
+ return "recurring";
2036
+ case "one-time":
2037
+ default:
2038
+ return "oneTime";
2039
+ }
2040
+ }
2041
+
2042
+ // src/primitives/MandateText.tsx
2043
+ import { forwardRef as forwardRef2, useContext as useContext6, useMemo as useMemo5 } from "react";
2044
+ import { jsx as jsx8 } from "react/jsx-runtime";
2045
+ var MandateText = forwardRef2(
2046
+ function MandateText2({
2047
+ planRef,
2048
+ productRef,
2049
+ variant,
2050
+ mode,
2051
+ amountMinor,
2052
+ currency,
2053
+ asChild,
2054
+ children,
2055
+ ...rest
2056
+ }, forwardedRef) {
2057
+ const solva = useContext6(SolvaPayContext);
2058
+ if (!solva) throw new MissingProviderError("MandateText");
2059
+ const locale = useLocale();
2060
+ const copy = useCopy();
2061
+ const planSelection = usePlanSelection();
2062
+ const resolvedPlanRef = planRef ?? planSelection?.selectedPlanRef ?? void 0;
2063
+ const resolvedProductRef = productRef ?? planSelection?.productRef;
2064
+ const { plan } = usePlan({ planRef: resolvedPlanRef, productRef: resolvedProductRef });
2065
+ const { product } = useProduct(resolvedProductRef);
2066
+ const { merchant } = useMerchant();
2067
+ const resolvedVariant = variant || deriveVariant(plan, mode);
2068
+ const effectiveAmount = amountMinor ?? plan?.price ?? 0;
2069
+ const effectiveCurrency = currency ?? plan?.currency ?? merchant?.defaultCurrency ?? "usd";
2070
+ const amountFormatted = formatPrice(effectiveAmount, effectiveCurrency, {
2071
+ locale,
2072
+ free: copy.interval.free
2073
+ });
2074
+ const ctx = useMemo5(
2075
+ () => ({
2076
+ merchant: {
2077
+ legalName: merchant?.legalName ?? merchant?.displayName ?? "",
2078
+ displayName: merchant?.displayName,
2079
+ supportEmail: merchant?.supportEmail,
2080
+ termsUrl: merchant?.termsUrl,
2081
+ privacyUrl: merchant?.privacyUrl
2082
+ },
2083
+ plan: plan ? {
2084
+ name: plan.name,
2085
+ interval: plan.interval,
2086
+ intervalCount: 1,
2087
+ trialDays: plan.trialDays,
2088
+ measures: plan.measures,
2089
+ billingCycle: plan.billingCycle
2090
+ } : void 0,
2091
+ product: product ? { name: product.name } : void 0,
2092
+ amountFormatted,
2093
+ trialDays: plan?.trialDays
2094
+ }),
2095
+ [merchant, plan, product, amountFormatted]
2096
+ );
2097
+ const template = copy.mandate[resolvedVariant];
2098
+ const text = typeof template === "function" ? template(ctx) : template;
2099
+ if (!text) return null;
2100
+ const Comp = asChild ? Slot : "p";
2101
+ return /* @__PURE__ */ jsx8(
2102
+ Comp,
2103
+ {
2104
+ ref: forwardedRef,
2105
+ "data-solvapay-mandate-text": "",
2106
+ "data-variant": resolvedVariant,
2107
+ ...rest,
2108
+ children: children ?? text
2109
+ }
2110
+ );
2111
+ }
2112
+ );
2113
+
2114
+ // src/components/Spinner.tsx
2115
+ import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
2116
+ var SIZE_PX = { sm: 16, md: 20, lg: 24 };
2117
+ var Spinner = ({ className, size = "md" }) => {
2118
+ const px = SIZE_PX[size];
2119
+ return /* @__PURE__ */ jsxs2(
2120
+ "svg",
2121
+ {
2122
+ "data-solvapay-spinner": "",
2123
+ className,
2124
+ width: px,
2125
+ height: px,
2126
+ viewBox: "0 0 24 24",
2127
+ fill: "none",
2128
+ "aria-hidden": "true",
2129
+ children: [
2130
+ /* @__PURE__ */ jsx9(
2131
+ "circle",
2132
+ {
2133
+ cx: "12",
2134
+ cy: "12",
2135
+ r: "10",
2136
+ stroke: "currentColor",
2137
+ strokeWidth: "4",
2138
+ strokeOpacity: "0.25"
2139
+ }
2140
+ ),
2141
+ /* @__PURE__ */ jsx9(
2142
+ "path",
2143
+ {
2144
+ fill: "currentColor",
2145
+ fillOpacity: "0.75",
2146
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
2147
+ }
2148
+ )
2149
+ ]
2150
+ }
2151
+ );
2152
+ };
2153
+
2154
+ // src/utils/confirmPayment.ts
2155
+ async function confirmPayment(input) {
2156
+ const { stripe, elements, clientSecret, mode, returnUrl, billingDetails, copy } = input;
2157
+ try {
2158
+ if (mode === "payment-element") {
2159
+ const paymentElement = elements.getElement("payment");
2160
+ if (!paymentElement) {
2161
+ return { status: "error", message: copy.errors.cardElementMissing };
2162
+ }
2163
+ const { error: submitError } = await elements.submit();
2164
+ if (submitError) {
2165
+ return {
2166
+ status: "error",
2167
+ message: submitError.message || copy.errors.paymentUnexpected
2168
+ };
2169
+ }
2170
+ const { error: error2, paymentIntent: paymentIntent2 } = await stripe.confirmPayment({
2171
+ elements,
2172
+ clientSecret,
2173
+ confirmParams: {
2174
+ return_url: returnUrl,
2175
+ payment_method_data: billingDetails ? { billing_details: billingDetails } : void 0
2176
+ },
2177
+ redirect: "if_required"
2178
+ });
2179
+ if (error2) {
2180
+ return { status: "error", message: error2.message || copy.errors.paymentUnexpected };
2181
+ }
2182
+ return mapIntent(paymentIntent2, copy);
2183
+ }
2184
+ const cardElement = elements.getElement("card");
2185
+ if (!cardElement) {
2186
+ return { status: "error", message: copy.errors.cardElementMissing };
2187
+ }
2188
+ const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
2189
+ payment_method: {
2190
+ card: cardElement,
2191
+ billing_details: billingDetails
2192
+ }
2193
+ });
2194
+ if (error) {
2195
+ return { status: "error", message: error.message || copy.errors.paymentUnexpected };
2196
+ }
2197
+ return mapIntent(paymentIntent, copy);
2198
+ } catch (err) {
2199
+ return {
2200
+ status: "error",
2201
+ message: err instanceof Error ? err.message : copy.errors.paymentUnexpected
2202
+ };
2203
+ }
2204
+ }
2205
+ function mapIntent(paymentIntent, copy) {
2206
+ if (!paymentIntent) {
2207
+ return { status: "error", message: copy.errors.paymentUnexpected };
2208
+ }
2209
+ if (paymentIntent.status === "succeeded") {
2210
+ return { status: "succeeded", paymentIntent };
2211
+ }
2212
+ if (paymentIntent.status === "requires_action") {
2213
+ return { status: "requires_action", message: copy.errors.paymentRequires3ds };
2214
+ }
2215
+ return {
2216
+ status: "other",
2217
+ message: interpolate(copy.errors.paymentStatusPrefix, {
2218
+ status: paymentIntent.status || "processing"
2219
+ }),
2220
+ paymentIntent
2221
+ };
2222
+ }
2223
+
2224
+ // src/utils/checkoutCta.ts
2225
+ function resolveCta(input) {
2226
+ if (input.override) return input.override;
2227
+ const { variant, plan, product, amountFormatted, copy } = input;
2228
+ if (variant === "recurring") {
2229
+ if (plan?.trialDays && plan.trialDays > 0) {
2230
+ return interpolate(copy.cta.trialStart, { trialDays: plan.trialDays });
2231
+ }
2232
+ return copy.cta.subscribe;
2233
+ }
2234
+ if (variant === "oneTime") {
2235
+ return interpolate(copy.cta.payAmount, { amount: amountFormatted });
2236
+ }
2237
+ if (variant === "topup") {
2238
+ return interpolate(copy.cta.addAmount, { amount: amountFormatted });
2239
+ }
2240
+ if (variant === "usageMetered" || variant === "freeTier") {
2241
+ const productName = product?.name ?? (typeof plan?.metadata?.productName === "string" ? plan.metadata.productName : plan?.name ?? "service");
2242
+ return interpolate(copy.cta.startUsing, { product: productName });
2243
+ }
2244
+ return copy.cta.payNow;
2245
+ }
2246
+
2247
+ // src/primitives/PaymentForm.tsx
2248
+ import {
2249
+ forwardRef as forwardRef3,
2250
+ useCallback as useCallback8,
2251
+ useContext as useContext7,
2252
+ useEffect as useEffect6,
2253
+ useMemo as useMemo6,
2254
+ useRef as useRef4,
2255
+ useState as useState8
2256
+ } from "react";
2257
+ import {
2258
+ Elements,
2259
+ useStripe,
2260
+ useElements,
2261
+ PaymentElement as StripePaymentElement,
2262
+ CardElement as StripeCardElement
2263
+ } from "@stripe/react-stripe-js";
2264
+
2265
+ // src/utils/processPaymentResult.ts
2266
+ async function reconcilePayment(input) {
2267
+ const { paymentIntentId, productRef, planRef, processPayment, refetchPurchase, copy } = input;
2268
+ if (!processPayment || !productRef) {
2269
+ try {
2270
+ await refetchPurchase();
2271
+ return { status: "success" };
2272
+ } catch (err) {
2273
+ return {
2274
+ status: "error",
2275
+ error: err instanceof Error ? err : new Error(String(err))
2276
+ };
2277
+ }
2278
+ }
2279
+ try {
2280
+ const result = await processPayment({ paymentIntentId, productRef, planRef });
2281
+ const isTimeout = result?.status === "timeout";
2282
+ if (isTimeout) {
2283
+ for (let attempt = 1; attempt <= 5; attempt++) {
2284
+ await new Promise((resolve) => setTimeout(resolve, attempt * 1e3));
2285
+ await refetchPurchase();
2286
+ }
2287
+ return {
2288
+ status: "timeout",
2289
+ error: new Error(copy.errors.paymentProcessingTimeout)
2290
+ };
2291
+ }
2292
+ await refetchPurchase();
2293
+ return { status: "success" };
2294
+ } catch (err) {
2295
+ return {
2296
+ status: "error",
2297
+ error: err instanceof Error ? err : new Error(String(err))
2298
+ };
2299
+ }
2300
+ }
2301
+
2302
+ // src/primitives/PaymentForm.tsx
2303
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
2304
+ function toSubmitVariant(variant) {
2305
+ switch (variant) {
2306
+ case "freeTier":
2307
+ return "free";
2308
+ case "topup":
2309
+ return "topup";
2310
+ case "usageMetered":
2311
+ return "activate";
2312
+ default:
2313
+ return "paid";
2314
+ }
2315
+ }
2316
+ var Root2 = forwardRef3(function PaymentFormRoot(props, forwardedRef) {
2317
+ const {
2318
+ planRef,
2319
+ productRef,
2320
+ onSuccess,
2321
+ onResult,
2322
+ onFreePlan,
2323
+ onError,
2324
+ returnUrl,
2325
+ submitButtonText,
2326
+ className,
2327
+ buttonClassName,
2328
+ prefillCustomer,
2329
+ requireTermsAcceptance = false,
2330
+ children
2331
+ } = props;
2332
+ const solva = useContext7(SolvaPayContext);
2333
+ if (!solva) throw new MissingProviderError("PaymentForm");
2334
+ const copy = useCopy();
2335
+ const locale = useLocale();
2336
+ const planSelection = usePlanSelection();
2337
+ const effectivePlanRef = planRef ?? planSelection?.selectedPlanRef ?? void 0;
2338
+ const effectiveProductRef = productRef ?? planSelection?.productRef;
2339
+ const { plan: resolvedPlan } = usePlan({
2340
+ planRef: effectivePlanRef,
2341
+ productRef: effectiveProductRef
2342
+ });
2343
+ const isFreePlan = resolvedPlan?.requiresPayment === false;
2344
+ const {
2345
+ loading: checkoutLoading,
2346
+ error: checkoutError,
2347
+ clientSecret,
2348
+ startCheckout,
2349
+ stripePromise,
2350
+ resolvedPlanRef
2351
+ } = useCheckout({
2352
+ planRef: effectivePlanRef,
2353
+ productRef: effectiveProductRef,
2354
+ customer: prefillCustomer
2355
+ });
2356
+ const hasInitializedRef = useRef4(false);
2357
+ const hasPlanOrProduct = !!(effectivePlanRef || effectiveProductRef);
2358
+ useEffect6(() => {
2359
+ if (isFreePlan) return;
2360
+ if (!hasInitializedRef.current && hasPlanOrProduct && !checkoutLoading && !checkoutError && !clientSecret) {
2361
+ hasInitializedRef.current = true;
2362
+ startCheckout().catch(() => {
2363
+ hasInitializedRef.current = false;
2364
+ });
2365
+ }
2366
+ if (hasPlanOrProduct && clientSecret) {
2367
+ hasInitializedRef.current = true;
2368
+ }
2369
+ }, [
2370
+ hasPlanOrProduct,
2371
+ checkoutLoading,
2372
+ checkoutError,
2373
+ clientSecret,
2374
+ startCheckout,
2375
+ isFreePlan
2376
+ ]);
2377
+ const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
2378
+ const elementsOptions = useMemo6(() => {
2379
+ if (!clientSecret) return void 0;
2380
+ return { clientSecret, locale };
2381
+ }, [clientSecret, locale]);
2382
+ const shouldRenderElements = !!(stripePromise && clientSecret);
2383
+ if (!hasPlanOrProduct) {
2384
+ return /* @__PURE__ */ jsx10(
2385
+ "div",
2386
+ {
2387
+ ref: forwardedRef,
2388
+ className,
2389
+ "data-solvapay-payment-form": "",
2390
+ "data-state": "error",
2391
+ children: /* @__PURE__ */ jsx10("div", { "data-solvapay-payment-form-error": "", children: copy.errors.configMissingPlanOrProduct })
2392
+ }
2393
+ );
2394
+ }
2395
+ if (checkoutError) {
2396
+ return /* @__PURE__ */ jsx10(
2397
+ "div",
2398
+ {
2399
+ ref: forwardedRef,
2400
+ className,
2401
+ "data-solvapay-payment-form": "",
2402
+ "data-state": "error",
2403
+ children: /* @__PURE__ */ jsxs3("div", { "data-solvapay-payment-form-error": "", children: [
2404
+ copy.errors.paymentInitFailed,
2405
+ " ",
2406
+ checkoutError.message || copy.errors.unknownError
2407
+ ] })
2408
+ }
2409
+ );
2410
+ }
2411
+ if (isFreePlan && resolvedPlan) {
2412
+ return /* @__PURE__ */ jsx10(
2413
+ "div",
2414
+ {
2415
+ ref: forwardedRef,
2416
+ className,
2417
+ "data-solvapay-payment-form": "",
2418
+ "data-state": "ready",
2419
+ "data-variant": "free",
2420
+ children: /* @__PURE__ */ jsx10(
2421
+ FreeInner,
2422
+ {
2423
+ planRef: effectivePlanRef,
2424
+ productRef: effectiveProductRef,
2425
+ plan: resolvedPlan,
2426
+ resolvedPlanRef,
2427
+ requireTermsAcceptance,
2428
+ submitButtonText,
2429
+ buttonClassName,
2430
+ onFreePlan,
2431
+ onResult,
2432
+ onError,
2433
+ children
2434
+ }
2435
+ )
2436
+ }
2437
+ );
2438
+ }
2439
+ if (shouldRenderElements && elementsOptions) {
2440
+ return /* @__PURE__ */ jsx10(
2441
+ "div",
2442
+ {
2443
+ ref: forwardedRef,
2444
+ className,
2445
+ "data-solvapay-payment-form": "",
2446
+ "data-state": "ready",
2447
+ "data-variant": "paid",
2448
+ children: /* @__PURE__ */ jsx10(Elements, { stripe: stripePromise, options: elementsOptions, children: /* @__PURE__ */ jsx10(
2449
+ PaidInner,
2450
+ {
2451
+ planRef: effectivePlanRef,
2452
+ productRef: effectiveProductRef,
2453
+ prefillCustomer,
2454
+ resolvedPlanRef,
2455
+ plan: resolvedPlan ?? null,
2456
+ clientSecret,
2457
+ returnUrl: finalReturnUrl,
2458
+ submitButtonText,
2459
+ buttonClassName,
2460
+ requireTermsAcceptance,
2461
+ onSuccess,
2462
+ onResult,
2463
+ onError,
2464
+ children
2465
+ }
2466
+ ) }, clientSecret)
2467
+ }
2468
+ );
2469
+ }
2470
+ return /* @__PURE__ */ jsx10(
2471
+ "div",
2472
+ {
2473
+ ref: forwardedRef,
2474
+ className,
2475
+ "data-solvapay-payment-form": "",
2476
+ "data-state": "loading",
2477
+ children: /* @__PURE__ */ jsx10("div", { "data-solvapay-payment-form-loading": "", children: /* @__PURE__ */ jsx10(Spinner, { size: "md" }) })
2478
+ }
2479
+ );
2480
+ });
2481
+ var PaidInner = ({
2482
+ planRef,
2483
+ productRef,
2484
+ prefillCustomer,
2485
+ resolvedPlanRef,
2486
+ plan,
2487
+ clientSecret,
2488
+ returnUrl,
2489
+ submitButtonText,
2490
+ buttonClassName,
2491
+ requireTermsAcceptance,
2492
+ onSuccess,
2493
+ onResult,
2494
+ onError,
2495
+ children
2496
+ }) => {
2497
+ const stripe = useStripe();
2498
+ const elements = useElements();
2499
+ const copy = useCopy();
2500
+ const customer = useCustomer();
2501
+ const { processPayment } = useSolvaPay();
2502
+ const { refetch, hasPaidPurchase } = usePurchase();
2503
+ const hasPaidPurchaseRef = useRef4(hasPaidPurchase);
2504
+ useEffect6(() => {
2505
+ hasPaidPurchaseRef.current = hasPaidPurchase;
2506
+ }, [hasPaidPurchase]);
2507
+ const [elementKind, setElementKind] = useState8(
2508
+ children ? null : "payment-element"
2509
+ );
2510
+ const [paymentInputComplete, setPaymentInputComplete] = useState8(false);
2511
+ const [termsAccepted, setTermsAccepted] = useState8(false);
2512
+ const [isProcessing, setIsProcessing] = useState8(false);
2513
+ const [error, setError] = useState8(null);
2514
+ const isReady = !!(stripe && elements);
2515
+ const canSubmit = isReady && !!clientSecret && !!elementKind && paymentInputComplete && (!requireTermsAcceptance || termsAccepted) && !isProcessing;
2516
+ const submit = useCallback8(async () => {
2517
+ if (!stripe || !elements || !clientSecret || !elementKind) {
2518
+ const msg2 = !stripe || !elements ? copy.errors.stripeUnavailable : copy.errors.paymentIntentUnavailable;
2519
+ setError(msg2);
2520
+ onError?.(new Error(msg2));
2521
+ return;
2522
+ }
2523
+ setError(null);
2524
+ setIsProcessing(true);
2525
+ const result = await confirmPayment({
2526
+ stripe,
2527
+ elements,
2528
+ clientSecret,
2529
+ mode: elementKind,
2530
+ returnUrl,
2531
+ billingDetails: {
2532
+ name: customer.name ?? prefillCustomer?.name,
2533
+ email: customer.email ?? prefillCustomer?.email
2534
+ },
2535
+ copy
2536
+ });
2537
+ if (result.status === "error") {
2538
+ setError(result.message);
2539
+ setIsProcessing(false);
2540
+ onError?.(new Error(result.message));
2541
+ return;
2542
+ }
2543
+ if (result.status === "requires_action" || result.status === "other") {
2544
+ setError(result.message);
2545
+ setIsProcessing(false);
2546
+ return;
2547
+ }
2548
+ const paymentIntent = result.paymentIntent;
2549
+ const reconcileResult = await reconcilePayment({
2550
+ paymentIntentId: paymentIntent.id,
2551
+ productRef,
2552
+ planRef: planRef || resolvedPlanRef || void 0,
2553
+ processPayment,
2554
+ refetchPurchase: refetch,
2555
+ copy
2556
+ });
2557
+ if (reconcileResult.status === "success") {
2558
+ const pi = paymentIntent;
2559
+ onSuccess?.(pi);
2560
+ const paid = { kind: "paid", paymentIntent: pi };
2561
+ onResult?.(paid);
2562
+ const CONFIRMATION_TIMEOUT_MS = 1e4;
2563
+ const startedAt = Date.now();
2564
+ let attempt = 0;
2565
+ while (!hasPaidPurchaseRef.current && Date.now() - startedAt < CONFIRMATION_TIMEOUT_MS) {
2566
+ attempt += 1;
2567
+ await new Promise((r) => setTimeout(r, Math.min(500 * attempt, 1500)));
2568
+ if (hasPaidPurchaseRef.current) break;
2569
+ try {
2570
+ await refetch();
2571
+ } catch {
2572
+ }
2573
+ }
2574
+ if (!hasPaidPurchaseRef.current) {
2575
+ const confirmationMsg = copy.errors.paymentConfirmationDelayed;
2576
+ setError(confirmationMsg);
2577
+ setIsProcessing(false);
2578
+ onError?.(new Error(confirmationMsg));
2579
+ return;
2580
+ }
2581
+ setIsProcessing(false);
2582
+ return;
2583
+ }
2584
+ setIsProcessing(false);
2585
+ const msg = reconcileResult.status === "timeout" ? reconcileResult.error.message : copy.errors.paymentProcessingFailed;
2586
+ setError(msg);
2587
+ onError?.(reconcileResult.error);
2588
+ }, [
2589
+ stripe,
2590
+ elements,
2591
+ clientSecret,
2592
+ elementKind,
2593
+ returnUrl,
2594
+ customer,
2595
+ prefillCustomer,
2596
+ copy,
2597
+ processPayment,
2598
+ productRef,
2599
+ planRef,
2600
+ resolvedPlanRef,
2601
+ refetch,
2602
+ onSuccess,
2603
+ onResult,
2604
+ onError
2605
+ ]);
2606
+ const contextValue = useMemo6(
2607
+ () => ({
2608
+ planRef,
2609
+ productRef,
2610
+ prefillCustomer,
2611
+ resolvedPlanRef,
2612
+ plan,
2613
+ clientSecret,
2614
+ stripe: stripe ?? null,
2615
+ elements: elements ?? null,
2616
+ isProcessing,
2617
+ isReady,
2618
+ paymentInputComplete,
2619
+ termsAccepted,
2620
+ requireTermsAcceptance,
2621
+ canSubmit,
2622
+ error,
2623
+ elementKind,
2624
+ returnUrl,
2625
+ submitButtonText,
2626
+ buttonClassName,
2627
+ setElementKind,
2628
+ setPaymentInputComplete,
2629
+ setTermsAccepted,
2630
+ submit
2631
+ }),
2632
+ [
2633
+ planRef,
2634
+ productRef,
2635
+ prefillCustomer,
2636
+ resolvedPlanRef,
2637
+ plan,
2638
+ clientSecret,
2639
+ stripe,
2640
+ elements,
2641
+ isProcessing,
2642
+ isReady,
2643
+ paymentInputComplete,
2644
+ termsAccepted,
2645
+ requireTermsAcceptance,
2646
+ canSubmit,
2647
+ error,
2648
+ elementKind,
2649
+ returnUrl,
2650
+ submitButtonText,
2651
+ buttonClassName,
2652
+ submit
2653
+ ]
2654
+ );
2655
+ return /* @__PURE__ */ jsx10(PaymentFormProvider, { value: contextValue, children });
2656
+ };
2657
+ var FreeInner = ({
2658
+ planRef,
2659
+ productRef,
2660
+ plan,
2661
+ resolvedPlanRef,
2662
+ requireTermsAcceptance,
2663
+ submitButtonText,
2664
+ buttonClassName,
2665
+ onFreePlan,
2666
+ onResult,
2667
+ onError,
2668
+ children
2669
+ }) => {
2670
+ const copy = useCopy();
2671
+ const { refetch } = usePurchase();
2672
+ const { activate, state, error: activationError, result: activationResult } = useActivation();
2673
+ const [termsAccepted, setTermsAccepted] = useState8(false);
2674
+ const [localError, setLocalError] = useState8(null);
2675
+ const resultFiredRef = useRef4(false);
2676
+ useEffect6(() => {
2677
+ if (state === "activated" && activationResult && !resultFiredRef.current) {
2678
+ resultFiredRef.current = true;
2679
+ const res = { kind: "activated", result: activationResult };
2680
+ onResult?.(res);
2681
+ refetch().catch(() => {
2682
+ });
2683
+ }
2684
+ }, [state, activationResult, onResult, refetch]);
2685
+ const isProcessing = state === "activating";
2686
+ const canSubmit = !isProcessing && (!requireTermsAcceptance || termsAccepted) && !!productRef;
2687
+ const submit = useCallback8(async () => {
2688
+ if (!productRef) {
2689
+ const msg = copy.errors.configMissingPlanOrProduct;
2690
+ setLocalError(msg);
2691
+ onError?.(new Error(msg));
2692
+ return;
2693
+ }
2694
+ setLocalError(null);
2695
+ try {
2696
+ if (onFreePlan) {
2697
+ await onFreePlan(plan);
2698
+ onResult?.({
2699
+ kind: "activated",
2700
+ result: {
2701
+ status: "activated",
2702
+ productRef,
2703
+ planRef: plan.reference
2704
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2705
+ }
2706
+ });
2707
+ return;
2708
+ }
2709
+ await activate({ productRef, planRef: plan.reference });
2710
+ } catch (err) {
2711
+ const msg = err instanceof Error ? err.message : copy.activation.failed;
2712
+ setLocalError(msg);
2713
+ onError?.(err instanceof Error ? err : new Error(msg));
2714
+ }
2715
+ }, [productRef, plan, onFreePlan, activate, onResult, onError, copy]);
2716
+ const error = localError ?? activationError;
2717
+ const contextValue = useMemo6(
2718
+ () => ({
2719
+ planRef,
2720
+ productRef,
2721
+ prefillCustomer: void 0,
2722
+ resolvedPlanRef,
2723
+ plan,
2724
+ clientSecret: null,
2725
+ stripe: null,
2726
+ elements: null,
2727
+ isProcessing,
2728
+ isReady: true,
2729
+ paymentInputComplete: true,
2730
+ termsAccepted,
2731
+ requireTermsAcceptance,
2732
+ canSubmit,
2733
+ error,
2734
+ elementKind: null,
2735
+ returnUrl: "",
2736
+ submitButtonText,
2737
+ buttonClassName,
2738
+ setElementKind: () => {
2739
+ },
2740
+ setPaymentInputComplete: () => {
2741
+ },
2742
+ setTermsAccepted,
2743
+ submit
2744
+ }),
2745
+ [
2746
+ planRef,
2747
+ productRef,
2748
+ resolvedPlanRef,
2749
+ plan,
2750
+ isProcessing,
2751
+ termsAccepted,
2752
+ requireTermsAcceptance,
2753
+ canSubmit,
2754
+ error,
2755
+ submitButtonText,
2756
+ buttonClassName,
2757
+ submit
2758
+ ]
2759
+ );
2760
+ return /* @__PURE__ */ jsx10(PaymentFormProvider, { value: contextValue, children });
2761
+ };
2762
+ var Summary = (props) => {
2763
+ const ctx = usePaymentForm();
2764
+ return /* @__PURE__ */ jsx10(
2765
+ CheckoutSummary2,
2766
+ {
2767
+ ...props,
2768
+ planRef: ctx.planRef || ctx.resolvedPlanRef || void 0,
2769
+ productRef: ctx.productRef
2770
+ }
2771
+ );
2772
+ };
2773
+ var MandateTextPrimitive = (props) => {
2774
+ const ctx = usePaymentForm();
2775
+ return /* @__PURE__ */ jsx10(
2776
+ MandateText,
2777
+ {
2778
+ ...props,
2779
+ planRef: ctx.planRef || ctx.resolvedPlanRef || void 0,
2780
+ productRef: ctx.productRef
2781
+ }
2782
+ );
2783
+ };
2784
+ var CustomerFields = forwardRef3(
2785
+ function PaymentFormCustomerFields({ asChild, readOnly: _readOnly = true, children, ...rest }, ref) {
2786
+ const copy = useCopy();
2787
+ const customer = useCustomer();
2788
+ const { prefillCustomer } = usePaymentForm();
2789
+ const email = customer.email ?? prefillCustomer?.email;
2790
+ const name = customer.name ?? prefillCustomer?.name;
2791
+ if (!email && !name) return null;
2792
+ const Comp = asChild ? Slot : "div";
2793
+ return /* @__PURE__ */ jsx10(Comp, { ref, "data-solvapay-payment-form-customer-fields": "", ...rest, children: children ?? /* @__PURE__ */ jsxs3(Fragment2, { children: [
2794
+ email && /* @__PURE__ */ jsxs3("div", { "data-solvapay-payment-form-customer-email": "", children: [
2795
+ /* @__PURE__ */ jsxs3("span", { children: [
2796
+ copy.customer.emailLabel,
2797
+ ": "
2798
+ ] }),
2799
+ /* @__PURE__ */ jsx10("span", { children: email })
2800
+ ] }),
2801
+ name && /* @__PURE__ */ jsxs3("div", { "data-solvapay-payment-form-customer-name": "", children: [
2802
+ /* @__PURE__ */ jsxs3("span", { children: [
2803
+ copy.customer.nameLabel,
2804
+ ": "
2805
+ ] }),
2806
+ /* @__PURE__ */ jsx10("span", { children: name })
2807
+ ] })
2808
+ ] }) });
2809
+ }
2810
+ );
2811
+ var PaymentElementSlot = ({ options }) => {
2812
+ const { setElementKind, setPaymentInputComplete, isReady, stripe, elements } = usePaymentForm();
2813
+ const locale = useLocale();
2814
+ useEffect6(() => {
2815
+ if (stripe && elements) setElementKind("payment-element");
2816
+ }, [setElementKind, stripe, elements]);
2817
+ if (!stripe || !elements) return null;
2818
+ if (!isReady) {
2819
+ return /* @__PURE__ */ jsx10("div", { "data-solvapay-payment-form-loading": "", children: /* @__PURE__ */ jsx10(Spinner, { size: "sm" }) });
2820
+ }
2821
+ return /* @__PURE__ */ jsx10("div", { "data-solvapay-payment-form-payment-element": "", children: /* @__PURE__ */ jsx10(
2822
+ StripePaymentElement,
2823
+ {
2824
+ options,
2825
+ onChange: (e) => setPaymentInputComplete(e.complete)
2826
+ },
2827
+ locale || "default"
2828
+ ) });
2829
+ };
2830
+ var CardElementSlot = ({ options }) => {
2831
+ const { setElementKind, setPaymentInputComplete, isReady, stripe, elements } = usePaymentForm();
2832
+ useEffect6(() => {
2833
+ if (stripe && elements) setElementKind("card-element");
2834
+ }, [setElementKind, stripe, elements]);
2835
+ if (!stripe || !elements) return null;
2836
+ if (!isReady) {
2837
+ return /* @__PURE__ */ jsx10("div", { "data-solvapay-payment-form-loading": "", children: /* @__PURE__ */ jsx10(Spinner, { size: "sm" }) });
2838
+ }
2839
+ return /* @__PURE__ */ jsx10("div", { "data-solvapay-payment-form-card-element": "", children: /* @__PURE__ */ jsx10(
2840
+ StripeCardElement,
2841
+ {
2842
+ options,
2843
+ onChange: (e) => setPaymentInputComplete(e.complete)
2844
+ }
2845
+ ) });
2846
+ };
2847
+ var TermsCheckbox = forwardRef3(
2848
+ function PaymentFormTermsCheckbox({ asChild, label, children, ...rest }, ref) {
2849
+ const { termsAccepted, setTermsAccepted } = usePaymentForm();
2850
+ const copy = useCopy();
2851
+ const id = "solvapay-terms-checkbox";
2852
+ if (asChild) {
2853
+ return /* @__PURE__ */ jsx10(Slot, { ref, "data-solvapay-payment-form-terms": "", ...rest, children });
2854
+ }
2855
+ return /* @__PURE__ */ jsxs3(
2856
+ "label",
2857
+ {
2858
+ ref,
2859
+ htmlFor: id,
2860
+ "data-solvapay-payment-form-terms": "",
2861
+ ...rest,
2862
+ children: [
2863
+ /* @__PURE__ */ jsx10(
2864
+ "input",
2865
+ {
2866
+ id,
2867
+ type: "checkbox",
2868
+ checked: termsAccepted,
2869
+ onChange: (e) => setTermsAccepted(e.target.checked)
2870
+ }
2871
+ ),
2872
+ /* @__PURE__ */ jsx10("span", { children: label ?? copy.terms.checkboxLabel })
2873
+ ]
2874
+ }
2875
+ );
2876
+ }
2877
+ );
2878
+ var SubmitButton = forwardRef3(
2879
+ function PaymentFormSubmitButton({ asChild, onClick, children, ...rest }, ref) {
2880
+ const ctx = usePaymentForm();
2881
+ const copy = useCopy();
2882
+ const locale = useLocale();
2883
+ const { plan } = usePlan({
2884
+ planRef: ctx.planRef || ctx.resolvedPlanRef || void 0,
2885
+ productRef: ctx.productRef
2886
+ });
2887
+ const { product } = useProduct(ctx.productRef);
2888
+ const variant = deriveVariant(plan ?? ctx.plan ?? void 0);
2889
+ const dataVariant = toSubmitVariant(variant);
2890
+ const dataState = ctx.isProcessing ? "processing" : !ctx.canSubmit ? "disabled" : "idle";
2891
+ const amountFormatted = formatPrice(
2892
+ plan?.price ?? ctx.plan?.price ?? 0,
2893
+ plan?.currency ?? ctx.plan?.currency ?? "usd",
2894
+ { locale, free: copy.interval.free }
2895
+ );
2896
+ const label = resolveCta({
2897
+ variant,
2898
+ plan: plan ?? ctx.plan,
2899
+ product,
2900
+ amountFormatted,
2901
+ copy,
2902
+ override: typeof children === "string" ? children : ctx.submitButtonText
2903
+ });
2904
+ const content = ctx.isProcessing ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
2905
+ /* @__PURE__ */ jsx10(Spinner, { size: "sm" }),
2906
+ /* @__PURE__ */ jsx10("span", { children: copy.cta.processing })
2907
+ ] }) : children && typeof children !== "string" ? children : label;
2908
+ const buttonProps = {
2909
+ "data-solvapay-payment-form-submit": "",
2910
+ "data-state": dataState,
2911
+ "data-variant": dataVariant,
2912
+ "aria-busy": ctx.isProcessing,
2913
+ "aria-disabled": !ctx.canSubmit,
2914
+ "aria-label": label,
2915
+ disabled: !ctx.canSubmit,
2916
+ className: rest.className ?? ctx.buttonClassName,
2917
+ onClick: composeEventHandlers(onClick, (e) => {
2918
+ e.preventDefault();
2919
+ ctx.submit();
2920
+ }),
2921
+ ...rest
2922
+ };
2923
+ if (asChild) {
2924
+ return (
2925
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2926
+ /* @__PURE__ */ jsx10(Slot, { ref, ...buttonProps, children: content })
2927
+ );
2928
+ }
2929
+ return /* @__PURE__ */ jsx10("button", { ref, type: "submit", ...buttonProps, children: content });
2930
+ }
2931
+ );
2932
+ var Loading = forwardRef3(function PaymentFormLoading({ asChild, children, ...rest }, ref) {
2933
+ const ctx = usePaymentForm();
2934
+ if (ctx.isReady && ctx.clientSecret) return null;
2935
+ const Comp = asChild ? Slot : "div";
2936
+ return /* @__PURE__ */ jsx10(Comp, { ref, "data-solvapay-payment-form-loading": "", ...rest, children: children ?? /* @__PURE__ */ jsx10(Spinner, { size: "sm" }) });
2937
+ });
2938
+ var ErrorSlot = forwardRef3(function PaymentFormError({ asChild, children, ...rest }, ref) {
2939
+ const ctx = usePaymentForm();
2940
+ if (!ctx.error) return null;
2941
+ const Comp = asChild ? Slot : "div";
2942
+ return /* @__PURE__ */ jsx10(
2943
+ Comp,
2944
+ {
2945
+ ref,
2946
+ role: "alert",
2947
+ "aria-live": "assertive",
2948
+ "aria-atomic": "true",
2949
+ "data-solvapay-payment-form-error": "",
2950
+ ...rest,
2951
+ children: children ?? ctx.error
2952
+ }
2953
+ );
2954
+ });
2955
+ var PaymentFormRoot2 = Root2;
2956
+ var PaymentFormSummary = Summary;
2957
+ var PaymentFormCustomerFields2 = CustomerFields;
2958
+ var PaymentFormPaymentElement = PaymentElementSlot;
2959
+ var PaymentFormCardElement = CardElementSlot;
2960
+ var PaymentFormMandateText = MandateTextPrimitive;
2961
+ var PaymentFormTermsCheckbox2 = TermsCheckbox;
2962
+ var PaymentFormSubmitButton2 = SubmitButton;
2963
+ var PaymentFormLoading2 = Loading;
2964
+ var PaymentFormError2 = ErrorSlot;
2965
+ var PaymentForm = {
2966
+ Root: Root2,
2967
+ Summary,
2968
+ CustomerFields,
2969
+ PaymentElement: PaymentElementSlot,
2970
+ CardElement: CardElementSlot,
2971
+ MandateText: MandateTextPrimitive,
2972
+ TermsCheckbox,
2973
+ SubmitButton,
2974
+ Loading,
2975
+ Error: ErrorSlot
2976
+ };
2977
+
2978
+ // src/hooks/useTopupAmountSelector.ts
2979
+ import { useState as useState9, useCallback as useCallback9, useMemo as useMemo7 } from "react";
2980
+ function getQuickAmounts(currency) {
2981
+ switch (currency.toUpperCase()) {
2982
+ case "SEK":
2983
+ case "NOK":
2984
+ case "DKK":
2985
+ return [100, 500, 1e3, 5e3];
2986
+ case "JPY":
2987
+ return [1e3, 5e3, 1e4, 5e4];
2988
+ case "KRW":
2989
+ return [1e4, 5e4, 1e5, 5e5];
2990
+ case "ISK":
2991
+ case "HUF":
2992
+ return [1e3, 5e3, 1e4, 5e4];
2993
+ default:
2994
+ return [10, 50, 100, 500];
2995
+ }
2996
+ }
2997
+ function getCurrencySymbol(currency) {
2998
+ try {
2999
+ const parts = new Intl.NumberFormat("en", { style: "currency", currency }).formatToParts(0);
3000
+ const sym = parts.find((p) => p.type === "currency");
3001
+ return sym?.value || currency;
3002
+ } catch {
3003
+ return currency;
3004
+ }
3005
+ }
3006
+ var DEFAULT_MIN = 1;
3007
+ var DEFAULT_MAX = 1e5;
3008
+ function useTopupAmountSelector(options) {
3009
+ const { currency, minAmount = DEFAULT_MIN, maxAmount = DEFAULT_MAX } = options;
3010
+ const copy = useCopy();
3011
+ const [selectedAmount, setSelectedAmount] = useState9(null);
3012
+ const [customAmount, setCustomAmountRaw] = useState9("");
3013
+ const [error, setError] = useState9(null);
3014
+ const quickAmounts = useMemo7(() => getQuickAmounts(currency), [currency]);
3015
+ const currencySymbol = useMemo7(() => getCurrencySymbol(currency), [currency]);
3016
+ const resolvedAmount = useMemo7(() => {
3017
+ if (customAmount) {
3018
+ const parsed = parseFloat(customAmount);
3019
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
3020
+ }
3021
+ return selectedAmount;
3022
+ }, [selectedAmount, customAmount]);
3023
+ const selectQuickAmount = useCallback9(
3024
+ (amount) => {
3025
+ setSelectedAmount(amount);
3026
+ setCustomAmountRaw("");
3027
+ setError(null);
3028
+ },
3029
+ []
3030
+ );
3031
+ const setCustomAmount = useCallback9(
3032
+ (value) => {
3033
+ const sanitized = value.replace(/[^0-9.]/g, "");
3034
+ const dotIndex = sanitized.indexOf(".");
3035
+ const cleaned = dotIndex === -1 ? sanitized : sanitized.slice(0, dotIndex + 1) + sanitized.slice(dotIndex + 1).replace(/\./g, "");
3036
+ setCustomAmountRaw(cleaned);
3037
+ setSelectedAmount(null);
3038
+ setError(null);
3039
+ },
3040
+ []
3041
+ );
3042
+ const validate = useCallback9(() => {
3043
+ if (resolvedAmount == null) {
3044
+ setError(copy.topup.selectOrEnterAmount);
3045
+ return false;
3046
+ }
3047
+ if (resolvedAmount < minAmount) {
3048
+ setError(
3049
+ interpolate(copy.topup.minAmount, {
3050
+ amount: formatPrice(minAmount * getMinorUnitsPerMajor(currency), currency, {
3051
+ free: ""
3052
+ })
3053
+ })
3054
+ );
3055
+ return false;
3056
+ }
3057
+ if (resolvedAmount > maxAmount) {
3058
+ setError(
3059
+ interpolate(copy.topup.maxAmount, {
3060
+ amount: formatPrice(maxAmount * getMinorUnitsPerMajor(currency), currency, {
3061
+ free: ""
3062
+ })
3063
+ })
3064
+ );
3065
+ return false;
3066
+ }
3067
+ setError(null);
3068
+ return true;
3069
+ }, [resolvedAmount, minAmount, maxAmount, currency, copy]);
3070
+ const reset = useCallback9(() => {
3071
+ setSelectedAmount(null);
3072
+ setCustomAmountRaw("");
3073
+ setError(null);
3074
+ }, []);
3075
+ return {
3076
+ quickAmounts,
3077
+ selectedAmount,
3078
+ customAmount,
3079
+ resolvedAmount,
3080
+ selectQuickAmount,
3081
+ setCustomAmount,
3082
+ error,
3083
+ validate,
3084
+ reset,
3085
+ currencySymbol
3086
+ };
3087
+ }
3088
+
3089
+ // src/hooks/useBalance.ts
3090
+ import { useEffect as useEffect7 } from "react";
3091
+ function useBalance() {
3092
+ const { balance } = useSolvaPay();
3093
+ useEffect7(() => {
3094
+ balance.refetch();
3095
+ }, [balance.refetch]);
3096
+ return balance;
3097
+ }
3098
+
3099
+ // src/primitives/AmountPicker.tsx
3100
+ import {
3101
+ createContext as createContext6,
3102
+ forwardRef as forwardRef4,
3103
+ useCallback as useCallback10,
3104
+ useContext as useContext8,
3105
+ useEffect as useEffect8,
3106
+ useMemo as useMemo8
3107
+ } from "react";
3108
+ import { jsx as jsx11 } from "react/jsx-runtime";
3109
+ var AmountPickerContext = createContext6(null);
3110
+ function usePickerCtx(part) {
3111
+ const ctx = useContext8(AmountPickerContext);
3112
+ if (!ctx) {
3113
+ throw new Error(`AmountPicker.${part} must be rendered inside <AmountPicker.Root>.`);
3114
+ }
3115
+ return ctx;
3116
+ }
3117
+ var Root3 = forwardRef4(function AmountPickerRoot({
3118
+ currency,
3119
+ minAmount,
3120
+ maxAmount,
3121
+ emit = "major",
3122
+ selector: externalSelector,
3123
+ onChange,
3124
+ asChild,
3125
+ children,
3126
+ ...rest
3127
+ }, forwardedRef) {
3128
+ const solva = useContext8(SolvaPayContext);
3129
+ if (!solva) throw new MissingProviderError("AmountPicker");
3130
+ const internalSelector = useTopupAmountSelector({ currency, minAmount, maxAmount });
3131
+ const selector = externalSelector ?? internalSelector;
3132
+ const { creditsPerMinorUnit, displayExchangeRate } = useBalance();
3133
+ const minorPerMajor = getMinorUnitsPerMajor(currency);
3134
+ const resolvedAmountMinor = selector.resolvedAmount != null && selector.resolvedAmount > 0 ? Math.round(selector.resolvedAmount * minorPerMajor) : null;
3135
+ useEffect8(() => {
3136
+ if (emit === "minor") {
3137
+ onChange?.(resolvedAmountMinor);
3138
+ } else {
3139
+ onChange?.(selector.resolvedAmount);
3140
+ }
3141
+ }, [emit, selector.resolvedAmount, resolvedAmountMinor, onChange]);
3142
+ const rate = displayExchangeRate ?? 1;
3143
+ const estimatedCredits = creditsPerMinorUnit != null && creditsPerMinorUnit > 0 && resolvedAmountMinor != null ? Math.floor(resolvedAmountMinor / rate * creditsPerMinorUnit) : null;
3144
+ const isApproximate = rate !== 1;
3145
+ const ctx = useMemo8(
3146
+ () => ({
3147
+ ...selector,
3148
+ currency,
3149
+ emit,
3150
+ creditsPerMinorUnit,
3151
+ displayExchangeRate,
3152
+ estimatedCredits,
3153
+ isApproximate,
3154
+ resolvedAmountMinor
3155
+ }),
3156
+ [
3157
+ selector,
3158
+ currency,
3159
+ emit,
3160
+ creditsPerMinorUnit,
3161
+ displayExchangeRate,
3162
+ estimatedCredits,
3163
+ isApproximate,
3164
+ resolvedAmountMinor
3165
+ ]
3166
+ );
3167
+ const Comp = asChild ? Slot : "div";
3168
+ return /* @__PURE__ */ jsx11(AmountPickerContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx11(Comp, { ref: forwardedRef, "data-solvapay-amount-picker": "", ...rest, children }) });
3169
+ });
3170
+ var Option = forwardRef4(function AmountPickerOption({ asChild, amount, onClick, children, ...rest }, forwardedRef) {
3171
+ const ctx = usePickerCtx("Option");
3172
+ const isActive = ctx.selectedAmount === amount && !ctx.customAmount;
3173
+ const state = rest.disabled ? "disabled" : isActive ? "selected" : "idle";
3174
+ const commonProps = {
3175
+ "data-solvapay-amount-picker-option": "",
3176
+ "data-state": state,
3177
+ "data-amount": String(amount),
3178
+ "aria-pressed": isActive,
3179
+ onClick: composeEventHandlers(onClick, () => {
3180
+ ctx.selectQuickAmount(amount);
3181
+ }),
3182
+ ...rest
3183
+ };
3184
+ const defaultLabel = formatPrice(
3185
+ amount * getMinorUnitsPerMajor(ctx.currency),
3186
+ ctx.currency,
3187
+ { free: "" }
3188
+ );
3189
+ if (asChild) {
3190
+ return (
3191
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3192
+ /* @__PURE__ */ jsx11(Slot, { ref: forwardedRef, ...commonProps, children: children ?? defaultLabel })
3193
+ );
3194
+ }
3195
+ return /* @__PURE__ */ jsx11("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? defaultLabel });
3196
+ });
3197
+ var Custom = forwardRef4(function AmountPickerCustom({ asChild, onChange, onFocus, ...rest }, forwardedRef) {
3198
+ const ctx = usePickerCtx("Custom");
3199
+ const state = ctx.customAmount ? "active" : "dormant";
3200
+ const commonProps = {
3201
+ "data-solvapay-amount-picker-custom": "",
3202
+ "data-state": state,
3203
+ type: "text",
3204
+ inputMode: "decimal",
3205
+ placeholder: "0.00",
3206
+ value: ctx.customAmount,
3207
+ onChange: composeEventHandlers(onChange, (e) => {
3208
+ ctx.setCustomAmount(e.target.value);
3209
+ }),
3210
+ onFocus: composeEventHandlers(onFocus, () => {
3211
+ if (ctx.selectedAmount != null) ctx.selectQuickAmount(0);
3212
+ }),
3213
+ ...rest
3214
+ };
3215
+ if (asChild) {
3216
+ return (
3217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3218
+ /* @__PURE__ */ jsx11(Slot, { ref: forwardedRef, ...commonProps })
3219
+ );
3220
+ }
3221
+ return /* @__PURE__ */ jsx11("input", { ref: forwardedRef, ...commonProps });
3222
+ });
3223
+ var Confirm = forwardRef4(function AmountPickerConfirm({ asChild, onClick, onConfirm, disabled, children, ...rest }, forwardedRef) {
3224
+ const ctx = usePickerCtx("Confirm");
3225
+ const isDisabled = disabled || !ctx.resolvedAmount;
3226
+ const handleConfirm = useCallback10(() => {
3227
+ if (!ctx.validate()) return;
3228
+ if (ctx.emit === "minor") {
3229
+ if (ctx.resolvedAmountMinor != null) onConfirm?.(ctx.resolvedAmountMinor);
3230
+ return;
3231
+ }
3232
+ if (ctx.resolvedAmount != null) onConfirm?.(ctx.resolvedAmount);
3233
+ }, [ctx, onConfirm]);
3234
+ const commonProps = {
3235
+ "data-solvapay-amount-picker-confirm": "",
3236
+ "data-state": isDisabled ? "disabled" : "idle",
3237
+ disabled: isDisabled,
3238
+ "aria-disabled": isDisabled || void 0,
3239
+ onClick: composeEventHandlers(onClick, handleConfirm),
3240
+ ...rest
3241
+ };
3242
+ if (asChild) {
3243
+ return (
3244
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3245
+ /* @__PURE__ */ jsx11(Slot, { ref: forwardedRef, ...commonProps, children })
3246
+ );
3247
+ }
3248
+ return /* @__PURE__ */ jsx11("button", { ref: forwardedRef, type: "button", ...commonProps, children });
3249
+ });
3250
+ var AmountPickerRoot2 = Root3;
3251
+ var AmountPickerOption2 = Option;
3252
+ var AmountPickerCustom2 = Custom;
3253
+ var AmountPickerConfirm2 = Confirm;
3254
+ var AmountPicker = {
3255
+ Root: Root3,
3256
+ Option,
3257
+ Custom,
3258
+ Confirm
3259
+ };
3260
+ function useAmountPicker() {
3261
+ return usePickerCtx("useAmountPicker");
3262
+ }
3263
+ function useAmountPickerCopy() {
3264
+ const copy = useCopy();
3265
+ const ctx = usePickerCtx("useAmountPickerCopy");
3266
+ return {
3267
+ selectAmountLabel: copy.amountPicker.selectAmountLabel,
3268
+ customAmountLabel: copy.amountPicker.customAmountLabel,
3269
+ creditEstimate: (credits) => interpolate(
3270
+ ctx.isApproximate ? copy.amountPicker.creditEstimateApprox : copy.amountPicker.creditEstimateExact,
3271
+ { credits: new Intl.NumberFormat().format(credits) }
3272
+ )
3273
+ };
3274
+ }
3275
+
3276
+ // src/hooks/useTopup.ts
3277
+ import { useState as useState10, useCallback as useCallback11, useRef as useRef5 } from "react";
3278
+ import { loadStripe as loadStripe2 } from "@stripe/stripe-js";
3279
+ var stripePromiseCache2 = /* @__PURE__ */ new Map();
3280
+ function getStripeCacheKey2(publishableKey, accountId) {
3281
+ return accountId ? `${publishableKey}:${accountId}` : publishableKey;
3282
+ }
3283
+ function useTopup(options) {
3284
+ const { amount, currency } = options;
3285
+ const { createTopupPayment, customerRef, updateCustomerRef } = useSolvaPay();
3286
+ const [loading, setLoading] = useState10(false);
3287
+ const [error, setError] = useState10(null);
3288
+ const [stripePromise, setStripePromise] = useState10(null);
3289
+ const [clientSecret, setClientSecret] = useState10(null);
3290
+ const isStartingRef = useRef5(false);
3291
+ const startTopup = useCallback11(async () => {
3292
+ if (isStartingRef.current || loading) {
3293
+ return;
3294
+ }
3295
+ if (!amount || amount <= 0) {
3296
+ setError(new Error("useTopup: amount must be a positive number"));
3297
+ return;
3298
+ }
3299
+ isStartingRef.current = true;
3300
+ setLoading(true);
3301
+ setError(null);
3302
+ try {
3303
+ const result = await createTopupPayment({ amount, currency });
3304
+ if (!result || typeof result !== "object") {
3305
+ throw new Error("Invalid topup payment intent response from server");
3306
+ }
3307
+ if (!result.clientSecret || typeof result.clientSecret !== "string") {
3308
+ throw new Error("Invalid client secret in topup payment intent response");
3309
+ }
3310
+ if (!result.publishableKey || typeof result.publishableKey !== "string") {
3311
+ throw new Error("Invalid publishable key in topup payment intent response");
3312
+ }
3313
+ if (result.customerRef && result.customerRef !== customerRef && updateCustomerRef) {
3314
+ updateCustomerRef(result.customerRef);
3315
+ }
3316
+ const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
3317
+ const cacheKey = getStripeCacheKey2(result.publishableKey, result.accountId);
3318
+ let stripe = stripePromiseCache2.get(cacheKey);
3319
+ if (!stripe) {
3320
+ stripe = loadStripe2(result.publishableKey, stripeOptions);
3321
+ stripePromiseCache2.set(cacheKey, stripe);
3322
+ }
3323
+ setStripePromise(stripe);
3324
+ setClientSecret(result.clientSecret);
3325
+ } catch (err) {
3326
+ const error2 = err instanceof Error ? err : new Error("Failed to start topup");
3327
+ setError(error2);
3328
+ } finally {
3329
+ setLoading(false);
3330
+ isStartingRef.current = false;
3331
+ }
3332
+ }, [amount, currency, createTopupPayment, customerRef, updateCustomerRef, loading]);
3333
+ const reset = useCallback11(() => {
3334
+ isStartingRef.current = false;
3335
+ setLoading(false);
3336
+ setError(null);
3337
+ setStripePromise(null);
3338
+ setClientSecret(null);
3339
+ }, []);
3340
+ return {
3341
+ loading,
3342
+ error,
3343
+ stripePromise,
3344
+ clientSecret,
3345
+ startTopup,
3346
+ reset
3347
+ };
3348
+ }
3349
+
3350
+ // src/primitives/TopupForm.tsx
3351
+ import {
3352
+ createContext as createContext7,
3353
+ forwardRef as forwardRef5,
3354
+ useCallback as useCallback12,
3355
+ useContext as useContext9,
3356
+ useEffect as useEffect9,
3357
+ useMemo as useMemo9,
3358
+ useRef as useRef6,
3359
+ useState as useState11
3360
+ } from "react";
3361
+ import {
3362
+ Elements as Elements2,
3363
+ useStripe as useStripe2,
3364
+ useElements as useElements2,
3365
+ PaymentElement as StripePaymentElement2
3366
+ } from "@stripe/react-stripe-js";
3367
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
3368
+ var TopupFormContext = createContext7(null);
3369
+ function useTopupCtx(part) {
3370
+ const ctx = useContext9(TopupFormContext);
3371
+ if (!ctx) {
3372
+ throw new Error(`TopupForm.${part} must be rendered inside <TopupForm.Root>.`);
3373
+ }
3374
+ return ctx;
3375
+ }
3376
+ var Root4 = forwardRef5(function TopupFormRoot(props, forwardedRef) {
3377
+ const {
3378
+ amount,
3379
+ currency,
3380
+ onSuccess,
3381
+ onError,
3382
+ returnUrl,
3383
+ submitButtonText: _submitButtonText,
3384
+ buttonClassName: _buttonClassName,
3385
+ className,
3386
+ asChild,
3387
+ children,
3388
+ ...rest
3389
+ } = props;
3390
+ const solva = useContext9(SolvaPayContext);
3391
+ if (!solva) throw new MissingProviderError("TopupForm");
3392
+ const copy = useCopy();
3393
+ const locale = useLocale();
3394
+ const { loading, error: topupError, clientSecret, startTopup, stripePromise } = useTopup({
3395
+ amount,
3396
+ currency
3397
+ });
3398
+ const hasInitializedRef = useRef6(false);
3399
+ const hasAmount = amount > 0;
3400
+ useEffect9(() => {
3401
+ if (!hasInitializedRef.current && hasAmount && !loading && !topupError && !clientSecret) {
3402
+ hasInitializedRef.current = true;
3403
+ startTopup().catch(() => {
3404
+ hasInitializedRef.current = false;
3405
+ });
3406
+ }
3407
+ if (hasAmount && clientSecret) hasInitializedRef.current = true;
3408
+ }, [hasAmount, loading, topupError, clientSecret, startTopup]);
3409
+ const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
3410
+ const elementsOptions = useMemo9(() => {
3411
+ if (!clientSecret) return void 0;
3412
+ return { clientSecret, locale };
3413
+ }, [clientSecret, locale]);
3414
+ const Comp = asChild ? Slot : "div";
3415
+ const outerError = !hasAmount ? copy.errors.configMissingAmount : topupError ? `${copy.errors.topupInitFailed} ${topupError.message || copy.errors.unknownError}` : null;
3416
+ const dataState = outerError ? "error" : clientSecret && stripePromise ? "ready" : "loading";
3417
+ const canMountElements = !!(stripePromise && clientSecret && elementsOptions);
3418
+ const innerCommon = {
3419
+ amount,
3420
+ currency,
3421
+ clientSecret,
3422
+ returnUrl: finalReturnUrl,
3423
+ outerError,
3424
+ state: dataState,
3425
+ onSuccess,
3426
+ onError
3427
+ };
3428
+ const shell = /* @__PURE__ */ jsx12(
3429
+ Comp,
3430
+ {
3431
+ ref: forwardedRef,
3432
+ className,
3433
+ "data-solvapay-topup-form": "",
3434
+ "data-state": dataState,
3435
+ ...rest,
3436
+ children
3437
+ }
3438
+ );
3439
+ if (canMountElements) {
3440
+ return /* @__PURE__ */ jsx12(Elements2, { stripe: stripePromise, options: elementsOptions, children: /* @__PURE__ */ jsx12(Inner, { ...innerCommon, children: shell }) }, clientSecret);
3441
+ }
3442
+ return /* @__PURE__ */ jsx12(OfflineInner, { ...innerCommon, children: shell });
3443
+ });
3444
+ var Inner = ({
3445
+ amount,
3446
+ currency,
3447
+ clientSecret,
3448
+ returnUrl,
3449
+ outerError,
3450
+ state,
3451
+ onSuccess,
3452
+ onError,
3453
+ children
3454
+ }) => {
3455
+ const stripe = useStripe2();
3456
+ const elements = useElements2();
3457
+ const copy = useCopy();
3458
+ const [paymentInputComplete, setPaymentInputComplete] = useState11(false);
3459
+ const [isProcessing, setIsProcessing] = useState11(false);
3460
+ const [error, setError] = useState11(null);
3461
+ const isReady = !!(stripe && elements);
3462
+ const canSubmit = isReady && paymentInputComplete && !isProcessing && !!clientSecret;
3463
+ const submit = useCallback12(async () => {
3464
+ if (!stripe || !elements || !clientSecret) {
3465
+ const msg = copy.errors.stripeUnavailable;
3466
+ setError(msg);
3467
+ onError?.(new Error(msg));
3468
+ return;
3469
+ }
3470
+ setError(null);
3471
+ setIsProcessing(true);
3472
+ const { error: submitError } = await elements.submit();
3473
+ if (submitError) {
3474
+ const msg = submitError.message || copy.errors.paymentUnexpected;
3475
+ setError(msg);
3476
+ setIsProcessing(false);
3477
+ onError?.(new Error(msg));
3478
+ return;
3479
+ }
3480
+ const { error: confirmError, paymentIntent } = await stripe.confirmPayment({
3481
+ elements,
3482
+ clientSecret,
3483
+ confirmParams: { return_url: returnUrl },
3484
+ redirect: "if_required"
3485
+ });
3486
+ if (confirmError) {
3487
+ const msg = confirmError.message || copy.errors.paymentUnexpected;
3488
+ setError(msg);
3489
+ setIsProcessing(false);
3490
+ onError?.(new Error(msg));
3491
+ return;
3492
+ }
3493
+ setIsProcessing(false);
3494
+ if (paymentIntent) {
3495
+ await onSuccess?.(paymentIntent);
3496
+ }
3497
+ }, [stripe, elements, clientSecret, returnUrl, copy, onSuccess, onError]);
3498
+ const effectiveError = error ?? outerError;
3499
+ const ctx = useMemo9(
3500
+ () => ({
3501
+ amount,
3502
+ currency,
3503
+ state,
3504
+ clientSecret,
3505
+ stripe: stripe ?? null,
3506
+ elements: elements ?? null,
3507
+ isReady,
3508
+ isProcessing,
3509
+ paymentInputComplete,
3510
+ canSubmit,
3511
+ error: effectiveError,
3512
+ returnUrl,
3513
+ setPaymentInputComplete,
3514
+ submit
3515
+ }),
3516
+ [
3517
+ amount,
3518
+ currency,
3519
+ state,
3520
+ clientSecret,
3521
+ stripe,
3522
+ elements,
3523
+ isReady,
3524
+ isProcessing,
3525
+ paymentInputComplete,
3526
+ canSubmit,
3527
+ effectiveError,
3528
+ returnUrl,
3529
+ submit
3530
+ ]
3531
+ );
3532
+ return /* @__PURE__ */ jsx12(TopupFormContext.Provider, { value: ctx, children });
3533
+ };
3534
+ var OfflineInner = ({
3535
+ amount,
3536
+ currency,
3537
+ clientSecret,
3538
+ returnUrl,
3539
+ outerError,
3540
+ state,
3541
+ children
3542
+ }) => {
3543
+ const noopSubmit = useCallback12(async () => {
3544
+ }, []);
3545
+ const noopSet = useCallback12(() => {
3546
+ }, []);
3547
+ const ctx = useMemo9(
3548
+ () => ({
3549
+ amount,
3550
+ currency,
3551
+ state,
3552
+ clientSecret,
3553
+ stripe: null,
3554
+ elements: null,
3555
+ isReady: false,
3556
+ isProcessing: false,
3557
+ paymentInputComplete: false,
3558
+ canSubmit: false,
3559
+ error: outerError,
3560
+ returnUrl,
3561
+ setPaymentInputComplete: noopSet,
3562
+ submit: noopSubmit
3563
+ }),
3564
+ [amount, currency, state, clientSecret, outerError, returnUrl, noopSet, noopSubmit]
3565
+ );
3566
+ return /* @__PURE__ */ jsx12(TopupFormContext.Provider, { value: ctx, children });
3567
+ };
3568
+ var PaymentElementSlot2 = ({ options }) => {
3569
+ const { setPaymentInputComplete, isReady, stripe, elements } = useTopupCtx("PaymentElement");
3570
+ const locale = useLocale();
3571
+ if (!stripe || !elements) return null;
3572
+ if (!isReady) {
3573
+ return /* @__PURE__ */ jsx12("div", { "data-solvapay-topup-form-loading": "", children: /* @__PURE__ */ jsx12(Spinner, { size: "sm" }) });
3574
+ }
3575
+ return /* @__PURE__ */ jsx12("div", { "data-solvapay-topup-form-payment-element": "", children: /* @__PURE__ */ jsx12(
3576
+ StripePaymentElement2,
3577
+ {
3578
+ options,
3579
+ onChange: (e) => setPaymentInputComplete(e.complete)
3580
+ },
3581
+ locale || "default"
3582
+ ) });
3583
+ };
3584
+ var SubmitButton2 = forwardRef5(function TopupFormSubmitButton({ asChild, onClick, children, ...rest }, forwardedRef) {
3585
+ const ctx = useTopupCtx("SubmitButton");
3586
+ const copy = useCopy();
3587
+ const dataState = ctx.isProcessing ? "processing" : !ctx.canSubmit ? "disabled" : "idle";
3588
+ const content = ctx.isProcessing ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
3589
+ /* @__PURE__ */ jsx12(Spinner, { size: "sm" }),
3590
+ /* @__PURE__ */ jsx12("span", { children: copy.cta.processing })
3591
+ ] }) : children ? children : copy.cta.topUp;
3592
+ const commonProps = {
3593
+ "data-solvapay-topup-form-submit": "",
3594
+ "data-state": dataState,
3595
+ "aria-busy": ctx.isProcessing,
3596
+ "aria-disabled": !ctx.canSubmit,
3597
+ disabled: !ctx.canSubmit,
3598
+ onClick: composeEventHandlers(onClick, (e) => {
3599
+ e.preventDefault();
3600
+ void ctx.submit();
3601
+ }),
3602
+ ...rest
3603
+ };
3604
+ if (asChild) {
3605
+ return (
3606
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3607
+ /* @__PURE__ */ jsx12(Slot, { ref: forwardedRef, ...commonProps, children: content })
3608
+ );
3609
+ }
3610
+ return /* @__PURE__ */ jsx12("button", { ref: forwardedRef, type: "submit", ...commonProps, children: content });
3611
+ });
3612
+ var Loading2 = forwardRef5(function TopupFormLoading({ asChild, children, ...rest }, forwardedRef) {
3613
+ const ctx = useTopupCtx("Loading");
3614
+ if (ctx.state !== "loading") return null;
3615
+ const Comp = asChild ? Slot : "div";
3616
+ return /* @__PURE__ */ jsx12(Comp, { ref: forwardedRef, "data-solvapay-topup-form-loading": "", ...rest, children: children ?? /* @__PURE__ */ jsx12(Spinner, { size: "sm" }) });
3617
+ });
3618
+ var ErrorSlot2 = forwardRef5(function TopupFormError({ asChild, children, ...rest }, forwardedRef) {
3619
+ const ctx = useTopupCtx("Error");
3620
+ if (!ctx.error) return null;
3621
+ const Comp = asChild ? Slot : "div";
3622
+ return /* @__PURE__ */ jsx12(
3623
+ Comp,
3624
+ {
3625
+ ref: forwardedRef,
3626
+ role: "alert",
3627
+ "aria-live": "assertive",
3628
+ "aria-atomic": "true",
3629
+ "data-solvapay-topup-form-error": "",
3630
+ ...rest,
3631
+ children: children ?? ctx.error
3632
+ }
3633
+ );
3634
+ });
3635
+ var TopupFormRoot2 = Root4;
3636
+ var TopupFormPaymentElement = PaymentElementSlot2;
3637
+ var TopupFormSubmitButton2 = SubmitButton2;
3638
+ var TopupFormLoading2 = Loading2;
3639
+ var TopupFormError2 = ErrorSlot2;
3640
+ var TopupForm = {
3641
+ Root: Root4,
3642
+ AmountPicker: AmountPicker.Root,
3643
+ PaymentElement: PaymentElementSlot2,
3644
+ SubmitButton: SubmitButton2,
3645
+ Loading: Loading2,
3646
+ Error: ErrorSlot2
3647
+ };
3648
+ function useTopupForm() {
3649
+ return useTopupCtx("useTopupForm");
3650
+ }
3651
+
3652
+ // src/primitives/BalanceBadge.tsx
3653
+ import { forwardRef as forwardRef6, useContext as useContext10 } from "react";
3654
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
3655
+ var BalanceBadge = forwardRef6(function BalanceBadge2({ asChild, numberOnly, lowThreshold = 10, children, ...rest }, forwardedRef) {
3656
+ const solva = useContext10(SolvaPayContext);
3657
+ if (!solva) throw new MissingProviderError("BalanceBadge");
3658
+ const { credits, displayCurrency, creditsPerMinorUnit, displayExchangeRate, loading } = useBalance();
3659
+ const copy = useCopy();
3660
+ const locale = useLocale();
3661
+ const state = loading ? "loading" : credits == null || credits === 0 ? "zero" : credits <= lowThreshold ? "low" : "ok";
3662
+ const commonProps = {
3663
+ "data-solvapay-balance-badge": "",
3664
+ "data-state": state,
3665
+ "data-credits": credits == null ? void 0 : String(credits),
3666
+ "aria-busy": loading,
3667
+ ...rest
3668
+ };
3669
+ if (state === "loading") {
3670
+ if (asChild) {
3671
+ return (
3672
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3673
+ /* @__PURE__ */ jsx13(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ jsx13(Fragment4, {}) })
3674
+ );
3675
+ }
3676
+ return /* @__PURE__ */ jsx13("span", { ref: forwardedRef, ...commonProps });
3677
+ }
3678
+ if (credits == null) return null;
3679
+ const formattedCredits = new Intl.NumberFormat(locale).format(credits);
3680
+ let currencyEquivalent = "";
3681
+ if (!numberOnly && displayCurrency && creditsPerMinorUnit) {
3682
+ const usdCents = credits / creditsPerMinorUnit;
3683
+ const displayMajorUnits = usdCents * (displayExchangeRate ?? 1) / 100;
3684
+ const formatted = new Intl.NumberFormat(locale, {
3685
+ style: "currency",
3686
+ currency: displayCurrency,
3687
+ minimumFractionDigits: 2
3688
+ }).format(displayMajorUnits);
3689
+ currencyEquivalent = interpolate(copy.balance.currencyEquivalent, { amount: formatted });
3690
+ }
3691
+ const content = children ?? /* @__PURE__ */ jsxs5(Fragment4, { children: [
3692
+ formattedCredits,
3693
+ !numberOnly && copy.balance.credits,
3694
+ !numberOnly && currencyEquivalent
3695
+ ] });
3696
+ if (asChild) {
3697
+ return (
3698
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3699
+ /* @__PURE__ */ jsx13(Slot, { ref: forwardedRef, ...commonProps, children: content })
3700
+ );
3701
+ }
3702
+ return /* @__PURE__ */ jsx13("span", { ref: forwardedRef, ...commonProps, children: content });
3703
+ });
3704
+
3705
+ // src/primitives/PlanSelector.tsx
3706
+ import {
3707
+ createContext as createContext8,
3708
+ forwardRef as forwardRef7,
3709
+ useCallback as useCallback13,
3710
+ useContext as useContext11,
3711
+ useMemo as useMemo10
3712
+ } from "react";
3713
+
3714
+ // src/transport/list-plans.ts
3715
+ async function defaultListPlans(productRef, config) {
3716
+ const transport = config?.transport;
3717
+ if (transport) {
3718
+ return transport.listPlans ? transport.listPlans(productRef) : plansCache.get(productRef)?.plans ?? [];
3719
+ }
3720
+ const base = config?.api?.listPlans || "/api/list-plans";
3721
+ const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
3722
+ const fetchFn = config?.fetch || fetch;
3723
+ const { headers } = await buildRequestHeaders(config);
3724
+ const res = await fetchFn(url, { method: "GET", headers });
3725
+ if (!res.ok) {
3726
+ const error = new Error(`Failed to fetch plans: ${res.statusText || res.status}`);
3727
+ config?.onError?.(error, "listPlans");
3728
+ throw error;
3729
+ }
3730
+ const data = await res.json();
3731
+ return data.plans ?? [];
3732
+ }
3733
+
3734
+ // src/primitives/PlanSelector.tsx
3735
+ import { jsx as jsx14 } from "react/jsx-runtime";
3736
+ var PlanSelectorContext = createContext8(null);
3737
+ function usePlanSelectorContext(part) {
3738
+ const ctx = useContext11(PlanSelectorContext);
3739
+ if (!ctx) {
3740
+ throw new Error(`PlanSelector.${part} must be rendered inside <PlanSelector.Root>.`);
3741
+ }
3742
+ return ctx;
3743
+ }
3744
+ var CardContext = createContext8(null);
3745
+ function useCardContext(part) {
3746
+ const ctx = useContext11(CardContext);
3747
+ if (!ctx) {
3748
+ throw new Error(`PlanSelector.${part} must be rendered inside <PlanSelector.Card>.`);
3749
+ }
3750
+ return ctx;
3751
+ }
3752
+ var Root5 = forwardRef7(function PlanSelectorRoot(props, forwardedRef) {
3753
+ const {
3754
+ productRef,
3755
+ fetcher,
3756
+ filter,
3757
+ sortBy,
3758
+ autoSelectFirstPaid = true,
3759
+ initialPlanRef,
3760
+ currentPlanRef,
3761
+ popularPlanRef,
3762
+ onSelect,
3763
+ children,
3764
+ ...rest
3765
+ } = props;
3766
+ const solva = useContext11(SolvaPayContext);
3767
+ if (!solva) throw new MissingProviderError("PlanSelector");
3768
+ if (!productRef) throw new MissingProductRefError("PlanSelector");
3769
+ const { _config } = solva;
3770
+ const { purchases } = usePurchase();
3771
+ const effectiveFetcher = useMemo10(
3772
+ () => fetcher ?? ((ref) => defaultListPlans(ref, _config)),
3773
+ [fetcher, _config]
3774
+ );
3775
+ const { plans, selectedPlan, selectPlan, setSelectedPlanIndex, loading, error } = usePlans({
3776
+ productRef,
3777
+ fetcher: effectiveFetcher,
3778
+ filter,
3779
+ sortBy,
3780
+ autoSelectFirstPaid,
3781
+ initialPlanRef
3782
+ });
3783
+ const autoCurrentPlanRef = useMemo10(() => {
3784
+ const active = purchases.filter((p) => p.status === "active").sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())[0];
3785
+ return active?.planSnapshot?.reference ?? null;
3786
+ }, [purchases]);
3787
+ const resolvedCurrentPlanRef = currentPlanRef === null ? null : currentPlanRef ?? autoCurrentPlanRef;
3788
+ const isCurrent = useCallback13(
3789
+ (ref) => resolvedCurrentPlanRef === ref,
3790
+ [resolvedCurrentPlanRef]
3791
+ );
3792
+ const isFree = useCallback13(
3793
+ (ref) => plans.find((p) => p.reference === ref)?.requiresPayment === false,
3794
+ [plans]
3795
+ );
3796
+ const isPopular = useCallback13(
3797
+ (ref) => popularPlanRef === ref,
3798
+ [popularPlanRef]
3799
+ );
3800
+ const select = useCallback13(
3801
+ (ref) => {
3802
+ const plan = plans.find((p) => p.reference === ref);
3803
+ if (!plan) return;
3804
+ if (plan.requiresPayment === false) return;
3805
+ selectPlan(ref);
3806
+ onSelect?.(ref, plan);
3807
+ },
3808
+ [plans, selectPlan, onSelect]
3809
+ );
3810
+ const clearSelection = useCallback13(() => {
3811
+ setSelectedPlanIndex(-1);
3812
+ }, [setSelectedPlanIndex]);
3813
+ const selectedPlanRef = selectedPlan?.reference ?? null;
3814
+ const ctx = useMemo10(
3815
+ () => ({
3816
+ plans,
3817
+ loading,
3818
+ error,
3819
+ selectedPlan: selectedPlan ?? null,
3820
+ selectedPlanRef,
3821
+ popularPlanRef,
3822
+ currentPlanRef: resolvedCurrentPlanRef,
3823
+ isCurrent,
3824
+ isFree,
3825
+ isPopular,
3826
+ select,
3827
+ clearSelection
3828
+ }),
3829
+ [
3830
+ plans,
3831
+ loading,
3832
+ error,
3833
+ selectedPlan,
3834
+ selectedPlanRef,
3835
+ popularPlanRef,
3836
+ resolvedCurrentPlanRef,
3837
+ isCurrent,
3838
+ isFree,
3839
+ isPopular,
3840
+ select,
3841
+ clearSelection
3842
+ ]
3843
+ );
3844
+ return /* @__PURE__ */ jsx14(
3845
+ PlanSelectionProvider,
3846
+ {
3847
+ value: {
3848
+ productRef,
3849
+ selectedPlanRef,
3850
+ setSelectedPlanRef: (ref) => {
3851
+ if (ref) select(ref);
3852
+ },
3853
+ plans,
3854
+ loading,
3855
+ error
3856
+ },
3857
+ children: /* @__PURE__ */ jsx14("div", { ref: forwardedRef, "data-solvapay-plan-selector": "", ...rest, children: /* @__PURE__ */ jsx14(PlanSelectorContext.Provider, { value: ctx, children }) })
3858
+ }
3859
+ );
3860
+ });
3861
+ var Heading = forwardRef7(function PlanSelectorHeading({ asChild, children, ...rest }, forwardedRef) {
3862
+ usePlanSelectorContext("Heading");
3863
+ const copy = useCopy();
3864
+ const Comp = asChild ? Slot : "h3";
3865
+ return /* @__PURE__ */ jsx14(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-heading": "", ...rest, children: children ?? copy.planSelector.heading });
3866
+ });
3867
+ var Grid = forwardRef7(function PlanSelectorGrid({ children, ...rest }, forwardedRef) {
3868
+ const ctx = usePlanSelectorContext("Grid");
3869
+ return /* @__PURE__ */ jsx14("div", { ref: forwardedRef, "data-solvapay-plan-selector-grid": "", ...rest, children: ctx.plans.map((plan) => {
3870
+ const isCurrent = ctx.isCurrent(plan.reference);
3871
+ const isFree = ctx.isFree(plan.reference);
3872
+ const isPopular = ctx.isPopular(plan.reference);
3873
+ const selected = ctx.selectedPlanRef === plan.reference;
3874
+ const disabled = isCurrent || isFree;
3875
+ const state = isCurrent ? "current" : isFree ? "disabled" : selected ? "selected" : "idle";
3876
+ const cardCtx = {
3877
+ plan,
3878
+ state,
3879
+ isCurrent,
3880
+ isFree,
3881
+ isPopular,
3882
+ disabled,
3883
+ select: () => ctx.select(plan.reference)
3884
+ };
3885
+ return /* @__PURE__ */ jsx14(CardContext.Provider, { value: cardCtx, children }, plan.reference);
3886
+ }) });
3887
+ });
3888
+ var Card = forwardRef7(function PlanSelectorCard({ asChild, onClick, children, ...rest }, forwardedRef) {
3889
+ const card = useCardContext("Card");
3890
+ const dataTrial = !!(card.plan.trialDays && card.plan.trialDays > 0);
3891
+ const commonProps = {
3892
+ "data-solvapay-plan-selector-card": "",
3893
+ "data-state": card.state,
3894
+ "data-free": card.isFree ? "" : void 0,
3895
+ "data-popular": card.isPopular ? "" : void 0,
3896
+ "data-trial": dataTrial ? "" : void 0,
3897
+ "aria-disabled": card.disabled || void 0,
3898
+ onClick: composeEventHandlers(onClick, () => {
3899
+ if (!card.disabled) card.select();
3900
+ }),
3901
+ ...rest
3902
+ };
3903
+ if (asChild) {
3904
+ return (
3905
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3906
+ /* @__PURE__ */ jsx14(Slot, { ref: forwardedRef, ...commonProps, children })
3907
+ );
3908
+ }
3909
+ return /* @__PURE__ */ jsx14(
3910
+ "button",
3911
+ {
3912
+ ref: forwardedRef,
3913
+ type: "button",
3914
+ disabled: card.disabled,
3915
+ ...commonProps,
3916
+ children
3917
+ }
3918
+ );
3919
+ });
3920
+ var CardName = forwardRef7(function PlanSelectorCardName({ asChild, children, ...rest }, forwardedRef) {
3921
+ const card = useCardContext("CardName");
3922
+ const fallback = card.plan.requiresPayment === false ? "Free" : card.plan.type === "usage-based" ? "Pay as you go" : card.plan.type === "recurring" ? "Plan" : null;
3923
+ const label = card.plan.name ?? fallback;
3924
+ if (!label && children == null) return null;
3925
+ const Comp = asChild ? Slot : "span";
3926
+ return /* @__PURE__ */ jsx14(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-name": "", ...rest, children: children ?? label });
3927
+ });
3928
+ var CardPrice = forwardRef7(function PlanSelectorCardPrice({ asChild, children, ...rest }, forwardedRef) {
3929
+ const card = useCardContext("CardPrice");
3930
+ const locale = useLocale();
3931
+ const copy = useCopy();
3932
+ const formatted = useMemo10(() => {
3933
+ if (card.isFree) return copy.planSelector.freeBadge;
3934
+ if (card.plan.type === "usage-based") {
3935
+ const creditsPerUnit = card.plan.creditsPerUnit ?? 1;
3936
+ const perCallMinor = Math.max(1, Math.round(1 / creditsPerUnit));
3937
+ const rate = formatPrice(perCallMinor, card.plan.currency ?? "usd", {
3938
+ locale,
3939
+ free: ""
3940
+ });
3941
+ return `${rate} / call`;
3942
+ }
3943
+ return formatPrice(card.plan.price ?? 0, card.plan.currency ?? "usd", {
3944
+ locale,
3945
+ free: copy.interval.free
3946
+ });
3947
+ }, [
3948
+ card.isFree,
3949
+ card.plan.type,
3950
+ card.plan.price,
3951
+ card.plan.currency,
3952
+ card.plan.creditsPerUnit,
3953
+ locale,
3954
+ copy.planSelector.freeBadge,
3955
+ copy.interval.free
3956
+ ]);
3957
+ const Comp = asChild ? Slot : "span";
3958
+ return /* @__PURE__ */ jsx14(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-price": "", ...rest, children: children ?? formatted });
3959
+ });
3960
+ var CardInterval = forwardRef7(function PlanSelectorCardInterval({ asChild, children, ...rest }, forwardedRef) {
3961
+ const card = useCardContext("CardInterval");
3962
+ const copy = useCopy();
3963
+ if (card.isFree || card.plan.type === "usage-based") return null;
3964
+ const rawInterval = card.plan.interval ?? normalizeBillingCycle(card.plan.billingCycle);
3965
+ if (!rawInterval) return null;
3966
+ const text = interpolate(copy.planSelector.perIntervalShort, { interval: rawInterval });
3967
+ const Comp = asChild ? Slot : "span";
3968
+ return /* @__PURE__ */ jsx14(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-interval": "", ...rest, children: children ?? text });
3969
+ });
3970
+ function normalizeBillingCycle(cycle) {
3971
+ if (!cycle) return null;
3972
+ const lc = cycle.toLowerCase();
3973
+ if (lc === "monthly" || lc === "month") return "month";
3974
+ if (lc === "yearly" || lc === "annually" || lc === "annual" || lc === "year") return "year";
3975
+ if (lc === "weekly" || lc === "week") return "week";
3976
+ if (lc === "daily" || lc === "day") return "day";
3977
+ return cycle;
3978
+ }
3979
+ var CardBadge = forwardRef7(function PlanSelectorCardBadge({ asChild, children, ...rest }, forwardedRef) {
3980
+ const card = useCardContext("CardBadge");
3981
+ const copy = useCopy();
3982
+ let variant = null;
3983
+ let label = "";
3984
+ if (card.isCurrent) {
3985
+ variant = "current";
3986
+ label = copy.planSelector.currentBadge;
3987
+ } else if (card.isPopular && !card.isFree) {
3988
+ variant = "popular";
3989
+ label = copy.planSelector.popularBadge;
3990
+ }
3991
+ if (!variant) return null;
3992
+ const Comp = asChild ? Slot : "span";
3993
+ return /* @__PURE__ */ jsx14(
3994
+ Comp,
3995
+ {
3996
+ ref: forwardedRef,
3997
+ "data-solvapay-plan-selector-card-badge": "",
3998
+ "data-variant": variant,
3999
+ ...rest,
4000
+ children: children ?? label
4001
+ }
4002
+ );
4003
+ });
4004
+ var Loading3 = forwardRef7(function PlanSelectorLoading({ asChild, children, ...rest }, forwardedRef) {
4005
+ const ctx = usePlanSelectorContext("Loading");
4006
+ if (!ctx.loading || ctx.plans.length > 0) return null;
4007
+ const Comp = asChild ? Slot : "div";
4008
+ return /* @__PURE__ */ jsx14(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-loading": "", ...rest, children });
4009
+ });
4010
+ var ErrorSlot3 = forwardRef7(function PlanSelectorError({ asChild, children, ...rest }, forwardedRef) {
4011
+ const ctx = usePlanSelectorContext("Error");
4012
+ if (!ctx.error) return null;
4013
+ const Comp = asChild ? Slot : "div";
4014
+ return /* @__PURE__ */ jsx14(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-error": "", role: "alert", ...rest, children: children ?? ctx.error.message });
4015
+ });
4016
+ var PlanSelectorRoot2 = Root5;
4017
+ var PlanSelectorHeading2 = Heading;
4018
+ var PlanSelectorGrid2 = Grid;
4019
+ var PlanSelectorCard2 = Card;
4020
+ var PlanSelectorCardName2 = CardName;
4021
+ var PlanSelectorCardPrice2 = CardPrice;
4022
+ var PlanSelectorCardInterval2 = CardInterval;
4023
+ var PlanSelectorCardBadge2 = CardBadge;
4024
+ var PlanSelectorLoading2 = Loading3;
4025
+ var PlanSelectorError2 = ErrorSlot3;
4026
+ var PlanSelector = {
4027
+ Root: Root5,
4028
+ Heading,
4029
+ Grid,
4030
+ Card,
4031
+ CardName,
4032
+ CardPrice,
4033
+ CardInterval,
4034
+ CardBadge,
4035
+ Loading: Loading3,
4036
+ Error: ErrorSlot3
4037
+ };
4038
+ function usePlanSelector() {
4039
+ return usePlanSelectorContext("usePlanSelector");
4040
+ }
4041
+
4042
+ // src/hooks/usePurchaseActions.ts
4043
+ import { useState as useState12, useCallback as useCallback14 } from "react";
4044
+ function usePurchaseActions() {
4045
+ const ctx = useSolvaPay();
4046
+ const [isCancelling, setIsCancelling] = useState12(false);
4047
+ const [isReactivating, setIsReactivating] = useState12(false);
4048
+ const [isActivating, setIsActivating] = useState12(false);
4049
+ const cancelRenewal = useCallback14(
4050
+ async (params) => {
4051
+ setIsCancelling(true);
4052
+ try {
4053
+ return await ctx.cancelRenewal(params);
4054
+ } finally {
4055
+ setIsCancelling(false);
4056
+ }
4057
+ },
4058
+ [ctx.cancelRenewal]
4059
+ );
4060
+ const reactivateRenewal = useCallback14(
4061
+ async (params) => {
4062
+ setIsReactivating(true);
4063
+ try {
4064
+ return await ctx.reactivateRenewal(params);
4065
+ } finally {
4066
+ setIsReactivating(false);
4067
+ }
4068
+ },
4069
+ [ctx.reactivateRenewal]
4070
+ );
4071
+ const activatePlan = useCallback14(
4072
+ async (params) => {
4073
+ setIsActivating(true);
4074
+ try {
4075
+ return await ctx.activatePlan(params);
4076
+ } finally {
4077
+ setIsActivating(false);
4078
+ }
4079
+ },
4080
+ [ctx.activatePlan]
4081
+ );
4082
+ return {
4083
+ cancelRenewal,
4084
+ reactivateRenewal,
4085
+ activatePlan,
4086
+ isCancelling,
4087
+ isReactivating,
4088
+ isActivating
4089
+ };
4090
+ }
4091
+
4092
+ // src/primitives/CancelPlanButton.tsx
4093
+ import { forwardRef as forwardRef8, useCallback as useCallback15, useContext as useContext12 } from "react";
4094
+ import { Fragment as Fragment5, jsx as jsx15 } from "react/jsx-runtime";
4095
+ function resolveConfirmText(confirm, purchase, defaults) {
4096
+ if (confirm === false) return null;
4097
+ if (typeof confirm === "string") return confirm;
4098
+ const planType = purchase?.planSnapshot?.planType;
4099
+ if (planType === "usage-based") return defaults.usageBased;
4100
+ return defaults.recurring;
4101
+ }
4102
+ var CancelPlanButton = forwardRef8(
4103
+ function CancelPlanButton2({
4104
+ asChild,
4105
+ purchaseRef,
4106
+ reason,
4107
+ confirm = true,
4108
+ onCancelled,
4109
+ onError,
4110
+ onClick,
4111
+ children,
4112
+ ...rest
4113
+ }, forwardedRef) {
4114
+ const solva = useContext12(SolvaPayContext);
4115
+ if (!solva) throw new MissingProviderError("CancelPlanButton");
4116
+ const copy = useCopy();
4117
+ const { activePurchase } = usePurchase();
4118
+ const { cancelRenewal, isCancelling } = usePurchaseActions();
4119
+ const effectivePurchase = purchaseRef ? activePurchase && activePurchase.reference === purchaseRef ? activePurchase : { reference: purchaseRef } : activePurchase;
4120
+ const effectiveRef = purchaseRef ?? effectivePurchase?.reference;
4121
+ const cancel = useCallback15(async () => {
4122
+ if (!effectiveRef) return;
4123
+ const confirmText = resolveConfirmText(confirm, effectivePurchase, {
4124
+ recurring: copy.cancelPlan.confirmRecurring,
4125
+ usageBased: copy.cancelPlan.confirmUsageBased
4126
+ });
4127
+ if (confirmText && typeof window !== "undefined" && !window.confirm(confirmText)) {
4128
+ return;
4129
+ }
4130
+ try {
4131
+ await cancelRenewal({ purchaseRef: effectiveRef, reason });
4132
+ onCancelled?.();
4133
+ } catch (err) {
4134
+ onError?.(err instanceof Error ? err : new Error(String(err)));
4135
+ }
4136
+ }, [effectiveRef, effectivePurchase, confirm, copy, cancelRenewal, reason, onCancelled, onError]);
4137
+ const disabled = isCancelling || !effectiveRef;
4138
+ const state = isCancelling ? "cancelling" : "idle";
4139
+ const commonProps = {
4140
+ "data-solvapay-cancel-plan": "",
4141
+ "data-state": state,
4142
+ "aria-busy": isCancelling,
4143
+ "aria-disabled": disabled || void 0,
4144
+ disabled,
4145
+ onClick: composeEventHandlers(onClick, () => {
4146
+ void cancel();
4147
+ }),
4148
+ ...rest
4149
+ };
4150
+ const label = isCancelling ? copy.cancelPlan.buttonLoading : copy.cancelPlan.button;
4151
+ if (asChild) {
4152
+ return (
4153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4154
+ /* @__PURE__ */ jsx15(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ jsx15(Fragment5, { children: label }) })
4155
+ );
4156
+ }
4157
+ return /* @__PURE__ */ jsx15("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? label });
4158
+ }
4159
+ );
4160
+
4161
+ // src/hooks/usePurchaseStatus.ts
4162
+ import { useMemo as useMemo11, useCallback as useCallback16 } from "react";
4163
+ function usePurchaseStatus() {
4164
+ const { purchases } = usePurchase();
4165
+ const locale = useLocale();
4166
+ const isPaidPurchase2 = useCallback16((p) => {
4167
+ return (p.amount ?? 0) > 0;
4168
+ }, []);
4169
+ const purchaseData = useMemo11(() => {
4170
+ const cancelledPaidPurchases = purchases.filter((p) => {
4171
+ return p.status === "active" && p.cancelledAt && isPaidPurchase2(p) && isPlanPurchase(p);
4172
+ });
4173
+ const cancelledPurchase = cancelledPaidPurchases.sort(
4174
+ (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
4175
+ )[0] || null;
4176
+ const shouldShowCancelledNotice = !!cancelledPurchase;
4177
+ return {
4178
+ cancelledPurchase,
4179
+ shouldShowCancelledNotice
4180
+ };
4181
+ }, [purchases, isPaidPurchase2]);
4182
+ const formatDate = useCallback16(
4183
+ (dateString) => {
4184
+ if (!dateString) return null;
4185
+ return new Date(dateString).toLocaleDateString(locale || "en-US", {
4186
+ year: "numeric",
4187
+ month: "long",
4188
+ day: "numeric"
4189
+ });
4190
+ },
4191
+ [locale]
4192
+ );
4193
+ const getDaysUntilExpiration = useCallback16((endDate) => {
4194
+ if (!endDate) return null;
4195
+ const now = /* @__PURE__ */ new Date();
4196
+ const expiration = new Date(endDate);
4197
+ const diffTime = expiration.getTime() - now.getTime();
4198
+ const diffDays = Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
4199
+ return diffDays > 0 ? diffDays : 0;
4200
+ }, []);
4201
+ return {
4202
+ cancelledPurchase: purchaseData.cancelledPurchase,
4203
+ shouldShowCancelledNotice: purchaseData.shouldShowCancelledNotice,
4204
+ formatDate,
4205
+ getDaysUntilExpiration
4206
+ };
4207
+ }
4208
+
4209
+ // src/primitives/CancelledPlanNotice.tsx
4210
+ import {
4211
+ createContext as createContext9,
4212
+ forwardRef as forwardRef9,
4213
+ useCallback as useCallback17,
4214
+ useContext as useContext13,
4215
+ useMemo as useMemo12
4216
+ } from "react";
4217
+ import { Fragment as Fragment6, jsx as jsx16 } from "react/jsx-runtime";
4218
+ var CancelledPlanNoticeContext = createContext9(null);
4219
+ function useNoticeCtx(part) {
4220
+ const ctx = useContext13(CancelledPlanNoticeContext);
4221
+ if (!ctx) {
4222
+ throw new Error(
4223
+ `CancelledPlanNotice.${part} must be rendered inside <CancelledPlanNotice.Root>.`
4224
+ );
4225
+ }
4226
+ return ctx;
4227
+ }
4228
+ var Root6 = forwardRef9(function CancelledPlanNoticeRoot({ onReactivated, onError, asChild, children, ...rest }, forwardedRef) {
4229
+ const solva = useContext13(SolvaPayContext);
4230
+ if (!solva) throw new MissingProviderError("CancelledPlanNotice");
4231
+ const {
4232
+ cancelledPurchase,
4233
+ shouldShowCancelledNotice,
4234
+ formatDate,
4235
+ getDaysUntilExpiration
4236
+ } = usePurchaseStatus();
4237
+ const { reactivateRenewal, isReactivating } = usePurchaseActions();
4238
+ const reactivate = useCallback17(async () => {
4239
+ if (!cancelledPurchase) return;
4240
+ try {
4241
+ await reactivateRenewal({ purchaseRef: cancelledPurchase.reference });
4242
+ onReactivated?.();
4243
+ } catch (err) {
4244
+ onError?.(err instanceof Error ? err : new Error(String(err)));
4245
+ }
4246
+ }, [cancelledPurchase, reactivateRenewal, onReactivated, onError]);
4247
+ const daysRemaining = useMemo12(
4248
+ () => cancelledPurchase ? getDaysUntilExpiration(cancelledPurchase.endDate) : null,
4249
+ [cancelledPurchase, getDaysUntilExpiration]
4250
+ );
4251
+ const ctx = useMemo12(() => {
4252
+ if (!cancelledPurchase) return null;
4253
+ const state = daysRemaining != null && daysRemaining > 0 ? "active" : "expired";
4254
+ return {
4255
+ purchase: cancelledPurchase,
4256
+ state,
4257
+ daysRemaining,
4258
+ hasReason: !!cancelledPurchase.cancellationReason,
4259
+ formatDate,
4260
+ reactivate,
4261
+ isReactivating
4262
+ };
4263
+ }, [cancelledPurchase, daysRemaining, formatDate, reactivate, isReactivating]);
4264
+ if (!shouldShowCancelledNotice || !ctx) return null;
4265
+ const Comp = asChild ? Slot : "div";
4266
+ return /* @__PURE__ */ jsx16(CancelledPlanNoticeContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx16(
4267
+ Comp,
4268
+ {
4269
+ ref: forwardedRef,
4270
+ "data-solvapay-cancelled-notice": "",
4271
+ "data-state": ctx.state,
4272
+ "data-has-reason": ctx.hasReason ? "" : void 0,
4273
+ ...rest,
4274
+ children
4275
+ }
4276
+ ) });
4277
+ });
4278
+ var Heading2 = forwardRef9(function CancelledPlanNoticeHeading({ asChild, children, ...rest }, forwardedRef) {
4279
+ useNoticeCtx("Heading");
4280
+ const copy = useCopy();
4281
+ const Comp = asChild ? Slot : "p";
4282
+ return /* @__PURE__ */ jsx16(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-heading": "", ...rest, children: children ?? copy.cancelledNotice.heading });
4283
+ });
4284
+ var Expires = forwardRef9(function CancelledPlanNoticeExpires({ asChild, children, ...rest }, forwardedRef) {
4285
+ const ctx = useNoticeCtx("Expires");
4286
+ const copy = useCopy();
4287
+ const date = ctx.formatDate(ctx.purchase.endDate);
4288
+ if (!ctx.purchase.endDate) {
4289
+ const Comp2 = asChild ? Slot : "p";
4290
+ return /* @__PURE__ */ jsx16(Comp2, { ref: forwardedRef, "data-solvapay-cancelled-notice-expires": "", ...rest, children: children ?? copy.cancelledNotice.accessEnded });
4291
+ }
4292
+ const Comp = asChild ? Slot : "p";
4293
+ return /* @__PURE__ */ jsx16(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-expires": "", ...rest, children: children ?? interpolate(copy.cancelledNotice.expiresLabel, { date: date ?? "" }) });
4294
+ });
4295
+ var DaysRemaining = forwardRef9(
4296
+ function CancelledPlanNoticeDaysRemaining({ asChild, children, ...rest }, forwardedRef) {
4297
+ const ctx = useNoticeCtx("DaysRemaining");
4298
+ const copy = useCopy();
4299
+ if (ctx.daysRemaining == null || ctx.daysRemaining <= 0) return null;
4300
+ const template = ctx.daysRemaining === 1 ? copy.cancelledNotice.dayRemaining : copy.cancelledNotice.daysRemaining;
4301
+ const Comp = asChild ? Slot : "p";
4302
+ return /* @__PURE__ */ jsx16(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-days-remaining": "", ...rest, children: children ?? interpolate(template, { days: ctx.daysRemaining }) });
4303
+ }
4304
+ );
4305
+ var AccessUntil = forwardRef9(
4306
+ function CancelledPlanNoticeAccessUntil({ asChild, children, ...rest }, forwardedRef) {
4307
+ const ctx = useNoticeCtx("AccessUntil");
4308
+ const copy = useCopy();
4309
+ if (!ctx.purchase.endDate) return null;
4310
+ const Comp = asChild ? Slot : "p";
4311
+ return /* @__PURE__ */ jsx16(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-access-until": "", ...rest, children: children ?? interpolate(copy.cancelledNotice.accessUntil, {
4312
+ product: ctx.purchase.productName
4313
+ }) });
4314
+ }
4315
+ );
4316
+ var CancelledOn = forwardRef9(
4317
+ function CancelledPlanNoticeCancelledOn({ asChild, children, ...rest }, forwardedRef) {
4318
+ const ctx = useNoticeCtx("CancelledOn");
4319
+ const copy = useCopy();
4320
+ if (!ctx.purchase.cancelledAt) return null;
4321
+ const date = ctx.formatDate(ctx.purchase.cancelledAt);
4322
+ const Comp = asChild ? Slot : "p";
4323
+ return /* @__PURE__ */ jsx16(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-cancelled-on": "", ...rest, children: children ?? interpolate(copy.cancelledNotice.cancelledOn, { date: date ?? "" }) });
4324
+ }
4325
+ );
4326
+ var Reason = forwardRef9(function CancelledPlanNoticeReason({ asChild, children, ...rest }, forwardedRef) {
4327
+ const ctx = useNoticeCtx("Reason");
4328
+ if (!ctx.hasReason) return null;
4329
+ const Comp = asChild ? Slot : "span";
4330
+ return /* @__PURE__ */ jsx16(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-reason": "", ...rest, children: children ?? ctx.purchase.cancellationReason });
4331
+ });
4332
+ var ReactivateButton = forwardRef9(
4333
+ function CancelledPlanNoticeReactivateButton({ asChild, onClick, children, ...rest }, forwardedRef) {
4334
+ const ctx = useNoticeCtx("ReactivateButton");
4335
+ const copy = useCopy();
4336
+ const disabled = ctx.isReactivating;
4337
+ const commonProps = {
4338
+ "data-solvapay-cancelled-notice-reactivate": "",
4339
+ "data-state": ctx.isReactivating ? "reactivating" : "idle",
4340
+ "aria-busy": ctx.isReactivating,
4341
+ "aria-disabled": disabled || void 0,
4342
+ disabled,
4343
+ onClick: composeEventHandlers(onClick, () => {
4344
+ void ctx.reactivate();
4345
+ }),
4346
+ ...rest
4347
+ };
4348
+ const label = ctx.isReactivating ? copy.cancelledNotice.reactivateButtonLoading : copy.cancelledNotice.reactivateButton;
4349
+ if (asChild) {
4350
+ return (
4351
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4352
+ /* @__PURE__ */ jsx16(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ jsx16(Fragment6, { children: label }) })
4353
+ );
4354
+ }
4355
+ return /* @__PURE__ */ jsx16("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? label });
4356
+ }
4357
+ );
4358
+ var CancelledPlanNoticeRoot2 = Root6;
4359
+ var CancelledPlanNoticeHeading2 = Heading2;
4360
+ var CancelledPlanNoticeExpires2 = Expires;
4361
+ var CancelledPlanNoticeDaysRemaining2 = DaysRemaining;
4362
+ var CancelledPlanNoticeAccessUntil2 = AccessUntil;
4363
+ var CancelledPlanNoticeCancelledOn2 = CancelledOn;
4364
+ var CancelledPlanNoticeReason2 = Reason;
4365
+ var CancelledPlanNoticeReactivateButton2 = ReactivateButton;
4366
+ var CancelledPlanNotice = {
4367
+ Root: Root6,
4368
+ Heading: Heading2,
4369
+ Expires,
4370
+ DaysRemaining,
4371
+ AccessUntil,
4372
+ CancelledOn,
4373
+ Reason,
4374
+ ReactivateButton
4375
+ };
4376
+ function useCancelledPlanNotice() {
4377
+ return useNoticeCtx("useCancelledPlanNotice");
4378
+ }
4379
+
4380
+ // src/hooks/useTransport.ts
4381
+ import { useMemo as useMemo13 } from "react";
4382
+ function useTransport() {
4383
+ const { _config } = useSolvaPay();
4384
+ return useMemo13(() => _config?.transport ?? createHttpTransport(_config), [_config]);
4385
+ }
4386
+
4387
+ // src/hooks/useUsage.ts
4388
+ import { useCallback as useCallback18, useEffect as useEffect10, useMemo as useMemo14, useState as useState13 } from "react";
4389
+ function deriveUsage(purchase) {
4390
+ if (!purchase) return null;
4391
+ const snap = purchase.planSnapshot;
4392
+ const usage = purchase.usage;
4393
+ const meterRef = snap?.meterRef ?? null;
4394
+ const total = typeof snap?.limit === "number" ? snap.limit : null;
4395
+ if (meterRef === null && !usage) return null;
4396
+ const used = typeof usage?.used === "number" ? usage.used : 0;
4397
+ const remaining = total !== null ? Math.max(0, total - used) : null;
4398
+ const percentUsed = total !== null && total > 0 ? Math.min(100, Math.round(used / total * 1e4) / 100) : null;
4399
+ return {
4400
+ meterRef,
4401
+ total,
4402
+ used,
4403
+ remaining,
4404
+ percentUsed,
4405
+ ...usage?.periodStart ? { periodStart: usage.periodStart } : {},
4406
+ ...usage?.periodEnd ? { periodEnd: usage.periodEnd } : {},
4407
+ ...purchase.reference ? { purchaseRef: purchase.reference } : {}
4408
+ };
4409
+ }
4410
+ function useUsage() {
4411
+ const { activePurchase, refetch: refetchPurchase, loading: purchaseLoading } = usePurchase();
4412
+ const transport = useTransport();
4413
+ const [override, setOverride] = useState13(null);
4414
+ const [error, setError] = useState13(null);
4415
+ const [transportLoading, setTransportLoading] = useState13(false);
4416
+ const derived = useMemo14(() => deriveUsage(activePurchase ?? null), [activePurchase]);
4417
+ const activePurchaseRef = activePurchase?.reference ?? null;
4418
+ useEffect10(() => {
4419
+ setOverride(null);
4420
+ }, [activePurchaseRef]);
4421
+ const usage = override ?? derived;
4422
+ const refetch = useCallback18(async () => {
4423
+ setError(null);
4424
+ if (typeof transport.getUsage === "function") {
4425
+ setTransportLoading(true);
4426
+ try {
4427
+ const next = await transport.getUsage();
4428
+ setOverride(next);
4429
+ } catch (err) {
4430
+ setError(err instanceof Error ? err : new Error("Failed to load usage"));
4431
+ } finally {
4432
+ setTransportLoading(false);
4433
+ }
4434
+ return;
4435
+ }
4436
+ try {
4437
+ await refetchPurchase();
4438
+ } catch (err) {
4439
+ setError(err instanceof Error ? err : new Error("Failed to refetch purchase"));
4440
+ }
4441
+ }, [transport, refetchPurchase]);
4442
+ const percentUsed = usage?.percentUsed ?? null;
4443
+ const isApproachingLimit = percentUsed !== null && percentUsed >= 80 && percentUsed < 100;
4444
+ const isAtLimit = percentUsed !== null && percentUsed >= 100;
4445
+ const isUnlimited = usage !== null && usage.total === null;
4446
+ return {
4447
+ usage,
4448
+ loading: purchaseLoading || transportLoading,
4449
+ error,
4450
+ refetch,
4451
+ percentUsed,
4452
+ isApproachingLimit,
4453
+ isAtLimit,
4454
+ isUnlimited,
4455
+ meterRef: usage?.meterRef ?? null
4456
+ };
4457
+ }
4458
+
4459
+ // src/primitives/UsageMeter.tsx
4460
+ import { createContext as createContext10, forwardRef as forwardRef10, useContext as useContext14, useMemo as useMemo15 } from "react";
4461
+ import { jsx as jsx17 } from "react/jsx-runtime";
4462
+ var UsageMeterContext = createContext10(null);
4463
+ function useUsageMeterCtx(part) {
4464
+ const ctx = useContext14(UsageMeterContext);
4465
+ if (!ctx) {
4466
+ throw new Error(`UsageMeter.${part} must be rendered inside <UsageMeter.Root>.`);
4467
+ }
4468
+ return ctx;
4469
+ }
4470
+ var Root7 = forwardRef10(function UsageMeterRoot({
4471
+ warningAt = 75,
4472
+ criticalAt = 90,
4473
+ classNames,
4474
+ asChild,
4475
+ usageOverride,
4476
+ children,
4477
+ className,
4478
+ ...rest
4479
+ }, forwardedRef) {
4480
+ const hookResult = useUsage();
4481
+ const usage = usageOverride !== void 0 ? usageOverride : hookResult.usage;
4482
+ const loading = hookResult.loading && !usage;
4483
+ const percentUsed = usage?.percentUsed ?? null;
4484
+ const state = loading ? "loading" : percentUsed === null ? "safe" : percentUsed >= criticalAt ? "critical" : percentUsed >= warningAt ? "warning" : "safe";
4485
+ const ctx = useMemo15(
4486
+ () => ({
4487
+ usage,
4488
+ loading,
4489
+ error: hookResult.error,
4490
+ percentUsed,
4491
+ isApproachingLimit: hookResult.isApproachingLimit,
4492
+ isAtLimit: hookResult.isAtLimit,
4493
+ isUnlimited: hookResult.isUnlimited,
4494
+ state,
4495
+ warningAt,
4496
+ criticalAt
4497
+ }),
4498
+ [
4499
+ usage,
4500
+ loading,
4501
+ hookResult.error,
4502
+ percentUsed,
4503
+ hookResult.isApproachingLimit,
4504
+ hookResult.isAtLimit,
4505
+ hookResult.isUnlimited,
4506
+ state,
4507
+ warningAt,
4508
+ criticalAt
4509
+ ]
4510
+ );
4511
+ const rootClass = [classNames?.root ?? "solvapay-usage-meter", className].filter(Boolean).join(" ");
4512
+ const Comp = asChild ? Slot : "div";
4513
+ return /* @__PURE__ */ jsx17(UsageMeterContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx17(
4514
+ Comp,
4515
+ {
4516
+ ref: forwardedRef,
4517
+ "data-solvapay-usage-meter": "",
4518
+ "data-state": state,
4519
+ className: rootClass,
4520
+ ...rest,
4521
+ children
4522
+ }
4523
+ ) });
4524
+ });
4525
+ var Bar = forwardRef10(function UsageMeterBar({ className, style, ...rest }, forwardedRef) {
4526
+ const ctx = useUsageMeterCtx("Bar");
4527
+ const width = ctx.percentUsed === null ? 0 : Math.max(0, Math.min(100, ctx.percentUsed));
4528
+ return /* @__PURE__ */ jsx17(
4529
+ "div",
4530
+ {
4531
+ ref: forwardedRef,
4532
+ "data-solvapay-usage-meter-bar": "",
4533
+ "data-state": ctx.state,
4534
+ role: "progressbar",
4535
+ "aria-valuenow": Math.round(width),
4536
+ "aria-valuemin": 0,
4537
+ "aria-valuemax": 100,
4538
+ className: className ?? "solvapay-usage-meter-bar",
4539
+ style: {
4540
+ // Expose as a CSS custom property so default styles can drive
4541
+ // `width: var(--solvapay-usage-meter-fill)` without inline styling.
4542
+ ["--solvapay-usage-meter-fill"]: `${width}%`,
4543
+ ...style
4544
+ },
4545
+ ...rest
4546
+ }
4547
+ );
4548
+ });
4549
+ var Label = forwardRef10(function UsageMeterLabel({ className, children, ...rest }, forwardedRef) {
4550
+ const ctx = useUsageMeterCtx("Label");
4551
+ const copy = useCopy();
4552
+ if (!ctx.usage) return null;
4553
+ if (ctx.isUnlimited) {
4554
+ return /* @__PURE__ */ jsx17(
4555
+ "span",
4556
+ {
4557
+ ref: forwardedRef,
4558
+ "data-solvapay-usage-meter-label": "",
4559
+ className: className ?? "solvapay-usage-meter-label",
4560
+ ...rest,
4561
+ children: children ?? copy.usage.unlimitedLabel
4562
+ }
4563
+ );
4564
+ }
4565
+ const unit = ctx.usage.meterRef ?? "units";
4566
+ const label = ctx.usage.total !== null ? interpolate(copy.usage.usedLabel, {
4567
+ used: String(ctx.usage.used),
4568
+ total: String(ctx.usage.total),
4569
+ unit
4570
+ }) : interpolate(copy.usage.remainingLabel, {
4571
+ remaining: String(ctx.usage.remaining ?? 0),
4572
+ unit
4573
+ });
4574
+ return /* @__PURE__ */ jsx17(
4575
+ "span",
4576
+ {
4577
+ ref: forwardedRef,
4578
+ "data-solvapay-usage-meter-label": "",
4579
+ className: className ?? "solvapay-usage-meter-label",
4580
+ ...rest,
4581
+ children: children ?? label
4582
+ }
4583
+ );
4584
+ });
4585
+ var Percentage = forwardRef10(function UsageMeterPercentage({ className, children, ...rest }, forwardedRef) {
4586
+ const ctx = useUsageMeterCtx("Percentage");
4587
+ const copy = useCopy();
4588
+ if (ctx.percentUsed === null) return null;
4589
+ return /* @__PURE__ */ jsx17(
4590
+ "span",
4591
+ {
4592
+ ref: forwardedRef,
4593
+ "data-solvapay-usage-meter-percentage": "",
4594
+ className: className ?? "solvapay-usage-meter-percentage",
4595
+ ...rest,
4596
+ children: children ?? interpolate(copy.usage.percentUsedLabel, { percent: String(Math.round(ctx.percentUsed)) })
4597
+ }
4598
+ );
4599
+ });
4600
+ function formatResetsIn(copy, periodEnd, nowMs = typeof performance !== "undefined" ? performance.timeOrigin + performance.now() : 0) {
4601
+ const end = new Date(periodEnd).getTime();
4602
+ if (Number.isNaN(end)) return null;
4603
+ const daysLeft = Math.max(0, Math.ceil((end - nowMs) / (24 * 60 * 60 * 1e3)));
4604
+ return daysLeft > 0 ? interpolate(copy.usage.resetsInLabel, { days: String(daysLeft) }) : interpolate(copy.usage.resetsOnLabel, { date: new Date(periodEnd).toLocaleDateString() });
4605
+ }
4606
+ var ResetsIn = forwardRef10(function UsageMeterResetsIn({ className, children, ...rest }, forwardedRef) {
4607
+ const ctx = useUsageMeterCtx("ResetsIn");
4608
+ const copy = useCopy();
4609
+ const periodEnd = ctx.usage?.periodEnd;
4610
+ if (!periodEnd) return null;
4611
+ const label = formatResetsIn(copy, periodEnd);
4612
+ if (!label) return null;
4613
+ return /* @__PURE__ */ jsx17(
4614
+ "span",
4615
+ {
4616
+ ref: forwardedRef,
4617
+ "data-solvapay-usage-meter-resets-in": "",
4618
+ className: className ?? "solvapay-usage-meter-resets-in",
4619
+ ...rest,
4620
+ children: children ?? label
4621
+ }
4622
+ );
4623
+ });
4624
+ var Loading4 = forwardRef10(function UsageMeterLoading({ className, children, ...rest }, forwardedRef) {
4625
+ const ctx = useUsageMeterCtx("Loading");
4626
+ const copy = useCopy();
4627
+ if (ctx.state !== "loading") return null;
4628
+ return /* @__PURE__ */ jsx17(
4629
+ "div",
4630
+ {
4631
+ ref: forwardedRef,
4632
+ "data-solvapay-usage-meter-loading": "",
4633
+ className: className ?? "solvapay-usage-meter-loading",
4634
+ ...rest,
4635
+ children: children ?? copy.usage.loadingLabel
4636
+ }
4637
+ );
4638
+ });
4639
+ var Empty = forwardRef10(function UsageMeterEmpty({ className, children, ...rest }, forwardedRef) {
4640
+ const ctx = useUsageMeterCtx("Empty");
4641
+ const copy = useCopy();
4642
+ if (ctx.loading || ctx.usage !== null) return null;
4643
+ return /* @__PURE__ */ jsx17(
4644
+ "div",
4645
+ {
4646
+ ref: forwardedRef,
4647
+ "data-solvapay-usage-meter-empty": "",
4648
+ className: className ?? "solvapay-usage-meter-empty",
4649
+ ...rest,
4650
+ children: children ?? copy.usage.emptyLabel
4651
+ }
4652
+ );
4653
+ });
4654
+ var UsageMeter = Object.assign(Root7, {
4655
+ Root: Root7,
4656
+ Bar,
4657
+ Label,
4658
+ Percentage,
4659
+ ResetsIn,
4660
+ Loading: Loading4,
4661
+ Empty
4662
+ });
4663
+ function useUsageMeter() {
4664
+ return useUsageMeterCtx("useUsageMeter");
4665
+ }
4666
+
4667
+ // src/hooks/usePaywallResolver.ts
4668
+ import { useCallback as useCallback19, useMemo as useMemo16 } from "react";
4669
+ function usePaywallResolver(content) {
4670
+ const { activePurchase, hasPaidPurchase, refetch: refetchPurchase } = usePurchase();
4671
+ const { credits, refetch: refetchBalance } = useBalance();
4672
+ const resolved = useMemo16(() => {
4673
+ if (!content) return false;
4674
+ const productMatches = !content.product || activePurchase?.productRef === content.product || activePurchase?.productName === content.product;
4675
+ if (content.kind === "payment_required") {
4676
+ return Boolean(hasPaidPurchase && productMatches);
4677
+ }
4678
+ const balance = content.balance;
4679
+ if (balance && typeof balance.remainingUnits === "number" && balance.remainingUnits > 0) {
4680
+ return true;
4681
+ }
4682
+ if (balance && credits != null && balance.creditsPerUnit && credits >= balance.creditsPerUnit) {
4683
+ return true;
4684
+ }
4685
+ return Boolean(productMatches && activePurchase?.status === "active");
4686
+ }, [content, hasPaidPurchase, activePurchase, credits]);
4687
+ const refetch = useCallback19(async () => {
4688
+ await Promise.all([
4689
+ refetchPurchase().catch(() => void 0),
4690
+ refetchBalance().catch(() => void 0)
4691
+ ]);
4692
+ }, [refetchPurchase, refetchBalance]);
4693
+ return { resolved, refetch };
4694
+ }
4695
+
4696
+ export {
4697
+ filterPurchases,
4698
+ getActivePurchases,
4699
+ getCancelledPurchasesWithEndDate,
4700
+ getMostRecentPurchase,
4701
+ getPrimaryPurchase,
4702
+ isPaidPurchase,
4703
+ isPlanPurchase,
4704
+ isTopupPurchase,
4705
+ DEFAULT_ROUTES,
4706
+ createHttpTransport,
4707
+ enCopy,
4708
+ mergeCopy,
4709
+ CopyContext,
4710
+ CopyProvider,
4711
+ SolvaPayContext,
4712
+ SolvaPayProvider,
4713
+ setRef,
4714
+ composeRefs,
4715
+ Slot,
4716
+ Slottable,
4717
+ composeEventHandlers,
4718
+ MissingProviderError,
4719
+ MissingProductRefError,
4720
+ useSolvaPay,
4721
+ useCheckout,
4722
+ usePurchase,
4723
+ useCustomer,
4724
+ useCopy,
4725
+ useLocale,
4726
+ plansCache,
4727
+ usePlans,
4728
+ usePlan,
4729
+ createTransportCacheKey,
4730
+ productCache,
4731
+ useProduct,
4732
+ useActivation,
4733
+ usePlanSelection,
4734
+ PaymentFormContext,
4735
+ usePaymentForm,
4736
+ PaymentFormProvider,
4737
+ getMinorUnitsPerMajor,
4738
+ toMajorUnits,
4739
+ formatPrice,
4740
+ interpolate,
4741
+ CheckoutSummaryRoot2 as CheckoutSummaryRoot,
4742
+ CheckoutSummaryProduct2 as CheckoutSummaryProduct,
4743
+ CheckoutSummaryPlan2 as CheckoutSummaryPlan,
4744
+ CheckoutSummaryPrice2 as CheckoutSummaryPrice,
4745
+ CheckoutSummaryTrial2 as CheckoutSummaryTrial,
4746
+ CheckoutSummaryTaxNote2 as CheckoutSummaryTaxNote,
4747
+ CheckoutSummary,
4748
+ useCheckoutSummary,
4749
+ CheckoutSummary2,
4750
+ merchantCache,
4751
+ useMerchant,
4752
+ deriveVariant,
4753
+ MandateText,
4754
+ Spinner,
4755
+ confirmPayment,
4756
+ resolveCta,
4757
+ PaymentFormRoot2 as PaymentFormRoot,
4758
+ PaymentFormSummary,
4759
+ PaymentFormCustomerFields2 as PaymentFormCustomerFields,
4760
+ PaymentFormPaymentElement,
4761
+ PaymentFormCardElement,
4762
+ PaymentFormMandateText,
4763
+ PaymentFormTermsCheckbox2 as PaymentFormTermsCheckbox,
4764
+ PaymentFormSubmitButton2 as PaymentFormSubmitButton,
4765
+ PaymentFormLoading2 as PaymentFormLoading,
4766
+ PaymentFormError2 as PaymentFormError,
4767
+ PaymentForm,
4768
+ useTopupAmountSelector,
4769
+ useBalance,
4770
+ AmountPickerRoot2 as AmountPickerRoot,
4771
+ AmountPickerOption2 as AmountPickerOption,
4772
+ AmountPickerCustom2 as AmountPickerCustom,
4773
+ AmountPickerConfirm2 as AmountPickerConfirm,
4774
+ AmountPicker,
4775
+ useAmountPicker,
4776
+ useAmountPickerCopy,
4777
+ useTopup,
4778
+ TopupFormRoot2 as TopupFormRoot,
4779
+ TopupFormPaymentElement,
4780
+ TopupFormSubmitButton2 as TopupFormSubmitButton,
4781
+ TopupFormLoading2 as TopupFormLoading,
4782
+ TopupFormError2 as TopupFormError,
4783
+ TopupForm,
4784
+ useTopupForm,
4785
+ BalanceBadge,
4786
+ defaultListPlans,
4787
+ PlanSelectorRoot2 as PlanSelectorRoot,
4788
+ PlanSelectorHeading2 as PlanSelectorHeading,
4789
+ PlanSelectorGrid2 as PlanSelectorGrid,
4790
+ PlanSelectorCard2 as PlanSelectorCard,
4791
+ PlanSelectorCardName2 as PlanSelectorCardName,
4792
+ PlanSelectorCardPrice2 as PlanSelectorCardPrice,
4793
+ PlanSelectorCardInterval2 as PlanSelectorCardInterval,
4794
+ PlanSelectorCardBadge2 as PlanSelectorCardBadge,
4795
+ PlanSelectorLoading2 as PlanSelectorLoading,
4796
+ PlanSelectorError2 as PlanSelectorError,
4797
+ PlanSelector,
4798
+ usePlanSelector,
4799
+ usePurchaseActions,
4800
+ CancelPlanButton,
4801
+ usePurchaseStatus,
4802
+ CancelledPlanNoticeRoot2 as CancelledPlanNoticeRoot,
4803
+ CancelledPlanNoticeHeading2 as CancelledPlanNoticeHeading,
4804
+ CancelledPlanNoticeExpires2 as CancelledPlanNoticeExpires,
4805
+ CancelledPlanNoticeDaysRemaining2 as CancelledPlanNoticeDaysRemaining,
4806
+ CancelledPlanNoticeAccessUntil2 as CancelledPlanNoticeAccessUntil,
4807
+ CancelledPlanNoticeCancelledOn2 as CancelledPlanNoticeCancelledOn,
4808
+ CancelledPlanNoticeReason2 as CancelledPlanNoticeReason,
4809
+ CancelledPlanNoticeReactivateButton2 as CancelledPlanNoticeReactivateButton,
4810
+ CancelledPlanNotice,
4811
+ useCancelledPlanNotice,
4812
+ useTransport,
4813
+ useUsage,
4814
+ Root7 as Root,
4815
+ Bar,
4816
+ Label,
4817
+ Percentage,
4818
+ ResetsIn,
4819
+ Loading4 as Loading,
4820
+ Empty,
4821
+ UsageMeter,
4822
+ useUsageMeter,
4823
+ usePaywallResolver
4824
+ };