@solvapay/react 1.0.8-preview.9 → 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.
package/dist/index.cjs CHANGED
@@ -40,6 +40,9 @@ __export(index_exports, {
40
40
  CopyContext: () => CopyContext,
41
41
  CopyProvider: () => CopyProvider,
42
42
  CreditGate: () => CreditGate2,
43
+ CurrentPlanCard: () => CurrentPlanCard,
44
+ DEFAULT_ROUTES: () => DEFAULT_ROUTES,
45
+ LaunchCustomerPortalButton: () => LaunchCustomerPortalButton,
43
46
  MandateText: () => MandateText,
44
47
  PaymentForm: () => PaymentForm2,
45
48
  PaymentFormContext: () => PaymentFormContext,
@@ -52,7 +55,10 @@ __export(index_exports, {
52
55
  Spinner: () => Spinner,
53
56
  StripePaymentFormWrapper: () => StripePaymentFormWrapper,
54
57
  TopupForm: () => TopupForm2,
58
+ UnsupportedTransportMethodError: () => UnsupportedTransportMethodError,
59
+ UpdatePaymentMethodButton: () => UpdatePaymentMethodButton,
55
60
  confirmPayment: () => confirmPayment,
61
+ createHttpTransport: () => createHttpTransport,
56
62
  defaultAuthAdapter: () => defaultAuthAdapter,
57
63
  deriveVariant: () => deriveVariant,
58
64
  enCopy: () => enCopy,
@@ -60,12 +66,16 @@ __export(index_exports, {
60
66
  formatPrice: () => formatPrice,
61
67
  getActivePurchases: () => getActivePurchases,
62
68
  getCancelledPurchasesWithEndDate: () => getCancelledPurchasesWithEndDate,
69
+ getMinorUnitsPerMajor: () => getMinorUnitsPerMajor,
63
70
  getMostRecentPurchase: () => getMostRecentPurchase,
64
71
  getPrimaryPurchase: () => getPrimaryPurchase,
65
72
  interpolate: () => interpolate,
66
73
  isPaidPurchase: () => isPaidPurchase,
74
+ isPlanPurchase: () => isPlanPurchase,
75
+ isTopupPurchase: () => isTopupPurchase,
67
76
  mergeCopy: () => mergeCopy,
68
77
  resolveCta: () => resolveCta,
78
+ toMajorUnits: () => toMajorUnits,
69
79
  useActivation: () => useActivation,
70
80
  useBalance: () => useBalance,
71
81
  useCheckout: () => useCheckout,
@@ -74,6 +84,8 @@ __export(index_exports, {
74
84
  useLocale: () => useLocale,
75
85
  useMerchant: () => useMerchant,
76
86
  usePaymentForm: () => usePaymentForm,
87
+ usePaymentMethod: () => usePaymentMethod,
88
+ usePaywallResolver: () => usePaywallResolver,
77
89
  usePlan: () => usePlan,
78
90
  usePlans: () => usePlans,
79
91
  useProduct: () => useProduct,
@@ -82,7 +94,9 @@ __export(index_exports, {
82
94
  usePurchaseStatus: () => usePurchaseStatus,
83
95
  useSolvaPay: () => useSolvaPay,
84
96
  useTopup: () => useTopup,
85
- useTopupAmountSelector: () => useTopupAmountSelector
97
+ useTopupAmountSelector: () => useTopupAmountSelector,
98
+ useTransport: () => useTransport,
99
+ useUsage: () => useUsage
86
100
  });
87
101
  module.exports = __toCommonJS(index_exports);
88
102
 
@@ -118,6 +132,12 @@ function getPrimaryPurchase(purchases) {
118
132
  function isPaidPurchase(purchase) {
119
133
  return (purchase.amount ?? 0) > 0;
120
134
  }
135
+ function isPlanPurchase(purchase) {
136
+ return !!purchase.planSnapshot && purchase.metadata?.purpose !== "credit_topup";
137
+ }
138
+ function isTopupPurchase(purchase) {
139
+ return !isPlanPurchase(purchase);
140
+ }
121
141
 
122
142
  // src/adapters/auth.ts
123
143
  var defaultAuthAdapter = {
@@ -149,16 +169,6 @@ function getAuthAdapter(config) {
149
169
  if (config?.auth?.adapter) {
150
170
  return config.auth.adapter;
151
171
  }
152
- if (config?.auth?.getToken || config?.auth?.getUserId) {
153
- return {
154
- async getToken() {
155
- return config?.auth?.getToken?.() || null;
156
- },
157
- async getUserId() {
158
- return config?.auth?.getUserId?.() || null;
159
- }
160
- };
161
- }
162
172
  return defaultAuthAdapter;
163
173
  }
164
174
  function getCachedCustomerRef(userId) {
@@ -218,6 +228,167 @@ async function buildRequestHeaders(config) {
218
228
  return { headers, userId };
219
229
  }
220
230
 
231
+ // src/transport/http.ts
232
+ async function request(config, url, opts) {
233
+ const { headers } = await buildRequestHeaders(config);
234
+ const fetchFn = config?.fetch || fetch;
235
+ const init = { method: opts.method, headers };
236
+ if (opts.body !== void 0) {
237
+ init.body = JSON.stringify(opts.body);
238
+ }
239
+ const res = await fetchFn(url, init);
240
+ if (!res.ok) {
241
+ let serverMessage;
242
+ try {
243
+ const data = await res.clone().json();
244
+ serverMessage = data?.error;
245
+ } catch {
246
+ }
247
+ const error = new Error(serverMessage || `${opts.errorPrefix}: ${res.statusText || res.status}`);
248
+ config?.onError?.(error, opts.onErrorContext);
249
+ throw error;
250
+ }
251
+ return await res.json();
252
+ }
253
+ var DEFAULT_ROUTES = {
254
+ checkPurchase: "/api/check-purchase",
255
+ createPayment: "/api/create-payment-intent",
256
+ processPayment: "/api/process-payment",
257
+ createTopupPayment: "/api/create-topup-payment-intent",
258
+ customerBalance: "/api/customer-balance",
259
+ cancelRenewal: "/api/cancel-renewal",
260
+ reactivateRenewal: "/api/reactivate-renewal",
261
+ activatePlan: "/api/activate-plan",
262
+ createCheckoutSession: "/api/create-checkout-session",
263
+ createCustomerSession: "/api/create-customer-session",
264
+ getMerchant: "/api/merchant",
265
+ getProduct: "/api/get-product",
266
+ listPlans: "/api/list-plans",
267
+ getPaymentMethod: "/api/payment-method",
268
+ getUsage: "/api/usage"
269
+ };
270
+ function routeFor(config, key) {
271
+ const configured = config?.api?.[key];
272
+ return configured || DEFAULT_ROUTES[key];
273
+ }
274
+ function createHttpTransport(config) {
275
+ return {
276
+ checkPurchase: () => request(config, routeFor(config, "checkPurchase"), {
277
+ method: "GET",
278
+ onErrorContext: "checkPurchase",
279
+ errorPrefix: "Failed to check purchase"
280
+ }),
281
+ createPayment: (params) => {
282
+ const body = {};
283
+ if (params.planRef) body.planRef = params.planRef;
284
+ if (params.productRef) body.productRef = params.productRef;
285
+ if (params.customer && (params.customer.name || params.customer.email)) {
286
+ body.customer = params.customer;
287
+ }
288
+ return request(config, routeFor(config, "createPayment"), {
289
+ method: "POST",
290
+ body,
291
+ onErrorContext: "createPayment",
292
+ errorPrefix: "Failed to create payment"
293
+ });
294
+ },
295
+ processPayment: (params) => request(config, routeFor(config, "processPayment"), {
296
+ method: "POST",
297
+ body: params,
298
+ onErrorContext: "processPayment",
299
+ errorPrefix: "Failed to process payment"
300
+ }),
301
+ createTopupPayment: (params) => request(config, routeFor(config, "createTopupPayment"), {
302
+ method: "POST",
303
+ body: { amount: params.amount, currency: params.currency },
304
+ onErrorContext: "createTopupPayment",
305
+ errorPrefix: "Failed to create topup payment"
306
+ }),
307
+ getBalance: () => request(config, routeFor(config, "customerBalance"), {
308
+ method: "GET",
309
+ onErrorContext: "getBalance",
310
+ errorPrefix: "Failed to fetch balance"
311
+ }),
312
+ cancelRenewal: (params) => request(config, routeFor(config, "cancelRenewal"), {
313
+ method: "POST",
314
+ body: params,
315
+ onErrorContext: "cancelRenewal",
316
+ errorPrefix: "Failed to cancel renewal"
317
+ }),
318
+ reactivateRenewal: (params) => request(config, routeFor(config, "reactivateRenewal"), {
319
+ method: "POST",
320
+ body: params,
321
+ onErrorContext: "reactivateRenewal",
322
+ errorPrefix: "Failed to reactivate renewal"
323
+ }),
324
+ activatePlan: (params) => request(config, routeFor(config, "activatePlan"), {
325
+ method: "POST",
326
+ body: params,
327
+ onErrorContext: "activatePlan",
328
+ errorPrefix: "Failed to activate plan"
329
+ }),
330
+ createCheckoutSession: (params) => {
331
+ const body = {};
332
+ if (params?.planRef) body.planRef = params.planRef;
333
+ if (params?.productRef) body.productRef = params.productRef;
334
+ if (params?.returnUrl) body.returnUrl = params.returnUrl;
335
+ return request(
336
+ config,
337
+ routeFor(config, "createCheckoutSession"),
338
+ {
339
+ method: "POST",
340
+ body,
341
+ onErrorContext: "createCheckoutSession",
342
+ errorPrefix: "Failed to create checkout session"
343
+ }
344
+ );
345
+ },
346
+ createCustomerSession: () => request(
347
+ config,
348
+ routeFor(config, "createCustomerSession"),
349
+ {
350
+ method: "POST",
351
+ body: {},
352
+ onErrorContext: "createCustomerSession",
353
+ errorPrefix: "Failed to create customer session"
354
+ }
355
+ ),
356
+ getMerchant: () => request(config, routeFor(config, "getMerchant"), {
357
+ method: "GET",
358
+ onErrorContext: "getMerchant",
359
+ errorPrefix: "Failed to fetch merchant"
360
+ }),
361
+ getProduct: (productRef) => {
362
+ const base = routeFor(config, "getProduct");
363
+ const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
364
+ return request(config, url, {
365
+ method: "GET",
366
+ onErrorContext: "getProduct",
367
+ errorPrefix: "Failed to fetch product"
368
+ });
369
+ },
370
+ listPlans: (productRef) => {
371
+ const base = routeFor(config, "listPlans");
372
+ const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
373
+ return request(config, url, {
374
+ method: "GET",
375
+ onErrorContext: "listPlans",
376
+ errorPrefix: "Failed to list plans"
377
+ });
378
+ },
379
+ getPaymentMethod: () => request(config, routeFor(config, "getPaymentMethod"), {
380
+ method: "GET",
381
+ onErrorContext: "getPaymentMethod",
382
+ errorPrefix: "Failed to load payment method"
383
+ }),
384
+ getUsage: () => request(config, routeFor(config, "getUsage"), {
385
+ method: "GET",
386
+ onErrorContext: "getUsage",
387
+ errorPrefix: "Failed to load usage"
388
+ })
389
+ };
390
+ }
391
+
221
392
  // src/i18n/context.tsx
222
393
  var import_react = require("react");
223
394
 
@@ -367,6 +538,26 @@ var enCopy = {
367
538
  lowBalanceSubheading: "Top up to continue using {product}",
368
539
  topUpCta: "Top up now"
369
540
  },
541
+ currentPlan: {
542
+ heading: "Your plan",
543
+ nextBilling: "Next billing: {date}",
544
+ expiresOn: "Expires {date}",
545
+ validIndefinitely: "Valid indefinitely",
546
+ paymentMethod: "{brand} \u2022\u2022\u2022\u2022 {last4}",
547
+ paymentMethodExpires: "expires {month}/{year}",
548
+ noPaymentMethod: "No payment method on file",
549
+ updatePaymentButton: "Update card",
550
+ cycleUnit: {
551
+ weekly: "week",
552
+ monthly: "month",
553
+ quarterly: "3 months",
554
+ yearly: "year"
555
+ }
556
+ },
557
+ customerPortal: {
558
+ launchButton: "Manage billing",
559
+ loadingLabel: "Loading portal\u2026"
560
+ },
370
561
  errors: {
371
562
  paymentInitFailed: "Payment initialization failed",
372
563
  topupInitFailed: "Top-up initialization failed",
@@ -380,7 +571,40 @@ var enCopy = {
380
571
  paymentProcessingFailed: "Payment processing failed. Please try again or contact support.",
381
572
  paymentRequires3ds: "Payment requires additional authentication. Please complete the verification.",
382
573
  paymentProcessingTimeout: "Payment processing timed out \u2014 webhooks may not be configured",
383
- paymentStatusPrefix: "Payment status: {status}"
574
+ paymentConfirmationDelayed: "Payment succeeded but confirmation is taking longer than usual. Refresh in a moment to see your purchase.",
575
+ paymentStatusPrefix: "Payment status: {status}",
576
+ paywallInvalidContent: "Paywall content is missing or malformed.",
577
+ usageLoadFailed: "Failed to load usage"
578
+ },
579
+ paywall: {
580
+ header: "Unlock access",
581
+ paymentRequiredHeading: "Upgrade to continue",
582
+ activationRequiredHeading: "Add credits to continue",
583
+ resolvedHeading: "You can continue",
584
+ productContext: "For {product}",
585
+ balanceLine: "You have {available} credits, need {required}.",
586
+ paymentRequiredMessage: "You've used all your included calls{forProduct}. Choose a plan below to keep going.",
587
+ paymentRequiredMessageRemaining: "Only {remaining} call{pluralSuffix} left{forProduct}. Choose a plan below to keep going.",
588
+ paymentRequiredProductSuffix: " for {product}",
589
+ retryButton: "Continue",
590
+ hostedCheckoutButton: "Open checkout",
591
+ hostedCheckoutLoading: "Loading checkout\u2026"
592
+ },
593
+ usage: {
594
+ header: "Usage",
595
+ percentUsedLabel: "{percent}% used",
596
+ usedLabel: "{used} / {total} {unit}",
597
+ remainingLabel: "{remaining} {unit} remaining",
598
+ unlimitedLabel: "Unlimited",
599
+ resetsInLabel: "Resets in {days} days",
600
+ resetsOnLabel: "Resets {date}",
601
+ loadingLabel: "Loading usage\u2026",
602
+ emptyLabel: "No usage-based plan is active.",
603
+ approachingLimit: "You're approaching your usage limit.",
604
+ atLimit: "You've reached your usage limit.",
605
+ topUpCta: "Add credits",
606
+ upgradeCta: "Upgrade plan",
607
+ refreshCta: "Refresh"
384
608
  }
385
609
  };
386
610
 
@@ -425,146 +649,66 @@ function useCopyContext() {
425
649
  // src/SolvaPayProvider.tsx
426
650
  var import_jsx_runtime2 = require("react/jsx-runtime");
427
651
  var SolvaPayContext = (0, import_react2.createContext)(null);
428
- var SolvaPayProvider = ({
429
- config,
430
- createPayment: customCreatePayment,
431
- checkPurchase: customCheckPurchase,
432
- processPayment: customProcessPayment,
433
- createTopupPayment: customCreateTopupPayment,
434
- children
435
- }) => {
436
- const [purchaseData, setPurchaseData] = (0, import_react2.useState)({
437
- purchases: []
652
+ function resolveTransport(config) {
653
+ return config?.transport ?? createHttpTransport(config);
654
+ }
655
+ var SolvaPayProvider = ({ config, children }) => {
656
+ const initial = config?.initial;
657
+ const [purchaseData, setPurchaseData] = (0, import_react2.useState)(() => {
658
+ if (!initial) return { purchases: [] };
659
+ return {
660
+ // The server-side `PurchaseCheckResult.purchases` shape has looser
661
+ // optional fields than `PurchaseInfo`, but the runtime data is
662
+ // identical — the same rows flow through `transport.checkPurchase()`
663
+ // today. Cast once here to unify the two entry points, and run
664
+ // the same `filterPurchases` the HTTP path applies in
665
+ // `fetchPurchase` so the bootstrap-hydrated data never contains
666
+ // cancelled / expired / suspended rows the active-only derivations
667
+ // (`activePurchase`, `hasPaidPurchase`, …) wouldn't expect.
668
+ purchases: filterPurchases(
669
+ initial.purchase?.purchases ?? []
670
+ ),
671
+ customerRef: initial.customerRef ?? initial.purchase?.customerRef,
672
+ email: initial.purchase?.email,
673
+ name: initial.purchase?.name
674
+ };
438
675
  });
439
676
  const [loading, setLoading] = (0, import_react2.useState)(false);
440
677
  const [isRefetching, setIsRefetching] = (0, import_react2.useState)(false);
441
678
  const [purchaseError, setPurchaseError] = (0, import_react2.useState)(null);
442
- const [internalCustomerRef, setInternalCustomerRef] = (0, import_react2.useState)(void 0);
679
+ const [internalCustomerRef, setInternalCustomerRef] = (0, import_react2.useState)(
680
+ initial?.customerRef ?? void 0
681
+ );
443
682
  const [userId, setUserId] = (0, import_react2.useState)(null);
444
- const [isAuthenticated, setIsAuthenticated] = (0, import_react2.useState)(false);
445
- const [creditsValue, setCreditsValue] = (0, import_react2.useState)(null);
446
- const [displayCurrencyValue, setDisplayCurrencyValue] = (0, import_react2.useState)(null);
447
- const [creditsPerMinorUnitValue, setCreditsPerMinorUnitValue] = (0, import_react2.useState)(null);
448
- const [displayExchangeRateValue, setDisplayExchangeRateValue] = (0, import_react2.useState)(null);
683
+ const [isAuthenticated, setIsAuthenticated] = (0, import_react2.useState)(!!initial?.customerRef);
684
+ const [creditsValue, setCreditsValue] = (0, import_react2.useState)(
685
+ initial?.balance?.credits ?? null
686
+ );
687
+ const [displayCurrencyValue, setDisplayCurrencyValue] = (0, import_react2.useState)(
688
+ initial?.balance?.displayCurrency ?? null
689
+ );
690
+ const [creditsPerMinorUnitValue, setCreditsPerMinorUnitValue] = (0, import_react2.useState)(
691
+ initial?.balance?.creditsPerMinorUnit ?? null
692
+ );
693
+ const [displayExchangeRateValue, setDisplayExchangeRateValue] = (0, import_react2.useState)(
694
+ initial?.balance?.displayExchangeRate ?? null
695
+ );
449
696
  const [balanceLoading, setBalanceLoading] = (0, import_react2.useState)(false);
450
697
  const balanceInFlightRef = (0, import_react2.useRef)(false);
451
- const balanceLoadedRef = (0, import_react2.useRef)(false);
698
+ const balanceLoadedRef = (0, import_react2.useRef)(!!initial?.balance);
452
699
  const optimisticUntilRef = (0, import_react2.useRef)(0);
453
700
  const optimisticTimerRef = (0, import_react2.useRef)(null);
454
701
  const fetchBalanceRef = (0, import_react2.useRef)(null);
455
702
  const inFlightRef = (0, import_react2.useRef)(null);
456
- const checkPurchaseRef = (0, import_react2.useRef)(null);
457
- const createPaymentRef = (0, import_react2.useRef)(null);
458
- const processPaymentRef = (0, import_react2.useRef)(null);
459
- const createTopupPaymentRef = (0, import_react2.useRef)(null);
460
- const configRef = (0, import_react2.useRef)(config);
461
- const buildDefaultCheckPurchaseRef = (0, import_react2.useRef)(
462
- null
703
+ const loadedCacheKeysRef = (0, import_react2.useRef)(
704
+ new Set(initial?.customerRef ? [initial.customerRef] : [])
463
705
  );
706
+ const configRef = (0, import_react2.useRef)(config);
707
+ const transportRef = (0, import_react2.useRef)(resolveTransport(config));
464
708
  (0, import_react2.useEffect)(() => {
465
709
  configRef.current = config;
710
+ transportRef.current = resolveTransport(config);
466
711
  }, [config]);
467
- (0, import_react2.useEffect)(() => {
468
- checkPurchaseRef.current = customCheckPurchase || null;
469
- }, [customCheckPurchase]);
470
- (0, import_react2.useEffect)(() => {
471
- createPaymentRef.current = customCreatePayment || null;
472
- }, [customCreatePayment]);
473
- (0, import_react2.useEffect)(() => {
474
- processPaymentRef.current = customProcessPayment || null;
475
- }, [customProcessPayment]);
476
- (0, import_react2.useEffect)(() => {
477
- createTopupPaymentRef.current = customCreateTopupPayment || null;
478
- }, [customCreateTopupPayment]);
479
- const buildDefaultCheckPurchase = (0, import_react2.useCallback)(async () => {
480
- const currentConfig = configRef.current;
481
- const { headers } = await buildRequestHeaders(currentConfig);
482
- const route = currentConfig?.api?.checkPurchase || "/api/check-purchase";
483
- const fetchFn = currentConfig?.fetch || fetch;
484
- const res = await fetchFn(route, { method: "GET", headers });
485
- if (!res.ok) {
486
- const error = new Error(`Failed to check purchase: ${res.statusText}`);
487
- currentConfig?.onError?.(error, "checkPurchase");
488
- throw error;
489
- }
490
- return res.json();
491
- }, []);
492
- (0, import_react2.useEffect)(() => {
493
- buildDefaultCheckPurchaseRef.current = buildDefaultCheckPurchase;
494
- }, [buildDefaultCheckPurchase]);
495
- const buildDefaultCreatePayment = (0, import_react2.useCallback)(
496
- async (params) => {
497
- const currentConfig = configRef.current;
498
- const { headers } = await buildRequestHeaders(currentConfig);
499
- const route = currentConfig?.api?.createPayment || "/api/create-payment-intent";
500
- const fetchFn = currentConfig?.fetch || fetch;
501
- const body = {};
502
- if (params.planRef) {
503
- body.planRef = params.planRef;
504
- }
505
- if (params.productRef) {
506
- body.productRef = params.productRef;
507
- }
508
- if (params.customer && (params.customer.name || params.customer.email)) {
509
- body.customer = params.customer;
510
- }
511
- const res = await fetchFn(route, {
512
- method: "POST",
513
- headers,
514
- body: JSON.stringify(body)
515
- });
516
- if (!res.ok) {
517
- const error = new Error(`Failed to create payment: ${res.statusText}`);
518
- currentConfig?.onError?.(error, "createPayment");
519
- throw error;
520
- }
521
- return res.json();
522
- },
523
- []
524
- );
525
- const buildDefaultProcessPayment = (0, import_react2.useCallback)(
526
- async (params) => {
527
- const currentConfig = configRef.current;
528
- const { headers } = await buildRequestHeaders(currentConfig);
529
- const route = currentConfig?.api?.processPayment || "/api/process-payment";
530
- const fetchFn = currentConfig?.fetch || fetch;
531
- const res = await fetchFn(route, {
532
- method: "POST",
533
- headers,
534
- body: JSON.stringify(params)
535
- });
536
- if (!res.ok) {
537
- const error = new Error(`Failed to process payment: ${res.statusText}`);
538
- currentConfig?.onError?.(error, "processPayment");
539
- throw error;
540
- }
541
- return res.json();
542
- },
543
- []
544
- );
545
- const buildDefaultCreateTopupPayment = (0, import_react2.useCallback)(
546
- async (params) => {
547
- const currentConfig = configRef.current;
548
- const { headers } = await buildRequestHeaders(currentConfig);
549
- const route = currentConfig?.api?.createTopupPayment || "/api/create-topup-payment-intent";
550
- const fetchFn = currentConfig?.fetch || fetch;
551
- const res = await fetchFn(route, {
552
- method: "POST",
553
- headers,
554
- body: JSON.stringify({
555
- amount: params.amount,
556
- currency: params.currency
557
- })
558
- });
559
- if (!res.ok) {
560
- const error = new Error(`Failed to create topup payment: ${res.statusText}`);
561
- currentConfig?.onError?.(error, "createTopupPayment");
562
- throw error;
563
- }
564
- return res.json();
565
- },
566
- []
567
- );
568
712
  const fetchBalanceImpl = (0, import_react2.useCallback)(async () => {
569
713
  if (optimisticUntilRef.current > Date.now()) return;
570
714
  if (!isAuthenticated && !internalCustomerRef) {
@@ -582,16 +726,12 @@ var SolvaPayProvider = ({
582
726
  setBalanceLoading(true);
583
727
  }
584
728
  try {
585
- const currentConfig = configRef.current;
586
- const { headers } = await buildRequestHeaders(currentConfig);
587
- const route = currentConfig?.api?.customerBalance || "/api/customer-balance";
588
- const fetchFn = currentConfig?.fetch || fetch;
589
- const res = await fetchFn(route, { method: "GET", headers });
590
- if (!res.ok) {
591
- console.error("[SolvaPayProvider] Failed to fetch balance:", res.statusText);
729
+ if (!transportRef.current.getBalance) {
730
+ setBalanceLoading(false);
731
+ balanceInFlightRef.current = false;
592
732
  return;
593
733
  }
594
- const data = await res.json();
734
+ const data = await transportRef.current.getBalance();
595
735
  setCreditsValue(data.credits ?? null);
596
736
  setDisplayCurrencyValue(data.displayCurrency ?? null);
597
737
  setCreditsPerMinorUnitValue(data.creditsPerMinorUnit ?? null);
@@ -622,54 +762,35 @@ var SolvaPayProvider = ({
622
762
  fetchBalanceRef.current?.();
623
763
  }, OPTIMISTIC_GRACE_MS);
624
764
  }, []);
625
- const _checkPurchase = (0, import_react2.useCallback)(async () => {
626
- if (checkPurchaseRef.current) {
627
- return checkPurchaseRef.current();
628
- }
629
- if (buildDefaultCheckPurchaseRef.current) {
630
- return buildDefaultCheckPurchaseRef.current();
631
- }
632
- return buildDefaultCheckPurchase();
633
- }, [buildDefaultCheckPurchase]);
634
765
  const createPayment = (0, import_react2.useCallback)(
635
- async (params) => {
636
- if (createPaymentRef.current) {
637
- return createPaymentRef.current(params);
638
- }
639
- return buildDefaultCreatePayment(params);
640
- },
641
- [buildDefaultCreatePayment]
766
+ (params) => transportRef.current.createPayment(params),
767
+ []
642
768
  );
643
769
  const processPayment = (0, import_react2.useCallback)(
644
- async (params) => {
645
- if (processPaymentRef.current) {
646
- return processPaymentRef.current(params);
647
- }
648
- return buildDefaultProcessPayment(params);
649
- },
650
- [buildDefaultProcessPayment]
770
+ (params) => transportRef.current.processPayment(params),
771
+ []
651
772
  );
652
773
  const createTopupPayment = (0, import_react2.useCallback)(
653
- async (params) => {
654
- if (createTopupPaymentRef.current) {
655
- return createTopupPaymentRef.current(params);
656
- }
657
- return buildDefaultCreateTopupPayment(params);
658
- },
659
- [buildDefaultCreateTopupPayment]
774
+ (params) => transportRef.current.createTopupPayment(params),
775
+ []
660
776
  );
661
777
  (0, import_react2.useEffect)(() => {
778
+ if (configRef.current?.initial) {
779
+ setIsAuthenticated(configRef.current.initial.customerRef !== null);
780
+ return;
781
+ }
662
782
  const detectAuth = async () => {
663
783
  const currentConfig = configRef.current;
664
- const adapter = getAuthAdapter(currentConfig);
665
- const token = await adapter.getToken();
666
- const detectedUserId = await adapter.getUserId();
784
+ const adapter2 = getAuthAdapter(currentConfig);
785
+ const token = await adapter2.getToken();
786
+ const detectedUserId = await adapter2.getUserId();
667
787
  const prevUserId = userId;
668
788
  setIsAuthenticated(!!token);
669
789
  setUserId(detectedUserId);
670
790
  if (prevUserId !== null && detectedUserId !== prevUserId) {
671
791
  clearCachedCustomerRef();
672
792
  setInternalCustomerRef(void 0);
793
+ loadedCacheKeysRef.current.clear();
673
794
  return;
674
795
  }
675
796
  const cachedRef = getCachedCustomerRef(detectedUserId);
@@ -678,11 +799,19 @@ var SolvaPayProvider = ({
678
799
  } else if (!token) {
679
800
  clearCachedCustomerRef();
680
801
  setInternalCustomerRef(void 0);
802
+ loadedCacheKeysRef.current.clear();
681
803
  } else if (token && !cachedRef) {
682
804
  setInternalCustomerRef(void 0);
683
805
  }
684
806
  };
685
807
  detectAuth();
808
+ const adapter = getAuthAdapter(configRef.current);
809
+ if (typeof adapter.subscribe === "function") {
810
+ const unsubscribe = adapter.subscribe(() => {
811
+ detectAuth();
812
+ });
813
+ return unsubscribe;
814
+ }
686
815
  const interval = setInterval(detectAuth, 3e4);
687
816
  return () => clearInterval(interval);
688
817
  }, [userId]);
@@ -701,20 +830,24 @@ var SolvaPayProvider = ({
701
830
  return;
702
831
  }
703
832
  inFlightRef.current = cacheKey;
704
- const hasExistingData = purchaseData.purchases.length > 0;
705
- if (hasExistingData) {
833
+ const hasLoadedOnce = loadedCacheKeysRef.current.has(cacheKey);
834
+ if (hasLoadedOnce) {
706
835
  setIsRefetching(true);
707
836
  } else {
708
837
  setLoading(true);
709
838
  }
710
839
  try {
711
- const checkFn = checkPurchaseRef.current || buildDefaultCheckPurchaseRef.current;
712
- if (!checkFn) {
713
- throw new Error("checkPurchase function not available");
840
+ if (!transportRef.current.checkPurchase) {
841
+ setLoading(false);
842
+ setIsRefetching(false);
843
+ loadedCacheKeysRef.current.add(cacheKey);
844
+ inFlightRef.current = null;
845
+ return;
714
846
  }
715
- const data = await checkFn();
847
+ const data = await transportRef.current.checkPurchase();
716
848
  if (data.customerRef) {
717
849
  setInternalCustomerRef(data.customerRef);
850
+ loadedCacheKeysRef.current.add(data.customerRef);
718
851
  const currentAdapter = getAuthAdapter(configRef.current);
719
852
  const currentUserId = await currentAdapter.getUserId();
720
853
  setCachedCustomerRef(data.customerRef, currentUserId);
@@ -734,40 +867,82 @@ var SolvaPayProvider = ({
734
867
  }
735
868
  } finally {
736
869
  if (inFlightRef.current === cacheKey) {
870
+ loadedCacheKeysRef.current.add(cacheKey);
737
871
  setLoading(false);
738
872
  setIsRefetching(false);
739
873
  inFlightRef.current = null;
740
874
  }
741
875
  }
742
876
  },
743
- [isAuthenticated, internalCustomerRef, userId, purchaseData.purchases.length]
877
+ [isAuthenticated, internalCustomerRef, userId]
744
878
  );
745
879
  const fetchPurchaseRef = (0, import_react2.useRef)(fetchPurchase);
746
880
  (0, import_react2.useEffect)(() => {
747
881
  fetchPurchaseRef.current = fetchPurchase;
748
882
  }, [fetchPurchase]);
883
+ const applyInitialRef = (0, import_react2.useRef)(null);
749
884
  const refetchPurchase = (0, import_react2.useCallback)(async () => {
750
885
  inFlightRef.current = null;
886
+ const refresh = configRef.current?.refreshInitial;
887
+ const hasCheckPurchase = !!transportRef.current.checkPurchase;
888
+ if (refresh && !hasCheckPurchase) {
889
+ setIsRefetching(true);
890
+ try {
891
+ const next = await refresh();
892
+ if (next) applyInitialRef.current?.(next);
893
+ } catch (err) {
894
+ console.error("[SolvaPayProvider] refetchPurchase (MCP) failed:", err);
895
+ } finally {
896
+ setIsRefetching(false);
897
+ }
898
+ return;
899
+ }
751
900
  await fetchPurchaseRef.current(true);
752
901
  }, []);
902
+ const applyInitial = (0, import_react2.useCallback)((next) => {
903
+ setPurchaseData({
904
+ // Mirror the HTTP path: strip non-active rows before any
905
+ // derivation (`activePurchase` etc.) reads the array.
906
+ purchases: filterPurchases(
907
+ next.purchase?.purchases ?? []
908
+ ),
909
+ customerRef: next.customerRef ?? next.purchase?.customerRef,
910
+ email: next.purchase?.email,
911
+ name: next.purchase?.name
912
+ });
913
+ if (next.customerRef) {
914
+ setInternalCustomerRef(next.customerRef);
915
+ loadedCacheKeysRef.current.add(next.customerRef);
916
+ } else {
917
+ setInternalCustomerRef(void 0);
918
+ loadedCacheKeysRef.current.clear();
919
+ }
920
+ setIsAuthenticated(next.customerRef !== null);
921
+ setCreditsValue(next.balance?.credits ?? null);
922
+ setDisplayCurrencyValue(next.balance?.displayCurrency ?? null);
923
+ setCreditsPerMinorUnitValue(next.balance?.creditsPerMinorUnit ?? null);
924
+ setDisplayExchangeRateValue(next.balance?.displayExchangeRate ?? null);
925
+ balanceLoadedRef.current = !!next.balance;
926
+ }, []);
927
+ (0, import_react2.useEffect)(() => {
928
+ applyInitialRef.current = applyInitial;
929
+ }, [applyInitial]);
930
+ const refreshBootstrap = (0, import_react2.useCallback)(async () => {
931
+ const refresh = configRef.current?.refreshInitial;
932
+ if (refresh) {
933
+ try {
934
+ const next = await refresh();
935
+ if (next) applyInitial(next);
936
+ } catch (err) {
937
+ console.error("[SolvaPayProvider] refreshBootstrap failed:", err);
938
+ }
939
+ return;
940
+ }
941
+ await Promise.all([refetchPurchase(), fetchBalanceRef.current?.() ?? Promise.resolve()]);
942
+ }, [refetchPurchase, applyInitial]);
753
943
  const cancelRenewal = (0, import_react2.useCallback)(
754
944
  async (params) => {
755
- const currentConfig = configRef.current;
756
- const { headers } = await buildRequestHeaders(currentConfig);
757
- const route = currentConfig?.api?.cancelRenewal || "/api/cancel-renewal";
758
- const fetchFn = currentConfig?.fetch || fetch;
759
- const res = await fetchFn(route, {
760
- method: "POST",
761
- headers,
762
- body: JSON.stringify(params)
763
- });
764
- if (!res.ok) {
765
- const data = await res.json().catch(() => ({}));
766
- const error = new Error(data.error || `Failed to cancel renewal: ${res.statusText}`);
767
- currentConfig?.onError?.(error, "cancelRenewal");
768
- throw error;
769
- }
770
- const result = await res.json();
945
+ const result = await transportRef.current.cancelRenewal(params);
771
946
  await refetchPurchase();
772
947
  return result;
773
948
  },
@@ -775,22 +950,7 @@ var SolvaPayProvider = ({
775
950
  );
776
951
  const reactivateRenewal = (0, import_react2.useCallback)(
777
952
  async (params) => {
778
- const currentConfig = configRef.current;
779
- const { headers } = await buildRequestHeaders(currentConfig);
780
- const route = currentConfig?.api?.reactivateRenewal || "/api/reactivate-renewal";
781
- const fetchFn = currentConfig?.fetch || fetch;
782
- const res = await fetchFn(route, {
783
- method: "POST",
784
- headers,
785
- body: JSON.stringify(params)
786
- });
787
- if (!res.ok) {
788
- const data = await res.json().catch(() => ({}));
789
- const error = new Error(data.error || `Failed to reactivate renewal: ${res.statusText}`);
790
- currentConfig?.onError?.(error, "reactivateRenewal");
791
- throw error;
792
- }
793
- const result = await res.json();
953
+ const result = await transportRef.current.reactivateRenewal(params);
794
954
  await refetchPurchase();
795
955
  return result;
796
956
  },
@@ -798,22 +958,7 @@ var SolvaPayProvider = ({
798
958
  );
799
959
  const activatePlan = (0, import_react2.useCallback)(
800
960
  async (params) => {
801
- const currentConfig = configRef.current;
802
- const { headers } = await buildRequestHeaders(currentConfig);
803
- const route = currentConfig?.api?.activatePlan || "/api/activate-plan";
804
- const fetchFn = currentConfig?.fetch || fetch;
805
- const res = await fetchFn(route, {
806
- method: "POST",
807
- headers,
808
- body: JSON.stringify(params)
809
- });
810
- if (!res.ok) {
811
- const data = await res.json().catch(() => ({}));
812
- const error = new Error(data.error || `Failed to activate plan: ${res.statusText}`);
813
- currentConfig?.onError?.(error, "activatePlan");
814
- throw error;
815
- }
816
- const result = await res.json();
961
+ const result = await transportRef.current.activatePlan(params);
817
962
  if (result.status === "activated" || result.status === "already_active") {
818
963
  await refetchPurchase();
819
964
  }
@@ -821,8 +966,13 @@ var SolvaPayProvider = ({
821
966
  },
822
967
  [refetchPurchase]
823
968
  );
969
+ const hydratedFromInitialRef = (0, import_react2.useRef)(!!initial);
824
970
  (0, import_react2.useEffect)(() => {
825
971
  inFlightRef.current = null;
972
+ if (hydratedFromInitialRef.current) {
973
+ hydratedFromInitialRef.current = false;
974
+ return;
975
+ }
826
976
  if (isAuthenticated || internalCustomerRef) {
827
977
  fetchPurchase();
828
978
  } else {
@@ -841,8 +991,10 @@ var SolvaPayProvider = ({
841
991
  [fetchPurchase, userId]
842
992
  );
843
993
  const purchase = (0, import_react2.useMemo)(() => {
844
- const activePurchase = getPrimaryPurchase(purchaseData.purchases);
845
- const activePaidPurchases = purchaseData.purchases.filter(
994
+ const planPurchases = purchaseData.purchases.filter(isPlanPurchase);
995
+ const balanceTransactions = purchaseData.purchases.filter((p) => !isPlanPurchase(p));
996
+ const activePurchase = getPrimaryPurchase(planPurchases);
997
+ const activePaidPurchases = planPurchases.filter(
846
998
  (p) => p.status === "active" && isPaidPurchase(p)
847
999
  );
848
1000
  const activePaidPurchase = activePaidPurchases.sort(
@@ -857,18 +1009,14 @@ var SolvaPayProvider = ({
857
1009
  name: purchaseData.name,
858
1010
  purchases: purchaseData.purchases,
859
1011
  hasProduct: (productName) => {
860
- return purchaseData.purchases.some(
861
- (p) => p.productName.toLowerCase() === productName.toLowerCase() && p.status === "active"
862
- );
863
- },
864
- hasPlan: (productName) => {
865
- return purchaseData.purchases.some(
1012
+ return planPurchases.some(
866
1013
  (p) => p.productName.toLowerCase() === productName.toLowerCase() && p.status === "active"
867
1014
  );
868
1015
  },
869
1016
  activePurchase,
870
1017
  hasPaidPurchase: activePaidPurchases.length > 0,
871
- activePaidPurchase
1018
+ activePaidPurchase,
1019
+ balanceTransactions
872
1020
  };
873
1021
  }, [loading, isRefetching, purchaseError, purchaseData, internalCustomerRef]);
874
1022
  const balance = (0, import_react2.useMemo)(
@@ -881,7 +1029,15 @@ var SolvaPayProvider = ({
881
1029
  refetch: fetchBalanceImpl,
882
1030
  adjustBalance: adjustBalanceImpl
883
1031
  }),
884
- [balanceLoading, creditsValue, displayCurrencyValue, creditsPerMinorUnitValue, displayExchangeRateValue, fetchBalanceImpl, adjustBalanceImpl]
1032
+ [
1033
+ balanceLoading,
1034
+ creditsValue,
1035
+ displayCurrencyValue,
1036
+ creditsPerMinorUnitValue,
1037
+ displayExchangeRateValue,
1038
+ fetchBalanceImpl,
1039
+ adjustBalanceImpl
1040
+ ]
885
1041
  );
886
1042
  const contextValue = (0, import_react2.useMemo)(
887
1043
  () => ({
@@ -896,6 +1052,7 @@ var SolvaPayProvider = ({
896
1052
  customerRef: purchaseData.customerRef || internalCustomerRef,
897
1053
  updateCustomerRef,
898
1054
  balance,
1055
+ refreshBootstrap,
899
1056
  _config: configRef.current
900
1057
  }),
901
1058
  [
@@ -910,7 +1067,8 @@ var SolvaPayProvider = ({
910
1067
  purchaseData.customerRef,
911
1068
  internalCustomerRef,
912
1069
  updateCustomerRef,
913
- balance
1070
+ balance,
1071
+ refreshBootstrap
914
1072
  ]
915
1073
  );
916
1074
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SolvaPayContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CopyProvider, { locale: config?.locale, copy: config?.copy, children }) });
@@ -1090,8 +1248,11 @@ var stripePromiseCache = /* @__PURE__ */ new Map();
1090
1248
  function getStripeCacheKey(publishableKey, accountId) {
1091
1249
  return accountId ? `${publishableKey}:${accountId}` : publishableKey;
1092
1250
  }
1093
- async function resolvePlanRef(productRef, fetchFn, headers, listPlansRoute) {
1251
+ async function resolvePlanRef(productRef, config) {
1252
+ const listPlansRoute = config?.api?.listPlans || "/api/list-plans";
1094
1253
  const url = `${listPlansRoute}?productRef=${encodeURIComponent(productRef)}`;
1254
+ const fetchFn = config?.fetch || fetch;
1255
+ const { headers } = await buildRequestHeaders(config);
1095
1256
  const res = await fetchFn(url, { method: "GET", headers });
1096
1257
  if (!res.ok) {
1097
1258
  throw new Error(`Failed to fetch plans for product "${productRef}": ${res.statusText}`);
@@ -1142,8 +1303,7 @@ function useCheckout(options) {
1142
1303
  try {
1143
1304
  let effectivePlanRef = planRef;
1144
1305
  if (!effectivePlanRef && productRef) {
1145
- const listPlansRoute = _config?.api?.listPlans || "/api/list-plans";
1146
- effectivePlanRef = await resolvePlanRef(productRef, fetch, {}, listPlansRoute);
1306
+ effectivePlanRef = await resolvePlanRef(productRef, _config);
1147
1307
  setResolvedPlanRef(effectivePlanRef);
1148
1308
  }
1149
1309
  if (!effectivePlanRef) {
@@ -1534,27 +1694,41 @@ function usePlan(options) {
1534
1694
 
1535
1695
  // src/hooks/useProduct.ts
1536
1696
  var import_react8 = require("react");
1697
+
1698
+ // src/transport/cache-key.ts
1699
+ var transportIds = /* @__PURE__ */ new WeakMap();
1700
+ var nextTransportId = 0;
1701
+ function transportIdFor(transport) {
1702
+ let id = transportIds.get(transport);
1703
+ if (id === void 0) {
1704
+ id = ++nextTransportId;
1705
+ transportIds.set(transport, id);
1706
+ }
1707
+ return id;
1708
+ }
1709
+ function createTransportCacheKey(config, fallbackRoute, suffix) {
1710
+ const transport = config?.transport;
1711
+ if (transport) {
1712
+ const id = transportIdFor(transport);
1713
+ return suffix ? `transport:${id}:${suffix}` : `transport:${id}`;
1714
+ }
1715
+ return fallbackRoute;
1716
+ }
1717
+
1718
+ // src/hooks/useProduct.ts
1537
1719
  var productCache = /* @__PURE__ */ new Map();
1538
1720
  var CACHE_DURATION3 = 5 * 60 * 1e3;
1539
- function routeFor(config) {
1540
- return config?.api?.getProduct || "/api/get-product";
1721
+ function cacheKeyFor(config, productRef) {
1722
+ return createTransportCacheKey(config, productRef, productRef);
1541
1723
  }
1542
1724
  async function fetchProduct(productRef, config) {
1543
- const base = routeFor(config);
1544
- const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
1545
- const fetchFn = config?.fetch || fetch;
1546
- const { headers } = await buildRequestHeaders(config);
1547
- const res = await fetchFn(url, { method: "GET", headers });
1548
- if (!res.ok) {
1549
- const error = new Error(`Failed to fetch product: ${res.statusText || res.status}`);
1550
- config?.onError?.(error, "getProduct");
1551
- throw error;
1552
- }
1553
- return await res.json();
1725
+ const transport = config?.transport ?? createHttpTransport(config);
1726
+ if (!transport.getProduct) return null;
1727
+ return transport.getProduct(productRef);
1554
1728
  }
1555
1729
  function useProduct(productRef) {
1556
1730
  const { _config } = useSolvaPay();
1557
- const cacheKey = productRef || "";
1731
+ const cacheKey = productRef ? cacheKeyFor(_config, productRef) : "";
1558
1732
  const [product, setProduct] = (0, import_react8.useState)(
1559
1733
  () => productRef ? productCache.get(cacheKey)?.product ?? null : null
1560
1734
  );
@@ -1572,7 +1746,8 @@ function useProduct(productRef) {
1572
1746
  setError(null);
1573
1747
  return;
1574
1748
  }
1575
- const cached = productCache.get(productRef);
1749
+ const key = cacheKeyFor(_config, productRef);
1750
+ const cached = productCache.get(key);
1576
1751
  const now = Date.now();
1577
1752
  if (!force && cached?.product && now - cached.timestamp < CACHE_DURATION3) {
1578
1753
  setProduct(cached.product);
@@ -1597,12 +1772,26 @@ function useProduct(productRef) {
1597
1772
  setLoading(true);
1598
1773
  setError(null);
1599
1774
  const promise = fetchProduct(productRef, _config);
1600
- productCache.set(productRef, { product: null, promise, timestamp: now });
1775
+ productCache.set(key, {
1776
+ product: cached?.product ?? null,
1777
+ promise,
1778
+ timestamp: now
1779
+ });
1601
1780
  const p = await promise;
1602
- productCache.set(productRef, { product: p, promise: null, timestamp: now });
1781
+ if (p === null) {
1782
+ productCache.set(key, {
1783
+ product: cached?.product ?? null,
1784
+ promise: null,
1785
+ timestamp: cached?.timestamp ?? now
1786
+ });
1787
+ setProduct(cached?.product ?? null);
1788
+ setLoading(false);
1789
+ return;
1790
+ }
1791
+ productCache.set(key, { product: p, promise: null, timestamp: now });
1603
1792
  setProduct(p);
1604
1793
  } catch (err) {
1605
- productCache.delete(productRef);
1794
+ productCache.delete(key);
1606
1795
  setError(err instanceof Error ? err : new Error("Failed to load product"));
1607
1796
  } finally {
1608
1797
  setLoading(false);
@@ -1724,6 +1913,9 @@ var ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
1724
1913
  function getFractionDigits(currency) {
1725
1914
  return ZERO_DECIMAL_CURRENCIES.has(currency.toLowerCase()) ? 0 : 2;
1726
1915
  }
1916
+ function getMinorUnitsPerMajor(currency) {
1917
+ return getFractionDigits(currency) === 0 ? 1 : 100;
1918
+ }
1727
1919
  function toMajorUnits(amountMinor, currency) {
1728
1920
  const fractionDigits = getFractionDigits(currency);
1729
1921
  return fractionDigits === 0 ? amountMinor : amountMinor / 100;
@@ -1733,7 +1925,10 @@ function formatPrice(amountMinor, currency, opts = {}) {
1733
1925
  if (amountMinor === 0 && free !== "") {
1734
1926
  return free;
1735
1927
  }
1736
- const fractionDigits = getFractionDigits(currency);
1928
+ const naturalFractionDigits = getFractionDigits(currency);
1929
+ const minorPerMajor = getMinorUnitsPerMajor(currency);
1930
+ const isWhole = amountMinor % minorPerMajor === 0;
1931
+ const fractionDigits = isWhole ? 0 : naturalFractionDigits;
1737
1932
  const major = toMajorUnits(amountMinor, currency);
1738
1933
  const formatter = new Intl.NumberFormat(locale, {
1739
1934
  style: "currency",
@@ -1871,24 +2066,17 @@ var import_react14 = require("react");
1871
2066
  var import_react13 = require("react");
1872
2067
  var merchantCache = /* @__PURE__ */ new Map();
1873
2068
  var CACHE_DURATION4 = 5 * 60 * 1e3;
1874
- function cacheKeyFor(config) {
1875
- return config?.api?.getMerchant || "/api/merchant";
2069
+ function cacheKeyFor2(config) {
2070
+ return createTransportCacheKey(config, config?.api?.getMerchant || "/api/merchant");
1876
2071
  }
1877
2072
  async function fetchMerchant(config) {
1878
- const route = cacheKeyFor(config);
1879
- const fetchFn = config?.fetch || fetch;
1880
- const { headers } = await buildRequestHeaders(config);
1881
- const res = await fetchFn(route, { method: "GET", headers });
1882
- if (!res.ok) {
1883
- const error = new Error(`Failed to fetch merchant: ${res.statusText || res.status}`);
1884
- config?.onError?.(error, "getMerchant");
1885
- throw error;
1886
- }
1887
- return await res.json();
2073
+ const transport = config?.transport ?? createHttpTransport(config);
2074
+ if (!transport.getMerchant) return null;
2075
+ return transport.getMerchant();
1888
2076
  }
1889
2077
  function useMerchant() {
1890
2078
  const { _config } = useSolvaPay();
1891
- const key = cacheKeyFor(_config);
2079
+ const key = cacheKeyFor2(_config);
1892
2080
  const [merchant, setMerchant] = (0, import_react13.useState)(
1893
2081
  () => merchantCache.get(key)?.merchant ?? null
1894
2082
  );
@@ -1924,8 +2112,22 @@ function useMerchant() {
1924
2112
  setLoading(true);
1925
2113
  setError(null);
1926
2114
  const promise = fetchMerchant(_config);
1927
- merchantCache.set(key, { merchant: null, promise, timestamp: now });
2115
+ merchantCache.set(key, {
2116
+ merchant: cached?.merchant ?? null,
2117
+ promise,
2118
+ timestamp: now
2119
+ });
1928
2120
  const m = await promise;
2121
+ if (m === null) {
2122
+ merchantCache.set(key, {
2123
+ merchant: cached?.merchant ?? null,
2124
+ promise: null,
2125
+ timestamp: cached?.timestamp ?? now
2126
+ });
2127
+ setMerchant(cached?.merchant ?? null);
2128
+ setLoading(false);
2129
+ return;
2130
+ }
1929
2131
  merchantCache.set(key, { merchant: m, promise: null, timestamp: now });
1930
2132
  setMerchant(m);
1931
2133
  } catch (err) {
@@ -2038,31 +2240,36 @@ var MandateText = (0, import_react14.forwardRef)(
2038
2240
 
2039
2241
  // src/components/Spinner.tsx
2040
2242
  var import_jsx_runtime9 = require("react/jsx-runtime");
2041
- var Spinner = ({
2042
- className = "",
2043
- size = "md"
2044
- }) => {
2045
- const sizeClasses = {
2046
- sm: "h-4 w-4",
2047
- md: "h-5 w-5",
2048
- lg: "h-6 w-6"
2049
- };
2243
+ var SIZE_PX = { sm: 16, md: 20, lg: 24 };
2244
+ var Spinner = ({ className, size = "md" }) => {
2245
+ const px = SIZE_PX[size];
2050
2246
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2051
2247
  "svg",
2052
2248
  {
2053
- className: `animate-spin ${sizeClasses[size]} ${className}`,
2054
- xmlns: "http://www.w3.org/2000/svg",
2055
- fill: "none",
2249
+ "data-solvapay-spinner": "",
2250
+ className,
2251
+ width: px,
2252
+ height: px,
2056
2253
  viewBox: "0 0 24 24",
2057
- role: "status",
2058
- "aria-busy": "true",
2254
+ fill: "none",
2255
+ "aria-hidden": "true",
2059
2256
  children: [
2060
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
2257
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2258
+ "circle",
2259
+ {
2260
+ cx: "12",
2261
+ cy: "12",
2262
+ r: "10",
2263
+ stroke: "currentColor",
2264
+ strokeWidth: "4",
2265
+ strokeOpacity: "0.25"
2266
+ }
2267
+ ),
2061
2268
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2062
2269
  "path",
2063
2270
  {
2064
- className: "opacity-75",
2065
2271
  fill: "currentColor",
2272
+ fillOpacity: "0.75",
2066
2273
  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"
2067
2274
  }
2068
2275
  )
@@ -2080,6 +2287,13 @@ async function confirmPayment(input) {
2080
2287
  if (!paymentElement) {
2081
2288
  return { status: "error", message: copy.errors.cardElementMissing };
2082
2289
  }
2290
+ const { error: submitError } = await elements.submit();
2291
+ if (submitError) {
2292
+ return {
2293
+ status: "error",
2294
+ message: submitError.message || copy.errors.paymentUnexpected
2295
+ };
2296
+ }
2083
2297
  const { error: error2, paymentIntent: paymentIntent2 } = await stripe.confirmPayment({
2084
2298
  elements,
2085
2299
  clientSecret,
@@ -2394,7 +2608,11 @@ var PaidInner = ({
2394
2608
  const copy = useCopy();
2395
2609
  const customer = useCustomer();
2396
2610
  const { processPayment } = useSolvaPay();
2397
- const { refetch } = usePurchase();
2611
+ const { refetch, hasPaidPurchase } = usePurchase();
2612
+ const hasPaidPurchaseRef = (0, import_react15.useRef)(hasPaidPurchase);
2613
+ (0, import_react15.useEffect)(() => {
2614
+ hasPaidPurchaseRef.current = hasPaidPurchase;
2615
+ }, [hasPaidPurchase]);
2398
2616
  const [elementKind, setElementKind] = (0, import_react15.useState)(
2399
2617
  children ? null : "payment-element"
2400
2618
  );
@@ -2445,14 +2663,34 @@ var PaidInner = ({
2445
2663
  refetchPurchase: refetch,
2446
2664
  copy
2447
2665
  });
2448
- setIsProcessing(false);
2449
2666
  if (reconcileResult.status === "success") {
2450
2667
  const pi = paymentIntent;
2451
2668
  onSuccess?.(pi);
2452
2669
  const paid = { kind: "paid", paymentIntent: pi };
2453
2670
  onResult?.(paid);
2671
+ const CONFIRMATION_TIMEOUT_MS = 1e4;
2672
+ const startedAt = Date.now();
2673
+ let attempt = 0;
2674
+ while (!hasPaidPurchaseRef.current && Date.now() - startedAt < CONFIRMATION_TIMEOUT_MS) {
2675
+ attempt += 1;
2676
+ await new Promise((r) => setTimeout(r, Math.min(500 * attempt, 1500)));
2677
+ if (hasPaidPurchaseRef.current) break;
2678
+ try {
2679
+ await refetch();
2680
+ } catch {
2681
+ }
2682
+ }
2683
+ if (!hasPaidPurchaseRef.current) {
2684
+ const confirmationMsg = copy.errors.paymentConfirmationDelayed;
2685
+ setError(confirmationMsg);
2686
+ setIsProcessing(false);
2687
+ onError?.(new Error(confirmationMsg));
2688
+ return;
2689
+ }
2690
+ setIsProcessing(false);
2454
2691
  return;
2455
2692
  }
2693
+ setIsProcessing(false);
2456
2694
  const msg = reconcileResult.status === "timeout" ? reconcileResult.error.message : copy.errors.paymentProcessingFailed;
2457
2695
  setError(msg);
2458
2696
  onError?.(reconcileResult.error);
@@ -2953,7 +3191,9 @@ function useTopupAmountSelector(options) {
2953
3191
  if (resolvedAmount < minAmount) {
2954
3192
  setError(
2955
3193
  interpolate(copy.topup.minAmount, {
2956
- amount: `${currencySymbol}${minAmount}`
3194
+ amount: formatPrice(minAmount * getMinorUnitsPerMajor(currency), currency, {
3195
+ free: ""
3196
+ })
2957
3197
  })
2958
3198
  );
2959
3199
  return false;
@@ -2961,14 +3201,16 @@ function useTopupAmountSelector(options) {
2961
3201
  if (resolvedAmount > maxAmount) {
2962
3202
  setError(
2963
3203
  interpolate(copy.topup.maxAmount, {
2964
- amount: `${currencySymbol}${maxAmount.toLocaleString()}`
3204
+ amount: formatPrice(maxAmount * getMinorUnitsPerMajor(currency), currency, {
3205
+ free: ""
3206
+ })
2965
3207
  })
2966
3208
  );
2967
3209
  return false;
2968
3210
  }
2969
3211
  setError(null);
2970
3212
  return true;
2971
- }, [resolvedAmount, minAmount, maxAmount, currencySymbol, copy]);
3213
+ }, [resolvedAmount, minAmount, maxAmount, currency, copy]);
2972
3214
  const reset = (0, import_react16.useCallback)(() => {
2973
3215
  setSelectedAmount(null);
2974
3216
  setCustomAmountRaw("");
@@ -3008,15 +3250,31 @@ function usePickerCtx(part) {
3008
3250
  }
3009
3251
  return ctx;
3010
3252
  }
3011
- var Root3 = (0, import_react18.forwardRef)(function AmountPickerRoot({ currency, minAmount, maxAmount, onChange, asChild, children, ...rest }, forwardedRef) {
3253
+ var Root3 = (0, import_react18.forwardRef)(function AmountPickerRoot({
3254
+ currency,
3255
+ minAmount,
3256
+ maxAmount,
3257
+ emit = "major",
3258
+ selector: externalSelector,
3259
+ onChange,
3260
+ asChild,
3261
+ children,
3262
+ ...rest
3263
+ }, forwardedRef) {
3012
3264
  const solva = (0, import_react18.useContext)(SolvaPayContext);
3013
3265
  if (!solva) throw new MissingProviderError("AmountPicker");
3014
- const selector = useTopupAmountSelector({ currency, minAmount, maxAmount });
3266
+ const internalSelector = useTopupAmountSelector({ currency, minAmount, maxAmount });
3267
+ const selector = externalSelector ?? internalSelector;
3015
3268
  const { creditsPerMinorUnit, displayExchangeRate } = useBalance();
3269
+ const minorPerMajor = getMinorUnitsPerMajor(currency);
3270
+ const resolvedAmountMinor = selector.resolvedAmount != null && selector.resolvedAmount > 0 ? Math.round(selector.resolvedAmount * minorPerMajor) : null;
3016
3271
  (0, import_react18.useEffect)(() => {
3017
- onChange?.(selector.resolvedAmount);
3018
- }, [selector.resolvedAmount, onChange]);
3019
- const resolvedAmountMinor = selector.resolvedAmount != null && selector.resolvedAmount > 0 ? Math.round(selector.resolvedAmount * 100) : null;
3272
+ if (emit === "minor") {
3273
+ onChange?.(resolvedAmountMinor);
3274
+ } else {
3275
+ onChange?.(selector.resolvedAmount);
3276
+ }
3277
+ }, [emit, selector.resolvedAmount, resolvedAmountMinor, onChange]);
3020
3278
  const rate = displayExchangeRate ?? 1;
3021
3279
  const estimatedCredits = creditsPerMinorUnit != null && creditsPerMinorUnit > 0 && resolvedAmountMinor != null ? Math.floor(resolvedAmountMinor / rate * creditsPerMinorUnit) : null;
3022
3280
  const isApproximate = rate !== 1;
@@ -3024,12 +3282,23 @@ var Root3 = (0, import_react18.forwardRef)(function AmountPickerRoot({ currency,
3024
3282
  () => ({
3025
3283
  ...selector,
3026
3284
  currency,
3285
+ emit,
3027
3286
  creditsPerMinorUnit,
3028
3287
  displayExchangeRate,
3029
3288
  estimatedCredits,
3030
- isApproximate
3289
+ isApproximate,
3290
+ resolvedAmountMinor
3031
3291
  }),
3032
- [selector, currency, creditsPerMinorUnit, displayExchangeRate, estimatedCredits, isApproximate]
3292
+ [
3293
+ selector,
3294
+ currency,
3295
+ emit,
3296
+ creditsPerMinorUnit,
3297
+ displayExchangeRate,
3298
+ estimatedCredits,
3299
+ isApproximate,
3300
+ resolvedAmountMinor
3301
+ ]
3033
3302
  );
3034
3303
  const Comp = asChild ? Slot : "div";
3035
3304
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AmountPickerContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Comp, { ref: forwardedRef, "data-solvapay-amount-picker": "", ...rest, children }) });
@@ -3048,19 +3317,18 @@ var Option = (0, import_react18.forwardRef)(function AmountPickerOption({ asChil
3048
3317
  }),
3049
3318
  ...rest
3050
3319
  };
3320
+ const defaultLabel = formatPrice(
3321
+ amount * getMinorUnitsPerMajor(ctx.currency),
3322
+ ctx.currency,
3323
+ { free: "" }
3324
+ );
3051
3325
  if (asChild) {
3052
3326
  return (
3053
3327
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3054
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
3055
- ctx.currencySymbol,
3056
- amount.toLocaleString()
3057
- ] }) })
3328
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? defaultLabel })
3058
3329
  );
3059
3330
  }
3060
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
3061
- ctx.currencySymbol,
3062
- amount.toLocaleString()
3063
- ] }) });
3331
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? defaultLabel });
3064
3332
  });
3065
3333
  var Custom = (0, import_react18.forwardRef)(function AmountPickerCustom({ asChild, onChange, onFocus, ...rest }, forwardedRef) {
3066
3334
  const ctx = usePickerCtx("Custom");
@@ -3092,9 +3360,12 @@ var Confirm = (0, import_react18.forwardRef)(function AmountPickerConfirm({ asCh
3092
3360
  const ctx = usePickerCtx("Confirm");
3093
3361
  const isDisabled = disabled || !ctx.resolvedAmount;
3094
3362
  const handleConfirm = (0, import_react18.useCallback)(() => {
3095
- if (ctx.validate() && ctx.resolvedAmount != null) {
3096
- onConfirm?.(ctx.resolvedAmount);
3363
+ if (!ctx.validate()) return;
3364
+ if (ctx.emit === "minor") {
3365
+ if (ctx.resolvedAmountMinor != null) onConfirm?.(ctx.resolvedAmountMinor);
3366
+ return;
3097
3367
  }
3368
+ if (ctx.resolvedAmount != null) onConfirm?.(ctx.resolvedAmount);
3098
3369
  }, [ctx, onConfirm]);
3099
3370
  const commonProps = {
3100
3371
  "data-solvapay-amount-picker-confirm": "",
@@ -3314,6 +3585,14 @@ var Inner = ({
3314
3585
  }
3315
3586
  setError(null);
3316
3587
  setIsProcessing(true);
3588
+ const { error: submitError } = await elements.submit();
3589
+ if (submitError) {
3590
+ const msg = submitError.message || copy.errors.paymentUnexpected;
3591
+ setError(msg);
3592
+ setIsProcessing(false);
3593
+ onError?.(new Error(msg));
3594
+ return;
3595
+ }
3317
3596
  const { error: confirmError, paymentIntent } = await stripe.confirmPayment({
3318
3597
  elements,
3319
3598
  clientSecret,
@@ -3817,24 +4096,13 @@ var import_react27 = require("react");
3817
4096
 
3818
4097
  // src/primitives/PlanSelector.tsx
3819
4098
  var import_react25 = require("react");
3820
- var import_jsx_runtime19 = require("react/jsx-runtime");
3821
- var PlanSelectorContext = (0, import_react25.createContext)(null);
3822
- function usePlanSelectorContext(part) {
3823
- const ctx = (0, import_react25.useContext)(PlanSelectorContext);
3824
- if (!ctx) {
3825
- throw new Error(`PlanSelector.${part} must be rendered inside <PlanSelector.Root>.`);
4099
+
4100
+ // src/transport/list-plans.ts
4101
+ async function defaultListPlans(productRef, config) {
4102
+ const transport = config?.transport;
4103
+ if (transport) {
4104
+ return transport.listPlans ? transport.listPlans(productRef) : plansCache.get(productRef)?.plans ?? [];
3826
4105
  }
3827
- return ctx;
3828
- }
3829
- var CardContext = (0, import_react25.createContext)(null);
3830
- function useCardContext(part) {
3831
- const ctx = (0, import_react25.useContext)(CardContext);
3832
- if (!ctx) {
3833
- throw new Error(`PlanSelector.${part} must be rendered inside <PlanSelector.Card>.`);
3834
- }
3835
- return ctx;
3836
- }
3837
- async function defaultListPlans(productRef, config) {
3838
4106
  const base = config?.api?.listPlans || "/api/list-plans";
3839
4107
  const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
3840
4108
  const fetchFn = config?.fetch || fetch;
@@ -3848,6 +4116,25 @@ async function defaultListPlans(productRef, config) {
3848
4116
  const data = await res.json();
3849
4117
  return data.plans ?? [];
3850
4118
  }
4119
+
4120
+ // src/primitives/PlanSelector.tsx
4121
+ var import_jsx_runtime19 = require("react/jsx-runtime");
4122
+ var PlanSelectorContext = (0, import_react25.createContext)(null);
4123
+ function usePlanSelectorContext(part) {
4124
+ const ctx = (0, import_react25.useContext)(PlanSelectorContext);
4125
+ if (!ctx) {
4126
+ throw new Error(`PlanSelector.${part} must be rendered inside <PlanSelector.Root>.`);
4127
+ }
4128
+ return ctx;
4129
+ }
4130
+ var CardContext = (0, import_react25.createContext)(null);
4131
+ function useCardContext(part) {
4132
+ const ctx = (0, import_react25.useContext)(CardContext);
4133
+ if (!ctx) {
4134
+ throw new Error(`PlanSelector.${part} must be rendered inside <PlanSelector.Card>.`);
4135
+ }
4136
+ return ctx;
4137
+ }
3851
4138
  var Root6 = (0, import_react25.forwardRef)(function PlanSelectorRoot(props, forwardedRef) {
3852
4139
  const {
3853
4140
  productRef,
@@ -3871,7 +4158,7 @@ var Root6 = (0, import_react25.forwardRef)(function PlanSelectorRoot(props, forw
3871
4158
  () => fetcher ?? ((ref) => defaultListPlans(ref, _config)),
3872
4159
  [fetcher, _config]
3873
4160
  );
3874
- const { plans, selectedPlan, selectPlan, loading, error } = usePlans({
4161
+ const { plans, selectedPlan, selectPlan, setSelectedPlanIndex, loading, error } = usePlans({
3875
4162
  productRef,
3876
4163
  fetcher: effectiveFetcher,
3877
4164
  filter,
@@ -3906,6 +4193,9 @@ var Root6 = (0, import_react25.forwardRef)(function PlanSelectorRoot(props, forw
3906
4193
  },
3907
4194
  [plans, selectPlan, onSelect]
3908
4195
  );
4196
+ const clearSelection = (0, import_react25.useCallback)(() => {
4197
+ setSelectedPlanIndex(-1);
4198
+ }, [setSelectedPlanIndex]);
3909
4199
  const selectedPlanRef = selectedPlan?.reference ?? null;
3910
4200
  const ctx = (0, import_react25.useMemo)(
3911
4201
  () => ({
@@ -3919,7 +4209,8 @@ var Root6 = (0, import_react25.forwardRef)(function PlanSelectorRoot(props, forw
3919
4209
  isCurrent,
3920
4210
  isFree,
3921
4211
  isPopular,
3922
- select
4212
+ select,
4213
+ clearSelection
3923
4214
  }),
3924
4215
  [
3925
4216
  plans,
@@ -3932,7 +4223,8 @@ var Root6 = (0, import_react25.forwardRef)(function PlanSelectorRoot(props, forw
3932
4223
  isCurrent,
3933
4224
  isFree,
3934
4225
  isPopular,
3935
- select
4226
+ select,
4227
+ clearSelection
3936
4228
  ]
3937
4229
  );
3938
4230
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
@@ -4013,9 +4305,11 @@ var Card = (0, import_react25.forwardRef)(function PlanSelectorCard({ asChild, o
4013
4305
  });
4014
4306
  var CardName = (0, import_react25.forwardRef)(function PlanSelectorCardName({ asChild, children, ...rest }, forwardedRef) {
4015
4307
  const card = useCardContext("CardName");
4016
- if (!card.plan.name) return null;
4308
+ const fallback = card.plan.requiresPayment === false ? "Free" : card.plan.type === "usage-based" ? "Pay as you go" : card.plan.type === "recurring" ? "Plan" : null;
4309
+ const label = card.plan.name ?? fallback;
4310
+ if (!label && children == null) return null;
4017
4311
  const Comp = asChild ? Slot : "span";
4018
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-name": "", ...rest, children: children ?? card.plan.name });
4312
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-name": "", ...rest, children: children ?? label });
4019
4313
  });
4020
4314
  var CardPrice = (0, import_react25.forwardRef)(function PlanSelectorCardPrice({ asChild, children, ...rest }, forwardedRef) {
4021
4315
  const card = useCardContext("CardPrice");
@@ -4023,14 +4317,25 @@ var CardPrice = (0, import_react25.forwardRef)(function PlanSelectorCardPrice({
4023
4317
  const copy = useCopy();
4024
4318
  const formatted = (0, import_react25.useMemo)(() => {
4025
4319
  if (card.isFree) return copy.planSelector.freeBadge;
4320
+ if (card.plan.type === "usage-based") {
4321
+ const creditsPerUnit = card.plan.creditsPerUnit ?? 1;
4322
+ const perCallMinor = Math.max(1, Math.round(1 / creditsPerUnit));
4323
+ const rate = formatPrice(perCallMinor, card.plan.currency ?? "usd", {
4324
+ locale,
4325
+ free: ""
4326
+ });
4327
+ return `${rate} / call`;
4328
+ }
4026
4329
  return formatPrice(card.plan.price ?? 0, card.plan.currency ?? "usd", {
4027
4330
  locale,
4028
4331
  free: copy.interval.free
4029
4332
  });
4030
4333
  }, [
4031
4334
  card.isFree,
4335
+ card.plan.type,
4032
4336
  card.plan.price,
4033
4337
  card.plan.currency,
4338
+ card.plan.creditsPerUnit,
4034
4339
  locale,
4035
4340
  copy.planSelector.freeBadge,
4036
4341
  copy.interval.free
@@ -4041,11 +4346,22 @@ var CardPrice = (0, import_react25.forwardRef)(function PlanSelectorCardPrice({
4041
4346
  var CardInterval = (0, import_react25.forwardRef)(function PlanSelectorCardInterval({ asChild, children, ...rest }, forwardedRef) {
4042
4347
  const card = useCardContext("CardInterval");
4043
4348
  const copy = useCopy();
4044
- if (card.isFree || !card.plan.interval) return null;
4045
- const text = interpolate(copy.planSelector.perIntervalShort, { interval: card.plan.interval });
4349
+ if (card.isFree || card.plan.type === "usage-based") return null;
4350
+ const rawInterval = card.plan.interval ?? normalizeBillingCycle(card.plan.billingCycle);
4351
+ if (!rawInterval) return null;
4352
+ const text = interpolate(copy.planSelector.perIntervalShort, { interval: rawInterval });
4046
4353
  const Comp = asChild ? Slot : "span";
4047
4354
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-interval": "", ...rest, children: children ?? text });
4048
4355
  });
4356
+ function normalizeBillingCycle(cycle) {
4357
+ if (!cycle) return null;
4358
+ const lc = cycle.toLowerCase();
4359
+ if (lc === "monthly" || lc === "month") return "month";
4360
+ if (lc === "yearly" || lc === "annually" || lc === "annual" || lc === "year") return "year";
4361
+ if (lc === "weekly" || lc === "week") return "week";
4362
+ if (lc === "daily" || lc === "day") return "day";
4363
+ return cycle;
4364
+ }
4049
4365
  var CardBadge = (0, import_react25.forwardRef)(function PlanSelectorCardBadge({ asChild, children, ...rest }, forwardedRef) {
4050
4366
  const card = useCardContext("CardBadge");
4051
4367
  const copy = useCopy();
@@ -4185,16 +4501,19 @@ var Root7 = (0, import_react26.forwardRef)(function ActivationFlowRoot({
4185
4501
  const retryActivation = (0, import_react26.useCallback)(async () => {
4186
4502
  if (!resolvedPlanRef) return;
4187
4503
  setStep("retrying");
4188
- const amountMinor = Math.round((amountSelector.resolvedAmount || 0) * 100);
4504
+ const amountMinor2 = Math.round(
4505
+ (amountSelector.resolvedAmount || 0) * getMinorUnitsPerMajor(currency)
4506
+ );
4189
4507
  const rate = displayExchangeRate ?? 1;
4190
4508
  if (creditsPerMinorUnit) {
4191
- adjustBalance(Math.floor(amountMinor / rate * creditsPerMinorUnit));
4509
+ adjustBalance(Math.floor(amountMinor2 / rate * creditsPerMinorUnit));
4192
4510
  }
4193
4511
  await new Promise((r) => setTimeout(r, retryDelayMs));
4194
4512
  await activate({ productRef, planRef: resolvedPlanRef });
4195
4513
  }, [
4196
4514
  resolvedPlanRef,
4197
4515
  amountSelector.resolvedAmount,
4516
+ currency,
4198
4517
  displayExchangeRate,
4199
4518
  creditsPerMinorUnit,
4200
4519
  adjustBalance,
@@ -4221,7 +4540,9 @@ var Root7 = (0, import_react26.forwardRef)(function ActivationFlowRoot({
4221
4540
  calledSuccessRef.current = false;
4222
4541
  setStep("summary");
4223
4542
  }, [reset]);
4224
- const amountCents = Math.round((amountSelector.resolvedAmount || 0) * 100);
4543
+ const amountMinor = Math.round(
4544
+ (amountSelector.resolvedAmount || 0) * getMinorUnitsPerMajor(currency)
4545
+ );
4225
4546
  const ctx = (0, import_react26.useMemo)(
4226
4547
  () => ({
4227
4548
  step,
@@ -4229,7 +4550,11 @@ var Root7 = (0, import_react26.forwardRef)(function ActivationFlowRoot({
4229
4550
  productRef,
4230
4551
  planRef: resolvedPlanRef ?? "",
4231
4552
  amountSelector,
4232
- amountCents,
4553
+ amountMinor,
4554
+ // Deprecated alias — same value as amountMinor so legacy consumers
4555
+ // keep working after the zero-decimal fix (previously this was
4556
+ // `resolvedAmount * 100`, which over-billed JPY/KRW by 100x).
4557
+ amountCents: amountMinor,
4233
4558
  currency,
4234
4559
  activate: handleActivate,
4235
4560
  reset: resetFlow,
@@ -4245,7 +4570,7 @@ var Root7 = (0, import_react26.forwardRef)(function ActivationFlowRoot({
4245
4570
  productRef,
4246
4571
  resolvedPlanRef,
4247
4572
  amountSelector,
4248
- amountCents,
4573
+ amountMinor,
4249
4574
  currency,
4250
4575
  handleActivate,
4251
4576
  resetFlow,
@@ -4307,7 +4632,7 @@ var ActivateButton = (0, import_react26.forwardRef)(
4307
4632
  var AmountPickerMount = ({ children }) => {
4308
4633
  const ctx = useFlowCtx("AmountPicker");
4309
4634
  if (ctx.step !== "selectAmount") return null;
4310
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "data-solvapay-activation-flow-amount-picker": "", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(AmountPicker.Root, { currency: ctx.currency, children }) });
4635
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "data-solvapay-activation-flow-amount-picker": "", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(AmountPicker.Root, { currency: ctx.currency, selector: ctx.amountSelector, children }) });
4311
4636
  };
4312
4637
  var ContinueButton = (0, import_react26.forwardRef)(
4313
4638
  function ActivationFlowContinueButton({ asChild, onClick, children, ...rest }, forwardedRef) {
@@ -4450,7 +4775,7 @@ var TopupPaymentStep = () => {
4450
4775
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4451
4776
  TopupForm2,
4452
4777
  {
4453
- amount: ctx.amountCents,
4778
+ amount: ctx.amountMinor,
4454
4779
  currency: ctx.currency,
4455
4780
  onSuccess: ctx.onTopupSuccess,
4456
4781
  onError: (err) => ctx.onError?.(err instanceof Error ? err : new Error(String(err)))
@@ -4631,26 +4956,12 @@ var CheckoutLayout = ({
4631
4956
  }
4632
4957
  );
4633
4958
  };
4634
- async function defaultListPlans2(productRef, config) {
4635
- const base = config?.api?.listPlans || "/api/list-plans";
4636
- const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
4637
- const fetchFn = config?.fetch || fetch;
4638
- const { headers } = await buildRequestHeaders(config);
4639
- const res = await fetchFn(url, { method: "GET", headers });
4640
- if (!res.ok) {
4641
- const error = new Error(`Failed to fetch plans: ${res.statusText || res.status}`);
4642
- config?.onError?.(error, "listPlans");
4643
- throw error;
4644
- }
4645
- const data = await res.json();
4646
- return data.plans ?? [];
4647
- }
4648
4959
  var SelectStep = ({ productRef, initialPlanRef, filter, sortBy, popularPlanRef, onContinue }) => {
4649
4960
  const copy = useCopy();
4650
4961
  const solva = (0, import_react27.useContext)(SolvaPayContext);
4651
4962
  const config = solva?._config;
4652
4963
  const fetcher = (0, import_react27.useCallback)(
4653
- (ref) => defaultListPlans2(ref, config),
4964
+ (ref) => defaultListPlans(ref, config),
4654
4965
  [config]
4655
4966
  );
4656
4967
  const { plans, selectedPlan } = usePlans({
@@ -4888,7 +5199,7 @@ function usePurchaseStatus() {
4888
5199
  }, []);
4889
5200
  const purchaseData = (0, import_react30.useMemo)(() => {
4890
5201
  const cancelledPaidPurchases = purchases.filter((p) => {
4891
- return p.status === "active" && p.cancelledAt && isPaidPurchase2(p);
5202
+ return p.status === "active" && p.cancelledAt && isPaidPurchase2(p) && isPlanPurchase(p);
4892
5203
  });
4893
5204
  const cancelledPurchase = cancelledPaidPurchases.sort(
4894
5205
  (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
@@ -5259,6 +5570,737 @@ var CreditGate2 = ({
5259
5570
  }
5260
5571
  );
5261
5572
  };
5573
+
5574
+ // src/hooks/usePaymentMethod.ts
5575
+ var import_react33 = require("react");
5576
+ var paymentMethodCache = /* @__PURE__ */ new Map();
5577
+ var CACHE_DURATION5 = 5 * 60 * 1e3;
5578
+ function cacheKeyFor3(config) {
5579
+ return createTransportCacheKey(
5580
+ config,
5581
+ config?.api?.getPaymentMethod || "/api/payment-method"
5582
+ );
5583
+ }
5584
+ async function fetchPaymentMethod(config) {
5585
+ const transport = config?.transport ?? createHttpTransport(config);
5586
+ if (!transport.getPaymentMethod) return null;
5587
+ return transport.getPaymentMethod();
5588
+ }
5589
+ function usePaymentMethod() {
5590
+ const { _config } = useSolvaPay();
5591
+ const key = cacheKeyFor3(_config);
5592
+ const [paymentMethod, setPaymentMethod] = (0, import_react33.useState)(
5593
+ () => paymentMethodCache.get(key)?.paymentMethod ?? null
5594
+ );
5595
+ const [loading, setLoading] = (0, import_react33.useState)(() => {
5596
+ const cached = paymentMethodCache.get(key);
5597
+ return !cached || !cached.paymentMethod && !cached.promise;
5598
+ });
5599
+ const [error, setError] = (0, import_react33.useState)(null);
5600
+ const load = (0, import_react33.useCallback)(
5601
+ async (force = false) => {
5602
+ const cached = paymentMethodCache.get(key);
5603
+ const now = Date.now();
5604
+ if (!force && cached?.paymentMethod && now - cached.timestamp < CACHE_DURATION5) {
5605
+ setPaymentMethod(cached.paymentMethod);
5606
+ setLoading(false);
5607
+ setError(null);
5608
+ return;
5609
+ }
5610
+ if (!force && cached?.promise) {
5611
+ try {
5612
+ setLoading(true);
5613
+ const pm = await cached.promise;
5614
+ setPaymentMethod(pm);
5615
+ setError(null);
5616
+ } catch (err) {
5617
+ setError(err instanceof Error ? err : new Error("Failed to load payment method"));
5618
+ } finally {
5619
+ setLoading(false);
5620
+ }
5621
+ return;
5622
+ }
5623
+ try {
5624
+ setLoading(true);
5625
+ setError(null);
5626
+ const promise = fetchPaymentMethod(_config);
5627
+ paymentMethodCache.set(key, {
5628
+ paymentMethod: cached?.paymentMethod ?? null,
5629
+ promise,
5630
+ timestamp: now
5631
+ });
5632
+ const pm = await promise;
5633
+ if (pm === null) {
5634
+ paymentMethodCache.set(key, {
5635
+ paymentMethod: cached?.paymentMethod ?? null,
5636
+ promise: null,
5637
+ timestamp: cached?.timestamp ?? now
5638
+ });
5639
+ setPaymentMethod(cached?.paymentMethod ?? null);
5640
+ setLoading(false);
5641
+ return;
5642
+ }
5643
+ paymentMethodCache.set(key, { paymentMethod: pm, promise: null, timestamp: now });
5644
+ setPaymentMethod(pm);
5645
+ } catch (err) {
5646
+ paymentMethodCache.delete(key);
5647
+ setError(err instanceof Error ? err : new Error("Failed to load payment method"));
5648
+ setPaymentMethod(null);
5649
+ } finally {
5650
+ setLoading(false);
5651
+ }
5652
+ },
5653
+ [_config, key]
5654
+ );
5655
+ (0, import_react33.useEffect)(() => {
5656
+ load();
5657
+ }, [load]);
5658
+ return {
5659
+ paymentMethod,
5660
+ loading,
5661
+ error,
5662
+ refetch: () => load(true)
5663
+ };
5664
+ }
5665
+
5666
+ // src/components/UpdatePaymentMethodButton.tsx
5667
+ var import_react36 = require("react");
5668
+
5669
+ // src/components/LaunchCustomerPortalButton.tsx
5670
+ var import_react35 = require("react");
5671
+
5672
+ // src/hooks/useTransport.ts
5673
+ var import_react34 = require("react");
5674
+ function useTransport() {
5675
+ const { _config } = useSolvaPay();
5676
+ return (0, import_react34.useMemo)(() => _config?.transport ?? createHttpTransport(_config), [_config]);
5677
+ }
5678
+
5679
+ // src/components/ExternalLinkGlyph.tsx
5680
+ var import_jsx_runtime30 = require("react/jsx-runtime");
5681
+ var ExternalLinkGlyph = ({ className }) => {
5682
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
5683
+ "svg",
5684
+ {
5685
+ className: `solvapay-mcp-external-glyph${className ? ` ${className}` : ""}`,
5686
+ width: "0.9em",
5687
+ height: "0.9em",
5688
+ viewBox: "0 0 24 24",
5689
+ fill: "none",
5690
+ stroke: "currentColor",
5691
+ strokeWidth: "2",
5692
+ strokeLinecap: "round",
5693
+ strokeLinejoin: "round",
5694
+ "aria-hidden": "true",
5695
+ focusable: "false",
5696
+ children: [
5697
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("path", { d: "M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6" }),
5698
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("path", { d: "M15 3h6v6" }),
5699
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("path", { d: "m10 14 11-11" })
5700
+ ]
5701
+ }
5702
+ );
5703
+ };
5704
+
5705
+ // src/components/LaunchCustomerPortalButton.tsx
5706
+ var import_jsx_runtime31 = (
5707
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5708
+ require("react/jsx-runtime")
5709
+ );
5710
+ var LaunchCustomerPortalButton = (0, import_react35.forwardRef)(function LaunchCustomerPortalButton2({
5711
+ children,
5712
+ onLaunch,
5713
+ onError,
5714
+ onClick,
5715
+ loadingClassName,
5716
+ errorClassName,
5717
+ asChild,
5718
+ ...rest
5719
+ }, forwardedRef) {
5720
+ const transport = useTransport();
5721
+ const copy = useCopy();
5722
+ const [state, setState] = (0, import_react35.useState)({ status: "loading" });
5723
+ const onErrorRef = (0, import_react35.useRef)(onError);
5724
+ (0, import_react35.useEffect)(() => {
5725
+ onErrorRef.current = onError;
5726
+ }, [onError]);
5727
+ (0, import_react35.useEffect)(() => {
5728
+ let cancelled = false;
5729
+ transport.createCustomerSession().then(({ customerUrl }) => {
5730
+ if (cancelled) return;
5731
+ setState({ status: "ready", href: customerUrl });
5732
+ }).catch((err) => {
5733
+ if (cancelled) return;
5734
+ const error = err instanceof Error ? err : new Error(String(err));
5735
+ setState({ status: "error", message: error.message });
5736
+ onErrorRef.current?.(error);
5737
+ });
5738
+ return () => {
5739
+ cancelled = true;
5740
+ };
5741
+ }, [transport]);
5742
+ if (state.status === "ready") {
5743
+ const label = children ?? copy.customerPortal.launchButton;
5744
+ const labelText = typeof label === "string" ? label : copy.customerPortal.launchButton;
5745
+ const readyProps = {
5746
+ href: state.href,
5747
+ target: "_blank",
5748
+ rel: "noopener noreferrer",
5749
+ "data-solvapay-launch-customer-portal": "",
5750
+ "data-state": "ready",
5751
+ "aria-label": `${labelText} (opens in a new tab)`,
5752
+ onClick: composeEventHandlers(onClick, () => {
5753
+ onLaunch?.(state.href);
5754
+ }),
5755
+ ...rest
5756
+ };
5757
+ if (asChild) {
5758
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(Slot, { ref: forwardedRef, ...readyProps, children: label });
5759
+ }
5760
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("a", { ref: forwardedRef, ...readyProps, children: [
5761
+ label,
5762
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ExternalLinkGlyph, {})
5763
+ ] });
5764
+ }
5765
+ if (state.status === "error") {
5766
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
5767
+ "button",
5768
+ {
5769
+ type: "button",
5770
+ className: errorClassName,
5771
+ "data-solvapay-launch-customer-portal": "",
5772
+ "data-state": "error",
5773
+ "aria-disabled": true,
5774
+ disabled: true,
5775
+ children: children ?? copy.customerPortal.launchButton
5776
+ }
5777
+ );
5778
+ }
5779
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
5780
+ "button",
5781
+ {
5782
+ type: "button",
5783
+ className: loadingClassName,
5784
+ "data-solvapay-launch-customer-portal": "",
5785
+ "data-state": "loading",
5786
+ "aria-busy": true,
5787
+ "aria-disabled": true,
5788
+ disabled: true,
5789
+ children: copy.customerPortal.loadingLabel
5790
+ }
5791
+ );
5792
+ });
5793
+
5794
+ // src/components/UpdatePaymentMethodButton.tsx
5795
+ var import_jsx_runtime32 = require("react/jsx-runtime");
5796
+ var UpdatePaymentMethodButton = (0, import_react36.forwardRef)(function UpdatePaymentMethodButton2({ mode = "portal", children, ...rest }, forwardedRef) {
5797
+ const copy = useCopy();
5798
+ if (mode !== "portal") {
5799
+ throw new Error(
5800
+ `<UpdatePaymentMethodButton mode="${mode}"> is not implemented yet \u2014 use mode="portal" or omit the prop.`
5801
+ );
5802
+ }
5803
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
5804
+ LaunchCustomerPortalButton,
5805
+ {
5806
+ ref: forwardedRef,
5807
+ "data-solvapay-update-payment-method": "",
5808
+ ...rest,
5809
+ children: children ?? copy.currentPlan.updatePaymentButton
5810
+ }
5811
+ );
5812
+ });
5813
+
5814
+ // src/primitives/UsageMeter.tsx
5815
+ var import_react38 = require("react");
5816
+
5817
+ // src/hooks/useUsage.ts
5818
+ var import_react37 = require("react");
5819
+ function deriveUsage(purchase) {
5820
+ if (!purchase) return null;
5821
+ const snap = purchase.planSnapshot;
5822
+ const usage = purchase.usage;
5823
+ const meterRef = snap?.meterRef ?? null;
5824
+ const total = typeof snap?.limit === "number" ? snap.limit : null;
5825
+ if (meterRef === null && !usage) return null;
5826
+ const used = typeof usage?.used === "number" ? usage.used : 0;
5827
+ const remaining = total !== null ? Math.max(0, total - used) : null;
5828
+ const percentUsed = total !== null && total > 0 ? Math.min(100, Math.round(used / total * 1e4) / 100) : null;
5829
+ return {
5830
+ meterRef,
5831
+ total,
5832
+ used,
5833
+ remaining,
5834
+ percentUsed,
5835
+ ...usage?.periodStart ? { periodStart: usage.periodStart } : {},
5836
+ ...usage?.periodEnd ? { periodEnd: usage.periodEnd } : {},
5837
+ ...purchase.reference ? { purchaseRef: purchase.reference } : {}
5838
+ };
5839
+ }
5840
+ function useUsage() {
5841
+ const { activePurchase, refetch: refetchPurchase, loading: purchaseLoading } = usePurchase();
5842
+ const transport = useTransport();
5843
+ const [override, setOverride] = (0, import_react37.useState)(null);
5844
+ const [error, setError] = (0, import_react37.useState)(null);
5845
+ const [transportLoading, setTransportLoading] = (0, import_react37.useState)(false);
5846
+ const derived = (0, import_react37.useMemo)(() => deriveUsage(activePurchase ?? null), [activePurchase]);
5847
+ const activePurchaseRef = activePurchase?.reference ?? null;
5848
+ (0, import_react37.useEffect)(() => {
5849
+ setOverride(null);
5850
+ }, [activePurchaseRef]);
5851
+ const usage = override ?? derived;
5852
+ const refetch = (0, import_react37.useCallback)(async () => {
5853
+ setError(null);
5854
+ if (typeof transport.getUsage === "function") {
5855
+ setTransportLoading(true);
5856
+ try {
5857
+ const next = await transport.getUsage();
5858
+ setOverride(next);
5859
+ } catch (err) {
5860
+ setError(err instanceof Error ? err : new Error("Failed to load usage"));
5861
+ } finally {
5862
+ setTransportLoading(false);
5863
+ }
5864
+ return;
5865
+ }
5866
+ try {
5867
+ await refetchPurchase();
5868
+ } catch (err) {
5869
+ setError(err instanceof Error ? err : new Error("Failed to refetch purchase"));
5870
+ }
5871
+ }, [transport, refetchPurchase]);
5872
+ const percentUsed = usage?.percentUsed ?? null;
5873
+ const isApproachingLimit = percentUsed !== null && percentUsed >= 80 && percentUsed < 100;
5874
+ const isAtLimit = percentUsed !== null && percentUsed >= 100;
5875
+ const isUnlimited = usage !== null && usage.total === null;
5876
+ return {
5877
+ usage,
5878
+ loading: purchaseLoading || transportLoading,
5879
+ error,
5880
+ refetch,
5881
+ percentUsed,
5882
+ isApproachingLimit,
5883
+ isAtLimit,
5884
+ isUnlimited,
5885
+ meterRef: usage?.meterRef ?? null
5886
+ };
5887
+ }
5888
+
5889
+ // src/primitives/UsageMeter.tsx
5890
+ var import_jsx_runtime33 = require("react/jsx-runtime");
5891
+ var UsageMeterContext = (0, import_react38.createContext)(null);
5892
+ function useUsageMeterCtx(part) {
5893
+ const ctx = (0, import_react38.useContext)(UsageMeterContext);
5894
+ if (!ctx) {
5895
+ throw new Error(`UsageMeter.${part} must be rendered inside <UsageMeter.Root>.`);
5896
+ }
5897
+ return ctx;
5898
+ }
5899
+ var Root10 = (0, import_react38.forwardRef)(function UsageMeterRoot({
5900
+ warningAt = 75,
5901
+ criticalAt = 90,
5902
+ classNames,
5903
+ asChild,
5904
+ usageOverride,
5905
+ children,
5906
+ className,
5907
+ ...rest
5908
+ }, forwardedRef) {
5909
+ const hookResult = useUsage();
5910
+ const usage = usageOverride !== void 0 ? usageOverride : hookResult.usage;
5911
+ const loading = hookResult.loading && !usage;
5912
+ const percentUsed = usage?.percentUsed ?? null;
5913
+ const state = loading ? "loading" : percentUsed === null ? "safe" : percentUsed >= criticalAt ? "critical" : percentUsed >= warningAt ? "warning" : "safe";
5914
+ const ctx = (0, import_react38.useMemo)(
5915
+ () => ({
5916
+ usage,
5917
+ loading,
5918
+ error: hookResult.error,
5919
+ percentUsed,
5920
+ isApproachingLimit: hookResult.isApproachingLimit,
5921
+ isAtLimit: hookResult.isAtLimit,
5922
+ isUnlimited: hookResult.isUnlimited,
5923
+ state,
5924
+ warningAt,
5925
+ criticalAt
5926
+ }),
5927
+ [
5928
+ usage,
5929
+ loading,
5930
+ hookResult.error,
5931
+ percentUsed,
5932
+ hookResult.isApproachingLimit,
5933
+ hookResult.isAtLimit,
5934
+ hookResult.isUnlimited,
5935
+ state,
5936
+ warningAt,
5937
+ criticalAt
5938
+ ]
5939
+ );
5940
+ const rootClass = [classNames?.root ?? "solvapay-usage-meter", className].filter(Boolean).join(" ");
5941
+ const Comp = asChild ? Slot : "div";
5942
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(UsageMeterContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
5943
+ Comp,
5944
+ {
5945
+ ref: forwardedRef,
5946
+ "data-solvapay-usage-meter": "",
5947
+ "data-state": state,
5948
+ className: rootClass,
5949
+ ...rest,
5950
+ children
5951
+ }
5952
+ ) });
5953
+ });
5954
+ var Bar = (0, import_react38.forwardRef)(function UsageMeterBar({ className, style, ...rest }, forwardedRef) {
5955
+ const ctx = useUsageMeterCtx("Bar");
5956
+ const width = ctx.percentUsed === null ? 0 : Math.max(0, Math.min(100, ctx.percentUsed));
5957
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
5958
+ "div",
5959
+ {
5960
+ ref: forwardedRef,
5961
+ "data-solvapay-usage-meter-bar": "",
5962
+ "data-state": ctx.state,
5963
+ role: "progressbar",
5964
+ "aria-valuenow": Math.round(width),
5965
+ "aria-valuemin": 0,
5966
+ "aria-valuemax": 100,
5967
+ className: className ?? "solvapay-usage-meter-bar",
5968
+ style: {
5969
+ // Expose as a CSS custom property so default styles can drive
5970
+ // `width: var(--solvapay-usage-meter-fill)` without inline styling.
5971
+ ["--solvapay-usage-meter-fill"]: `${width}%`,
5972
+ ...style
5973
+ },
5974
+ ...rest
5975
+ }
5976
+ );
5977
+ });
5978
+ var Label = (0, import_react38.forwardRef)(function UsageMeterLabel({ className, children, ...rest }, forwardedRef) {
5979
+ const ctx = useUsageMeterCtx("Label");
5980
+ const copy = useCopy();
5981
+ if (!ctx.usage) return null;
5982
+ if (ctx.isUnlimited) {
5983
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
5984
+ "span",
5985
+ {
5986
+ ref: forwardedRef,
5987
+ "data-solvapay-usage-meter-label": "",
5988
+ className: className ?? "solvapay-usage-meter-label",
5989
+ ...rest,
5990
+ children: children ?? copy.usage.unlimitedLabel
5991
+ }
5992
+ );
5993
+ }
5994
+ const unit = ctx.usage.meterRef ?? "units";
5995
+ const label = ctx.usage.total !== null ? interpolate(copy.usage.usedLabel, {
5996
+ used: String(ctx.usage.used),
5997
+ total: String(ctx.usage.total),
5998
+ unit
5999
+ }) : interpolate(copy.usage.remainingLabel, {
6000
+ remaining: String(ctx.usage.remaining ?? 0),
6001
+ unit
6002
+ });
6003
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
6004
+ "span",
6005
+ {
6006
+ ref: forwardedRef,
6007
+ "data-solvapay-usage-meter-label": "",
6008
+ className: className ?? "solvapay-usage-meter-label",
6009
+ ...rest,
6010
+ children: children ?? label
6011
+ }
6012
+ );
6013
+ });
6014
+ var Percentage = (0, import_react38.forwardRef)(function UsageMeterPercentage({ className, children, ...rest }, forwardedRef) {
6015
+ const ctx = useUsageMeterCtx("Percentage");
6016
+ const copy = useCopy();
6017
+ if (ctx.percentUsed === null) return null;
6018
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
6019
+ "span",
6020
+ {
6021
+ ref: forwardedRef,
6022
+ "data-solvapay-usage-meter-percentage": "",
6023
+ className: className ?? "solvapay-usage-meter-percentage",
6024
+ ...rest,
6025
+ children: children ?? interpolate(copy.usage.percentUsedLabel, { percent: String(Math.round(ctx.percentUsed)) })
6026
+ }
6027
+ );
6028
+ });
6029
+ function formatResetsIn(copy, periodEnd, nowMs = typeof performance !== "undefined" ? performance.timeOrigin + performance.now() : 0) {
6030
+ const end = new Date(periodEnd).getTime();
6031
+ if (Number.isNaN(end)) return null;
6032
+ const daysLeft = Math.max(0, Math.ceil((end - nowMs) / (24 * 60 * 60 * 1e3)));
6033
+ return daysLeft > 0 ? interpolate(copy.usage.resetsInLabel, { days: String(daysLeft) }) : interpolate(copy.usage.resetsOnLabel, { date: new Date(periodEnd).toLocaleDateString() });
6034
+ }
6035
+ var ResetsIn = (0, import_react38.forwardRef)(function UsageMeterResetsIn({ className, children, ...rest }, forwardedRef) {
6036
+ const ctx = useUsageMeterCtx("ResetsIn");
6037
+ const copy = useCopy();
6038
+ const periodEnd = ctx.usage?.periodEnd;
6039
+ if (!periodEnd) return null;
6040
+ const label = formatResetsIn(copy, periodEnd);
6041
+ if (!label) return null;
6042
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
6043
+ "span",
6044
+ {
6045
+ ref: forwardedRef,
6046
+ "data-solvapay-usage-meter-resets-in": "",
6047
+ className: className ?? "solvapay-usage-meter-resets-in",
6048
+ ...rest,
6049
+ children: children ?? label
6050
+ }
6051
+ );
6052
+ });
6053
+ var Loading7 = (0, import_react38.forwardRef)(function UsageMeterLoading({ className, children, ...rest }, forwardedRef) {
6054
+ const ctx = useUsageMeterCtx("Loading");
6055
+ const copy = useCopy();
6056
+ if (ctx.state !== "loading") return null;
6057
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
6058
+ "div",
6059
+ {
6060
+ ref: forwardedRef,
6061
+ "data-solvapay-usage-meter-loading": "",
6062
+ className: className ?? "solvapay-usage-meter-loading",
6063
+ ...rest,
6064
+ children: children ?? copy.usage.loadingLabel
6065
+ }
6066
+ );
6067
+ });
6068
+ var Empty = (0, import_react38.forwardRef)(function UsageMeterEmpty({ className, children, ...rest }, forwardedRef) {
6069
+ const ctx = useUsageMeterCtx("Empty");
6070
+ const copy = useCopy();
6071
+ if (ctx.loading || ctx.usage !== null) return null;
6072
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
6073
+ "div",
6074
+ {
6075
+ ref: forwardedRef,
6076
+ "data-solvapay-usage-meter-empty": "",
6077
+ className: className ?? "solvapay-usage-meter-empty",
6078
+ ...rest,
6079
+ children: children ?? copy.usage.emptyLabel
6080
+ }
6081
+ );
6082
+ });
6083
+ var UsageMeter = Object.assign(Root10, {
6084
+ Root: Root10,
6085
+ Bar,
6086
+ Label,
6087
+ Percentage,
6088
+ ResetsIn,
6089
+ Loading: Loading7,
6090
+ Empty
6091
+ });
6092
+
6093
+ // src/components/CurrentPlanCard.tsx
6094
+ var import_jsx_runtime34 = require("react/jsx-runtime");
6095
+ function PlanTypeLine({
6096
+ purchase,
6097
+ formatDate,
6098
+ className
6099
+ }) {
6100
+ const copy = useCopy();
6101
+ const planType = purchase.planSnapshot?.planType ?? "one-time";
6102
+ if (planType === "recurring" || purchase.isRecurring) {
6103
+ const date2 = formatDate(purchase.nextBillingDate);
6104
+ if (!date2) return null;
6105
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
6106
+ "span",
6107
+ {
6108
+ className,
6109
+ "data-solvapay-current-plan-next-billing": "",
6110
+ children: interpolate(copy.currentPlan.nextBilling, { date: date2 })
6111
+ }
6112
+ );
6113
+ }
6114
+ if (planType === "usage-based") {
6115
+ return null;
6116
+ }
6117
+ const date = formatDate(purchase.endDate);
6118
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className, "data-solvapay-current-plan-expires": "", children: date ? interpolate(copy.currentPlan.expiresOn, { date }) : copy.currentPlan.validIndefinitely });
6119
+ }
6120
+ function PaymentMethodLine({
6121
+ paymentMethod,
6122
+ className
6123
+ }) {
6124
+ const copy = useCopy();
6125
+ if (paymentMethod.kind === "none") {
6126
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className, "data-solvapay-current-plan-payment-method": "none", children: copy.currentPlan.noPaymentMethod });
6127
+ }
6128
+ const brandDisplay = paymentMethod.brand.charAt(0).toUpperCase() + paymentMethod.brand.slice(1);
6129
+ const label = interpolate(copy.currentPlan.paymentMethod, {
6130
+ brand: brandDisplay,
6131
+ last4: paymentMethod.last4
6132
+ });
6133
+ const expires = interpolate(copy.currentPlan.paymentMethodExpires, {
6134
+ month: String(paymentMethod.expMonth).padStart(2, "0"),
6135
+ year: paymentMethod.expYear
6136
+ });
6137
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { className, "data-solvapay-current-plan-payment-method": "card", children: [
6138
+ label,
6139
+ ", ",
6140
+ expires
6141
+ ] });
6142
+ }
6143
+ var CurrentPlanCard = ({
6144
+ hidePaymentMethod,
6145
+ hideCancelButton,
6146
+ hideUpdatePaymentButton,
6147
+ hideUsageMeter,
6148
+ classNames: overrides,
6149
+ className
6150
+ }) => {
6151
+ const copy = useCopy();
6152
+ const { activePurchase } = usePurchase();
6153
+ const { formatDate } = usePurchaseStatus();
6154
+ const { paymentMethod } = usePaymentMethod();
6155
+ if (!activePurchase) return null;
6156
+ const planType = activePurchase.planSnapshot?.planType ?? "one-time";
6157
+ const isUsageBased = planType === "usage-based";
6158
+ const amount = activePurchase.originalAmount ?? activePurchase.amount ?? 0;
6159
+ const currency = activePurchase.currency ?? "usd";
6160
+ const cycleKey = activePurchase.billingCycle;
6161
+ const intervalLabel = cycleKey ? copy.currentPlan.cycleUnit[cycleKey] ?? cycleKey : void 0;
6162
+ const priceLabel = formatPrice(amount, currency, {
6163
+ interval: intervalLabel
6164
+ });
6165
+ const planName = activePurchase.planSnapshot?.name ?? activePurchase.productName;
6166
+ const productContext = activePurchase.productName && activePurchase.productName !== planName ? activePurchase.productName : null;
6167
+ const rootClass = [
6168
+ "solvapay-current-plan-card",
6169
+ overrides?.root,
6170
+ className
6171
+ ].filter(Boolean).join(" ");
6172
+ const shouldShowPaymentMethod = !hidePaymentMethod && paymentMethod !== null;
6173
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
6174
+ "div",
6175
+ {
6176
+ className: rootClass,
6177
+ "data-solvapay-current-plan-card": "",
6178
+ "data-plan-type": planType,
6179
+ "data-solvapay-current-plan-ref": activePurchase.planRef ?? void 0,
6180
+ children: [
6181
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
6182
+ "h2",
6183
+ {
6184
+ className: overrides?.heading ?? "solvapay-current-plan-heading",
6185
+ "data-solvapay-current-plan-heading": "",
6186
+ children: copy.currentPlan.heading
6187
+ }
6188
+ ),
6189
+ productContext && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
6190
+ "div",
6191
+ {
6192
+ className: overrides?.productContext ?? "solvapay-current-plan-product-context",
6193
+ "data-solvapay-current-plan-product-context": "",
6194
+ children: productContext
6195
+ }
6196
+ ),
6197
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
6198
+ "div",
6199
+ {
6200
+ className: overrides?.planName ?? "solvapay-current-plan-name",
6201
+ "data-solvapay-current-plan-name": "",
6202
+ children: planName
6203
+ }
6204
+ ),
6205
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
6206
+ "div",
6207
+ {
6208
+ className: overrides?.price ?? "solvapay-current-plan-price",
6209
+ "data-solvapay-current-plan-price": "",
6210
+ children: priceLabel
6211
+ }
6212
+ ),
6213
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
6214
+ PlanTypeLine,
6215
+ {
6216
+ purchase: activePurchase,
6217
+ formatDate,
6218
+ className: overrides?.dateLine ?? "solvapay-current-plan-date-line"
6219
+ }
6220
+ ),
6221
+ isUsageBased && !hideUsageMeter && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
6222
+ "div",
6223
+ {
6224
+ className: overrides?.usageMeter ?? "solvapay-current-plan-usage-meter",
6225
+ "data-solvapay-current-plan-usage-meter": "",
6226
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(UsageMeter.Root, { children: [
6227
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(UsageMeter.Label, {}),
6228
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(UsageMeter.Bar, {}),
6229
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(UsageMeter.Percentage, {}),
6230
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(UsageMeter.ResetsIn, {}),
6231
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(UsageMeter.Loading, {})
6232
+ ] })
6233
+ }
6234
+ ),
6235
+ isUsageBased && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
6236
+ "div",
6237
+ {
6238
+ className: overrides?.balanceLine ?? "solvapay-current-plan-balance-line",
6239
+ "data-solvapay-current-plan-balance-line": "",
6240
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(BalanceBadge, {})
6241
+ }
6242
+ ),
6243
+ shouldShowPaymentMethod && paymentMethod && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
6244
+ PaymentMethodLine,
6245
+ {
6246
+ paymentMethod,
6247
+ className: overrides?.paymentMethod ?? "solvapay-current-plan-payment-method"
6248
+ }
6249
+ ),
6250
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
6251
+ "div",
6252
+ {
6253
+ className: overrides?.actions ?? "solvapay-current-plan-actions",
6254
+ "data-solvapay-current-plan-actions": "",
6255
+ children: [
6256
+ !hideUpdatePaymentButton && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(UpdatePaymentMethodButton, {}),
6257
+ !hideCancelButton && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(CancelPlanButton, {})
6258
+ ]
6259
+ }
6260
+ )
6261
+ ]
6262
+ }
6263
+ );
6264
+ };
6265
+
6266
+ // src/hooks/usePaywallResolver.ts
6267
+ var import_react39 = require("react");
6268
+ function usePaywallResolver(content) {
6269
+ const { activePurchase, hasPaidPurchase, refetch: refetchPurchase } = usePurchase();
6270
+ const { credits, refetch: refetchBalance } = useBalance();
6271
+ const resolved = (0, import_react39.useMemo)(() => {
6272
+ if (!content) return false;
6273
+ const productMatches = !content.product || activePurchase?.productRef === content.product || activePurchase?.productName === content.product;
6274
+ if (content.kind === "payment_required") {
6275
+ return Boolean(hasPaidPurchase && productMatches);
6276
+ }
6277
+ const balance = content.balance;
6278
+ if (balance && typeof balance.remainingUnits === "number" && balance.remainingUnits > 0) {
6279
+ return true;
6280
+ }
6281
+ if (balance && credits != null && balance.creditsPerUnit && credits >= balance.creditsPerUnit) {
6282
+ return true;
6283
+ }
6284
+ return Boolean(productMatches && activePurchase?.status === "active");
6285
+ }, [content, hasPaidPurchase, activePurchase, credits]);
6286
+ const refetch = (0, import_react39.useCallback)(async () => {
6287
+ await Promise.all([
6288
+ refetchPurchase().catch(() => void 0),
6289
+ refetchBalance().catch(() => void 0)
6290
+ ]);
6291
+ }, [refetchPurchase, refetchBalance]);
6292
+ return { resolved, refetch };
6293
+ }
6294
+
6295
+ // src/transport/types.ts
6296
+ var UnsupportedTransportMethodError = class extends Error {
6297
+ method;
6298
+ constructor(method) {
6299
+ super(`SolvaPay transport does not implement "${method}"`);
6300
+ this.name = "UnsupportedTransportMethodError";
6301
+ this.method = method;
6302
+ }
6303
+ };
5262
6304
  // Annotate the CommonJS export names for ESM import in node:
5263
6305
  0 && (module.exports = {
5264
6306
  ActivationFlow,
@@ -5271,6 +6313,9 @@ var CreditGate2 = ({
5271
6313
  CopyContext,
5272
6314
  CopyProvider,
5273
6315
  CreditGate,
6316
+ CurrentPlanCard,
6317
+ DEFAULT_ROUTES,
6318
+ LaunchCustomerPortalButton,
5274
6319
  MandateText,
5275
6320
  PaymentForm,
5276
6321
  PaymentFormContext,
@@ -5283,7 +6328,10 @@ var CreditGate2 = ({
5283
6328
  Spinner,
5284
6329
  StripePaymentFormWrapper,
5285
6330
  TopupForm,
6331
+ UnsupportedTransportMethodError,
6332
+ UpdatePaymentMethodButton,
5286
6333
  confirmPayment,
6334
+ createHttpTransport,
5287
6335
  defaultAuthAdapter,
5288
6336
  deriveVariant,
5289
6337
  enCopy,
@@ -5291,12 +6339,16 @@ var CreditGate2 = ({
5291
6339
  formatPrice,
5292
6340
  getActivePurchases,
5293
6341
  getCancelledPurchasesWithEndDate,
6342
+ getMinorUnitsPerMajor,
5294
6343
  getMostRecentPurchase,
5295
6344
  getPrimaryPurchase,
5296
6345
  interpolate,
5297
6346
  isPaidPurchase,
6347
+ isPlanPurchase,
6348
+ isTopupPurchase,
5298
6349
  mergeCopy,
5299
6350
  resolveCta,
6351
+ toMajorUnits,
5300
6352
  useActivation,
5301
6353
  useBalance,
5302
6354
  useCheckout,
@@ -5305,6 +6357,8 @@ var CreditGate2 = ({
5305
6357
  useLocale,
5306
6358
  useMerchant,
5307
6359
  usePaymentForm,
6360
+ usePaymentMethod,
6361
+ usePaywallResolver,
5308
6362
  usePlan,
5309
6363
  usePlans,
5310
6364
  useProduct,
@@ -5313,5 +6367,7 @@ var CreditGate2 = ({
5313
6367
  usePurchaseStatus,
5314
6368
  useSolvaPay,
5315
6369
  useTopup,
5316
- useTopupAmountSelector
6370
+ useTopupAmountSelector,
6371
+ useTransport,
6372
+ useUsage
5317
6373
  });