@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.
@@ -82,6 +82,16 @@ __export(primitives_exports, {
82
82
  PaymentFormSubmitButton: () => PaymentFormSubmitButton2,
83
83
  PaymentFormSummary: () => PaymentFormSummary,
84
84
  PaymentFormTermsCheckbox: () => PaymentFormTermsCheckbox2,
85
+ PaywallNotice: () => PaywallNotice,
86
+ PaywallNoticeBalance: () => Balance,
87
+ PaywallNoticeEmbeddedCheckout: () => EmbeddedCheckout,
88
+ PaywallNoticeHeading: () => Heading4,
89
+ PaywallNoticeHostedCheckoutLink: () => HostedCheckoutLink,
90
+ PaywallNoticeMessage: () => Message,
91
+ PaywallNoticePlans: () => Plans,
92
+ PaywallNoticeProductContext: () => ProductContext,
93
+ PaywallNoticeRetry: () => Retry,
94
+ PaywallNoticeRoot: () => Root10,
85
95
  PlanBadge: () => PlanBadge,
86
96
  PlanSelector: () => PlanSelector,
87
97
  PlanSelectorCard: () => PlanSelectorCard2,
@@ -109,6 +119,14 @@ __export(primitives_exports, {
109
119
  TopupFormPaymentElement: () => TopupFormPaymentElement,
110
120
  TopupFormRoot: () => TopupFormRoot2,
111
121
  TopupFormSubmitButton: () => TopupFormSubmitButton2,
122
+ UsageMeter: () => UsageMeter,
123
+ UsageMeterBar: () => Bar,
124
+ UsageMeterEmpty: () => Empty,
125
+ UsageMeterLabel: () => Label,
126
+ UsageMeterLoading: () => Loading7,
127
+ UsageMeterPercentage: () => Percentage,
128
+ UsageMeterResetsIn: () => ResetsIn,
129
+ UsageMeterRoot: () => Root11,
112
130
  composeEventHandlers: () => composeEventHandlers,
113
131
  composeRefs: () => composeRefs,
114
132
  setRef: () => setRef,
@@ -118,9 +136,11 @@ __export(primitives_exports, {
118
136
  useCancelledPlanNotice: () => useCancelledPlanNotice,
119
137
  useCheckoutSummary: () => useCheckoutSummary,
120
138
  useCreditGate: () => useCreditGate,
139
+ usePaywallNotice: () => usePaywallNotice,
121
140
  usePlanSelector: () => usePlanSelector,
122
141
  usePurchaseGate: () => usePurchaseGate,
123
- useTopupForm: () => useTopupForm
142
+ useTopupForm: () => useTopupForm,
143
+ useUsageMeter: () => useUsageMeter
124
144
  });
125
145
  module.exports = __toCommonJS(primitives_exports);
126
146
 
@@ -250,6 +270,11 @@ var import_react4 = require("react");
250
270
  // src/SolvaPayProvider.tsx
251
271
  var import_react3 = require("react");
252
272
 
273
+ // src/utils/purchases.ts
274
+ function isPlanPurchase(purchase) {
275
+ return !!purchase.planSnapshot && purchase.metadata?.purpose !== "credit_topup";
276
+ }
277
+
253
278
  // src/adapters/auth.ts
254
279
  var defaultAuthAdapter = {
255
280
  async getToken() {
@@ -280,16 +305,6 @@ function getAuthAdapter(config) {
280
305
  if (config?.auth?.adapter) {
281
306
  return config.auth.adapter;
282
307
  }
283
- if (config?.auth?.getToken || config?.auth?.getUserId) {
284
- return {
285
- async getToken() {
286
- return config?.auth?.getToken?.() || null;
287
- },
288
- async getUserId() {
289
- return config?.auth?.getUserId?.() || null;
290
- }
291
- };
292
- }
293
308
  return defaultAuthAdapter;
294
309
  }
295
310
  function getCachedCustomerRef(userId) {
@@ -340,6 +355,167 @@ async function buildRequestHeaders(config) {
340
355
  return { headers, userId };
341
356
  }
342
357
 
358
+ // src/transport/http.ts
359
+ async function request(config, url, opts) {
360
+ const { headers } = await buildRequestHeaders(config);
361
+ const fetchFn = config?.fetch || fetch;
362
+ const init = { method: opts.method, headers };
363
+ if (opts.body !== void 0) {
364
+ init.body = JSON.stringify(opts.body);
365
+ }
366
+ const res = await fetchFn(url, init);
367
+ if (!res.ok) {
368
+ let serverMessage;
369
+ try {
370
+ const data = await res.clone().json();
371
+ serverMessage = data?.error;
372
+ } catch {
373
+ }
374
+ const error = new Error(serverMessage || `${opts.errorPrefix}: ${res.statusText || res.status}`);
375
+ config?.onError?.(error, opts.onErrorContext);
376
+ throw error;
377
+ }
378
+ return await res.json();
379
+ }
380
+ var DEFAULT_ROUTES = {
381
+ checkPurchase: "/api/check-purchase",
382
+ createPayment: "/api/create-payment-intent",
383
+ processPayment: "/api/process-payment",
384
+ createTopupPayment: "/api/create-topup-payment-intent",
385
+ customerBalance: "/api/customer-balance",
386
+ cancelRenewal: "/api/cancel-renewal",
387
+ reactivateRenewal: "/api/reactivate-renewal",
388
+ activatePlan: "/api/activate-plan",
389
+ createCheckoutSession: "/api/create-checkout-session",
390
+ createCustomerSession: "/api/create-customer-session",
391
+ getMerchant: "/api/merchant",
392
+ getProduct: "/api/get-product",
393
+ listPlans: "/api/list-plans",
394
+ getPaymentMethod: "/api/payment-method",
395
+ getUsage: "/api/usage"
396
+ };
397
+ function routeFor(config, key) {
398
+ const configured = config?.api?.[key];
399
+ return configured || DEFAULT_ROUTES[key];
400
+ }
401
+ function createHttpTransport(config) {
402
+ return {
403
+ checkPurchase: () => request(config, routeFor(config, "checkPurchase"), {
404
+ method: "GET",
405
+ onErrorContext: "checkPurchase",
406
+ errorPrefix: "Failed to check purchase"
407
+ }),
408
+ createPayment: (params) => {
409
+ const body = {};
410
+ if (params.planRef) body.planRef = params.planRef;
411
+ if (params.productRef) body.productRef = params.productRef;
412
+ if (params.customer && (params.customer.name || params.customer.email)) {
413
+ body.customer = params.customer;
414
+ }
415
+ return request(config, routeFor(config, "createPayment"), {
416
+ method: "POST",
417
+ body,
418
+ onErrorContext: "createPayment",
419
+ errorPrefix: "Failed to create payment"
420
+ });
421
+ },
422
+ processPayment: (params) => request(config, routeFor(config, "processPayment"), {
423
+ method: "POST",
424
+ body: params,
425
+ onErrorContext: "processPayment",
426
+ errorPrefix: "Failed to process payment"
427
+ }),
428
+ createTopupPayment: (params) => request(config, routeFor(config, "createTopupPayment"), {
429
+ method: "POST",
430
+ body: { amount: params.amount, currency: params.currency },
431
+ onErrorContext: "createTopupPayment",
432
+ errorPrefix: "Failed to create topup payment"
433
+ }),
434
+ getBalance: () => request(config, routeFor(config, "customerBalance"), {
435
+ method: "GET",
436
+ onErrorContext: "getBalance",
437
+ errorPrefix: "Failed to fetch balance"
438
+ }),
439
+ cancelRenewal: (params) => request(config, routeFor(config, "cancelRenewal"), {
440
+ method: "POST",
441
+ body: params,
442
+ onErrorContext: "cancelRenewal",
443
+ errorPrefix: "Failed to cancel renewal"
444
+ }),
445
+ reactivateRenewal: (params) => request(config, routeFor(config, "reactivateRenewal"), {
446
+ method: "POST",
447
+ body: params,
448
+ onErrorContext: "reactivateRenewal",
449
+ errorPrefix: "Failed to reactivate renewal"
450
+ }),
451
+ activatePlan: (params) => request(config, routeFor(config, "activatePlan"), {
452
+ method: "POST",
453
+ body: params,
454
+ onErrorContext: "activatePlan",
455
+ errorPrefix: "Failed to activate plan"
456
+ }),
457
+ createCheckoutSession: (params) => {
458
+ const body = {};
459
+ if (params?.planRef) body.planRef = params.planRef;
460
+ if (params?.productRef) body.productRef = params.productRef;
461
+ if (params?.returnUrl) body.returnUrl = params.returnUrl;
462
+ return request(
463
+ config,
464
+ routeFor(config, "createCheckoutSession"),
465
+ {
466
+ method: "POST",
467
+ body,
468
+ onErrorContext: "createCheckoutSession",
469
+ errorPrefix: "Failed to create checkout session"
470
+ }
471
+ );
472
+ },
473
+ createCustomerSession: () => request(
474
+ config,
475
+ routeFor(config, "createCustomerSession"),
476
+ {
477
+ method: "POST",
478
+ body: {},
479
+ onErrorContext: "createCustomerSession",
480
+ errorPrefix: "Failed to create customer session"
481
+ }
482
+ ),
483
+ getMerchant: () => request(config, routeFor(config, "getMerchant"), {
484
+ method: "GET",
485
+ onErrorContext: "getMerchant",
486
+ errorPrefix: "Failed to fetch merchant"
487
+ }),
488
+ getProduct: (productRef) => {
489
+ const base = routeFor(config, "getProduct");
490
+ const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
491
+ return request(config, url, {
492
+ method: "GET",
493
+ onErrorContext: "getProduct",
494
+ errorPrefix: "Failed to fetch product"
495
+ });
496
+ },
497
+ listPlans: (productRef) => {
498
+ const base = routeFor(config, "listPlans");
499
+ const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
500
+ return request(config, url, {
501
+ method: "GET",
502
+ onErrorContext: "listPlans",
503
+ errorPrefix: "Failed to list plans"
504
+ });
505
+ },
506
+ getPaymentMethod: () => request(config, routeFor(config, "getPaymentMethod"), {
507
+ method: "GET",
508
+ onErrorContext: "getPaymentMethod",
509
+ errorPrefix: "Failed to load payment method"
510
+ }),
511
+ getUsage: () => request(config, routeFor(config, "getUsage"), {
512
+ method: "GET",
513
+ onErrorContext: "getUsage",
514
+ errorPrefix: "Failed to load usage"
515
+ })
516
+ };
517
+ }
518
+
343
519
  // src/i18n/context.tsx
344
520
  var import_react2 = require("react");
345
521
 
@@ -489,6 +665,26 @@ var enCopy = {
489
665
  lowBalanceSubheading: "Top up to continue using {product}",
490
666
  topUpCta: "Top up now"
491
667
  },
668
+ currentPlan: {
669
+ heading: "Your plan",
670
+ nextBilling: "Next billing: {date}",
671
+ expiresOn: "Expires {date}",
672
+ validIndefinitely: "Valid indefinitely",
673
+ paymentMethod: "{brand} \u2022\u2022\u2022\u2022 {last4}",
674
+ paymentMethodExpires: "expires {month}/{year}",
675
+ noPaymentMethod: "No payment method on file",
676
+ updatePaymentButton: "Update card",
677
+ cycleUnit: {
678
+ weekly: "week",
679
+ monthly: "month",
680
+ quarterly: "3 months",
681
+ yearly: "year"
682
+ }
683
+ },
684
+ customerPortal: {
685
+ launchButton: "Manage billing",
686
+ loadingLabel: "Loading portal\u2026"
687
+ },
492
688
  errors: {
493
689
  paymentInitFailed: "Payment initialization failed",
494
690
  topupInitFailed: "Top-up initialization failed",
@@ -502,7 +698,40 @@ var enCopy = {
502
698
  paymentProcessingFailed: "Payment processing failed. Please try again or contact support.",
503
699
  paymentRequires3ds: "Payment requires additional authentication. Please complete the verification.",
504
700
  paymentProcessingTimeout: "Payment processing timed out \u2014 webhooks may not be configured",
505
- paymentStatusPrefix: "Payment status: {status}"
701
+ paymentConfirmationDelayed: "Payment succeeded but confirmation is taking longer than usual. Refresh in a moment to see your purchase.",
702
+ paymentStatusPrefix: "Payment status: {status}",
703
+ paywallInvalidContent: "Paywall content is missing or malformed.",
704
+ usageLoadFailed: "Failed to load usage"
705
+ },
706
+ paywall: {
707
+ header: "Unlock access",
708
+ paymentRequiredHeading: "Upgrade to continue",
709
+ activationRequiredHeading: "Add credits to continue",
710
+ resolvedHeading: "You can continue",
711
+ productContext: "For {product}",
712
+ balanceLine: "You have {available} credits, need {required}.",
713
+ paymentRequiredMessage: "You've used all your included calls{forProduct}. Choose a plan below to keep going.",
714
+ paymentRequiredMessageRemaining: "Only {remaining} call{pluralSuffix} left{forProduct}. Choose a plan below to keep going.",
715
+ paymentRequiredProductSuffix: " for {product}",
716
+ retryButton: "Continue",
717
+ hostedCheckoutButton: "Open checkout",
718
+ hostedCheckoutLoading: "Loading checkout\u2026"
719
+ },
720
+ usage: {
721
+ header: "Usage",
722
+ percentUsedLabel: "{percent}% used",
723
+ usedLabel: "{used} / {total} {unit}",
724
+ remainingLabel: "{remaining} {unit} remaining",
725
+ unlimitedLabel: "Unlimited",
726
+ resetsInLabel: "Resets in {days} days",
727
+ resetsOnLabel: "Resets {date}",
728
+ loadingLabel: "Loading usage\u2026",
729
+ emptyLabel: "No usage-based plan is active.",
730
+ approachingLimit: "You're approaching your usage limit.",
731
+ atLimit: "You've reached your usage limit.",
732
+ topUpCta: "Add credits",
733
+ upgradeCta: "Upgrade plan",
734
+ refreshCta: "Refresh"
506
735
  }
507
736
  };
508
737
 
@@ -832,27 +1061,41 @@ function usePlan(options) {
832
1061
 
833
1062
  // src/hooks/useProduct.ts
834
1063
  var import_react7 = require("react");
1064
+
1065
+ // src/transport/cache-key.ts
1066
+ var transportIds = /* @__PURE__ */ new WeakMap();
1067
+ var nextTransportId = 0;
1068
+ function transportIdFor(transport) {
1069
+ let id = transportIds.get(transport);
1070
+ if (id === void 0) {
1071
+ id = ++nextTransportId;
1072
+ transportIds.set(transport, id);
1073
+ }
1074
+ return id;
1075
+ }
1076
+ function createTransportCacheKey(config, fallbackRoute, suffix) {
1077
+ const transport = config?.transport;
1078
+ if (transport) {
1079
+ const id = transportIdFor(transport);
1080
+ return suffix ? `transport:${id}:${suffix}` : `transport:${id}`;
1081
+ }
1082
+ return fallbackRoute;
1083
+ }
1084
+
1085
+ // src/hooks/useProduct.ts
835
1086
  var productCache = /* @__PURE__ */ new Map();
836
1087
  var CACHE_DURATION3 = 5 * 60 * 1e3;
837
- function routeFor(config) {
838
- return config?.api?.getProduct || "/api/get-product";
1088
+ function cacheKeyFor(config, productRef) {
1089
+ return createTransportCacheKey(config, productRef, productRef);
839
1090
  }
840
1091
  async function fetchProduct(productRef, config) {
841
- const base = routeFor(config);
842
- const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
843
- const fetchFn = config?.fetch || fetch;
844
- const { headers } = await buildRequestHeaders(config);
845
- const res = await fetchFn(url, { method: "GET", headers });
846
- if (!res.ok) {
847
- const error = new Error(`Failed to fetch product: ${res.statusText || res.status}`);
848
- config?.onError?.(error, "getProduct");
849
- throw error;
850
- }
851
- return await res.json();
1092
+ const transport = config?.transport ?? createHttpTransport(config);
1093
+ if (!transport.getProduct) return null;
1094
+ return transport.getProduct(productRef);
852
1095
  }
853
1096
  function useProduct(productRef) {
854
1097
  const { _config } = useSolvaPay();
855
- const cacheKey = productRef || "";
1098
+ const cacheKey = productRef ? cacheKeyFor(_config, productRef) : "";
856
1099
  const [product, setProduct] = (0, import_react7.useState)(
857
1100
  () => productRef ? productCache.get(cacheKey)?.product ?? null : null
858
1101
  );
@@ -870,7 +1113,8 @@ function useProduct(productRef) {
870
1113
  setError(null);
871
1114
  return;
872
1115
  }
873
- const cached = productCache.get(productRef);
1116
+ const key = cacheKeyFor(_config, productRef);
1117
+ const cached = productCache.get(key);
874
1118
  const now = Date.now();
875
1119
  if (!force && cached?.product && now - cached.timestamp < CACHE_DURATION3) {
876
1120
  setProduct(cached.product);
@@ -895,12 +1139,26 @@ function useProduct(productRef) {
895
1139
  setLoading(true);
896
1140
  setError(null);
897
1141
  const promise = fetchProduct(productRef, _config);
898
- productCache.set(productRef, { product: null, promise, timestamp: now });
1142
+ productCache.set(key, {
1143
+ product: cached?.product ?? null,
1144
+ promise,
1145
+ timestamp: now
1146
+ });
899
1147
  const p = await promise;
900
- productCache.set(productRef, { product: p, promise: null, timestamp: now });
1148
+ if (p === null) {
1149
+ productCache.set(key, {
1150
+ product: cached?.product ?? null,
1151
+ promise: null,
1152
+ timestamp: cached?.timestamp ?? now
1153
+ });
1154
+ setProduct(cached?.product ?? null);
1155
+ setLoading(false);
1156
+ return;
1157
+ }
1158
+ productCache.set(key, { product: p, promise: null, timestamp: now });
901
1159
  setProduct(p);
902
1160
  } catch (err) {
903
- productCache.delete(productRef);
1161
+ productCache.delete(key);
904
1162
  setError(err instanceof Error ? err : new Error("Failed to load product"));
905
1163
  } finally {
906
1164
  setLoading(false);
@@ -949,6 +1207,9 @@ var ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
949
1207
  function getFractionDigits(currency) {
950
1208
  return ZERO_DECIMAL_CURRENCIES.has(currency.toLowerCase()) ? 0 : 2;
951
1209
  }
1210
+ function getMinorUnitsPerMajor(currency) {
1211
+ return getFractionDigits(currency) === 0 ? 1 : 100;
1212
+ }
952
1213
  function toMajorUnits(amountMinor, currency) {
953
1214
  const fractionDigits = getFractionDigits(currency);
954
1215
  return fractionDigits === 0 ? amountMinor : amountMinor / 100;
@@ -958,7 +1219,10 @@ function formatPrice(amountMinor, currency, opts = {}) {
958
1219
  if (amountMinor === 0 && free !== "") {
959
1220
  return free;
960
1221
  }
961
- const fractionDigits = getFractionDigits(currency);
1222
+ const naturalFractionDigits = getFractionDigits(currency);
1223
+ const minorPerMajor = getMinorUnitsPerMajor(currency);
1224
+ const isWhole = amountMinor % minorPerMajor === 0;
1225
+ const fractionDigits = isWhole ? 0 : naturalFractionDigits;
962
1226
  const major = toMajorUnits(amountMinor, currency);
963
1227
  const formatter = new Intl.NumberFormat(locale, {
964
1228
  style: "currency",
@@ -1127,6 +1391,26 @@ function useCheckoutSummary() {
1127
1391
  // src/primitives/PlanSelector.tsx
1128
1392
  var import_react10 = require("react");
1129
1393
 
1394
+ // src/transport/list-plans.ts
1395
+ async function defaultListPlans(productRef, config) {
1396
+ const transport = config?.transport;
1397
+ if (transport) {
1398
+ return transport.listPlans ? transport.listPlans(productRef) : plansCache.get(productRef)?.plans ?? [];
1399
+ }
1400
+ const base = config?.api?.listPlans || "/api/list-plans";
1401
+ const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
1402
+ const fetchFn = config?.fetch || fetch;
1403
+ const { headers } = await buildRequestHeaders(config);
1404
+ const res = await fetchFn(url, { method: "GET", headers });
1405
+ if (!res.ok) {
1406
+ const error = new Error(`Failed to fetch plans: ${res.statusText || res.status}`);
1407
+ config?.onError?.(error, "listPlans");
1408
+ throw error;
1409
+ }
1410
+ const data = await res.json();
1411
+ return data.plans ?? [];
1412
+ }
1413
+
1130
1414
  // src/hooks/usePurchase.ts
1131
1415
  function usePurchase() {
1132
1416
  const { purchase, refetchPurchase } = useSolvaPay();
@@ -1154,20 +1438,6 @@ function useCardContext(part) {
1154
1438
  }
1155
1439
  return ctx;
1156
1440
  }
1157
- async function defaultListPlans(productRef, config) {
1158
- const base = config?.api?.listPlans || "/api/list-plans";
1159
- const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
1160
- const fetchFn = config?.fetch || fetch;
1161
- const { headers } = await buildRequestHeaders(config);
1162
- const res = await fetchFn(url, { method: "GET", headers });
1163
- if (!res.ok) {
1164
- const error = new Error(`Failed to fetch plans: ${res.statusText || res.status}`);
1165
- config?.onError?.(error, "listPlans");
1166
- throw error;
1167
- }
1168
- const data = await res.json();
1169
- return data.plans ?? [];
1170
- }
1171
1441
  var Root2 = (0, import_react10.forwardRef)(function PlanSelectorRoot(props, forwardedRef) {
1172
1442
  const {
1173
1443
  productRef,
@@ -1191,7 +1461,7 @@ var Root2 = (0, import_react10.forwardRef)(function PlanSelectorRoot(props, forw
1191
1461
  () => fetcher ?? ((ref) => defaultListPlans(ref, _config)),
1192
1462
  [fetcher, _config]
1193
1463
  );
1194
- const { plans, selectedPlan, selectPlan, loading, error } = usePlans({
1464
+ const { plans, selectedPlan, selectPlan, setSelectedPlanIndex, loading, error } = usePlans({
1195
1465
  productRef,
1196
1466
  fetcher: effectiveFetcher,
1197
1467
  filter,
@@ -1226,6 +1496,9 @@ var Root2 = (0, import_react10.forwardRef)(function PlanSelectorRoot(props, forw
1226
1496
  },
1227
1497
  [plans, selectPlan, onSelect]
1228
1498
  );
1499
+ const clearSelection = (0, import_react10.useCallback)(() => {
1500
+ setSelectedPlanIndex(-1);
1501
+ }, [setSelectedPlanIndex]);
1229
1502
  const selectedPlanRef = selectedPlan?.reference ?? null;
1230
1503
  const ctx = (0, import_react10.useMemo)(
1231
1504
  () => ({
@@ -1239,7 +1512,8 @@ var Root2 = (0, import_react10.forwardRef)(function PlanSelectorRoot(props, forw
1239
1512
  isCurrent,
1240
1513
  isFree,
1241
1514
  isPopular,
1242
- select
1515
+ select,
1516
+ clearSelection
1243
1517
  }),
1244
1518
  [
1245
1519
  plans,
@@ -1252,7 +1526,8 @@ var Root2 = (0, import_react10.forwardRef)(function PlanSelectorRoot(props, forw
1252
1526
  isCurrent,
1253
1527
  isFree,
1254
1528
  isPopular,
1255
- select
1529
+ select,
1530
+ clearSelection
1256
1531
  ]
1257
1532
  );
1258
1533
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -1333,9 +1608,11 @@ var Card = (0, import_react10.forwardRef)(function PlanSelectorCard({ asChild, o
1333
1608
  });
1334
1609
  var CardName = (0, import_react10.forwardRef)(function PlanSelectorCardName({ asChild, children, ...rest }, forwardedRef) {
1335
1610
  const card = useCardContext("CardName");
1336
- if (!card.plan.name) return null;
1611
+ const fallback = card.plan.requiresPayment === false ? "Free" : card.plan.type === "usage-based" ? "Pay as you go" : card.plan.type === "recurring" ? "Plan" : null;
1612
+ const label = card.plan.name ?? fallback;
1613
+ if (!label && children == null) return null;
1337
1614
  const Comp = asChild ? Slot : "span";
1338
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-name": "", ...rest, children: children ?? card.plan.name });
1615
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-name": "", ...rest, children: children ?? label });
1339
1616
  });
1340
1617
  var CardPrice = (0, import_react10.forwardRef)(function PlanSelectorCardPrice({ asChild, children, ...rest }, forwardedRef) {
1341
1618
  const card = useCardContext("CardPrice");
@@ -1343,14 +1620,25 @@ var CardPrice = (0, import_react10.forwardRef)(function PlanSelectorCardPrice({
1343
1620
  const copy = useCopy();
1344
1621
  const formatted = (0, import_react10.useMemo)(() => {
1345
1622
  if (card.isFree) return copy.planSelector.freeBadge;
1623
+ if (card.plan.type === "usage-based") {
1624
+ const creditsPerUnit = card.plan.creditsPerUnit ?? 1;
1625
+ const perCallMinor = Math.max(1, Math.round(1 / creditsPerUnit));
1626
+ const rate = formatPrice(perCallMinor, card.plan.currency ?? "usd", {
1627
+ locale,
1628
+ free: ""
1629
+ });
1630
+ return `${rate} / call`;
1631
+ }
1346
1632
  return formatPrice(card.plan.price ?? 0, card.plan.currency ?? "usd", {
1347
1633
  locale,
1348
1634
  free: copy.interval.free
1349
1635
  });
1350
1636
  }, [
1351
1637
  card.isFree,
1638
+ card.plan.type,
1352
1639
  card.plan.price,
1353
1640
  card.plan.currency,
1641
+ card.plan.creditsPerUnit,
1354
1642
  locale,
1355
1643
  copy.planSelector.freeBadge,
1356
1644
  copy.interval.free
@@ -1361,11 +1649,22 @@ var CardPrice = (0, import_react10.forwardRef)(function PlanSelectorCardPrice({
1361
1649
  var CardInterval = (0, import_react10.forwardRef)(function PlanSelectorCardInterval({ asChild, children, ...rest }, forwardedRef) {
1362
1650
  const card = useCardContext("CardInterval");
1363
1651
  const copy = useCopy();
1364
- if (card.isFree || !card.plan.interval) return null;
1365
- const text = interpolate(copy.planSelector.perIntervalShort, { interval: card.plan.interval });
1652
+ if (card.isFree || card.plan.type === "usage-based") return null;
1653
+ const rawInterval = card.plan.interval ?? normalizeBillingCycle(card.plan.billingCycle);
1654
+ if (!rawInterval) return null;
1655
+ const text = interpolate(copy.planSelector.perIntervalShort, { interval: rawInterval });
1366
1656
  const Comp = asChild ? Slot : "span";
1367
1657
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-interval": "", ...rest, children: children ?? text });
1368
1658
  });
1659
+ function normalizeBillingCycle(cycle) {
1660
+ if (!cycle) return null;
1661
+ const lc = cycle.toLowerCase();
1662
+ if (lc === "monthly" || lc === "month") return "month";
1663
+ if (lc === "yearly" || lc === "annually" || lc === "annual" || lc === "year") return "year";
1664
+ if (lc === "weekly" || lc === "week") return "week";
1665
+ if (lc === "daily" || lc === "day") return "day";
1666
+ return cycle;
1667
+ }
1369
1668
  var CardBadge = (0, import_react10.forwardRef)(function PlanSelectorCardBadge({ asChild, children, ...rest }, forwardedRef) {
1370
1669
  const card = useCardContext("CardBadge");
1371
1670
  const copy = useCopy();
@@ -1440,8 +1739,11 @@ var stripePromiseCache = /* @__PURE__ */ new Map();
1440
1739
  function getStripeCacheKey(publishableKey, accountId) {
1441
1740
  return accountId ? `${publishableKey}:${accountId}` : publishableKey;
1442
1741
  }
1443
- async function resolvePlanRef(productRef, fetchFn, headers, listPlansRoute) {
1742
+ async function resolvePlanRef(productRef, config) {
1743
+ const listPlansRoute = config?.api?.listPlans || "/api/list-plans";
1444
1744
  const url = `${listPlansRoute}?productRef=${encodeURIComponent(productRef)}`;
1745
+ const fetchFn = config?.fetch || fetch;
1746
+ const { headers } = await buildRequestHeaders(config);
1445
1747
  const res = await fetchFn(url, { method: "GET", headers });
1446
1748
  if (!res.ok) {
1447
1749
  throw new Error(`Failed to fetch plans for product "${productRef}": ${res.statusText}`);
@@ -1492,8 +1794,7 @@ function useCheckout(options) {
1492
1794
  try {
1493
1795
  let effectivePlanRef = planRef;
1494
1796
  if (!effectivePlanRef && productRef) {
1495
- const listPlansRoute = _config?.api?.listPlans || "/api/list-plans";
1496
- effectivePlanRef = await resolvePlanRef(productRef, fetch, {}, listPlansRoute);
1797
+ effectivePlanRef = await resolvePlanRef(productRef, _config);
1497
1798
  setResolvedPlanRef(effectivePlanRef);
1498
1799
  }
1499
1800
  if (!effectivePlanRef) {
@@ -1657,24 +1958,17 @@ var import_react15 = require("react");
1657
1958
  var import_react14 = require("react");
1658
1959
  var merchantCache = /* @__PURE__ */ new Map();
1659
1960
  var CACHE_DURATION4 = 5 * 60 * 1e3;
1660
- function cacheKeyFor(config) {
1661
- return config?.api?.getMerchant || "/api/merchant";
1961
+ function cacheKeyFor2(config) {
1962
+ return createTransportCacheKey(config, config?.api?.getMerchant || "/api/merchant");
1662
1963
  }
1663
1964
  async function fetchMerchant(config) {
1664
- const route = cacheKeyFor(config);
1665
- const fetchFn = config?.fetch || fetch;
1666
- const { headers } = await buildRequestHeaders(config);
1667
- const res = await fetchFn(route, { method: "GET", headers });
1668
- if (!res.ok) {
1669
- const error = new Error(`Failed to fetch merchant: ${res.statusText || res.status}`);
1670
- config?.onError?.(error, "getMerchant");
1671
- throw error;
1672
- }
1673
- return await res.json();
1965
+ const transport = config?.transport ?? createHttpTransport(config);
1966
+ if (!transport.getMerchant) return null;
1967
+ return transport.getMerchant();
1674
1968
  }
1675
1969
  function useMerchant() {
1676
1970
  const { _config } = useSolvaPay();
1677
- const key = cacheKeyFor(_config);
1971
+ const key = cacheKeyFor2(_config);
1678
1972
  const [merchant, setMerchant] = (0, import_react14.useState)(
1679
1973
  () => merchantCache.get(key)?.merchant ?? null
1680
1974
  );
@@ -1710,8 +2004,22 @@ function useMerchant() {
1710
2004
  setLoading(true);
1711
2005
  setError(null);
1712
2006
  const promise = fetchMerchant(_config);
1713
- merchantCache.set(key, { merchant: null, promise, timestamp: now });
2007
+ merchantCache.set(key, {
2008
+ merchant: cached?.merchant ?? null,
2009
+ promise,
2010
+ timestamp: now
2011
+ });
1714
2012
  const m = await promise;
2013
+ if (m === null) {
2014
+ merchantCache.set(key, {
2015
+ merchant: cached?.merchant ?? null,
2016
+ promise: null,
2017
+ timestamp: cached?.timestamp ?? now
2018
+ });
2019
+ setMerchant(cached?.merchant ?? null);
2020
+ setLoading(false);
2021
+ return;
2022
+ }
1715
2023
  merchantCache.set(key, { merchant: m, promise: null, timestamp: now });
1716
2024
  setMerchant(m);
1717
2025
  } catch (err) {
@@ -1824,31 +2132,36 @@ var MandateText = (0, import_react15.forwardRef)(
1824
2132
 
1825
2133
  // src/components/Spinner.tsx
1826
2134
  var import_jsx_runtime10 = require("react/jsx-runtime");
1827
- var Spinner = ({
1828
- className = "",
1829
- size = "md"
1830
- }) => {
1831
- const sizeClasses = {
1832
- sm: "h-4 w-4",
1833
- md: "h-5 w-5",
1834
- lg: "h-6 w-6"
1835
- };
2135
+ var SIZE_PX = { sm: 16, md: 20, lg: 24 };
2136
+ var Spinner = ({ className, size = "md" }) => {
2137
+ const px = SIZE_PX[size];
1836
2138
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1837
2139
  "svg",
1838
2140
  {
1839
- className: `animate-spin ${sizeClasses[size]} ${className}`,
1840
- xmlns: "http://www.w3.org/2000/svg",
1841
- fill: "none",
2141
+ "data-solvapay-spinner": "",
2142
+ className,
2143
+ width: px,
2144
+ height: px,
1842
2145
  viewBox: "0 0 24 24",
1843
- role: "status",
1844
- "aria-busy": "true",
2146
+ fill: "none",
2147
+ "aria-hidden": "true",
1845
2148
  children: [
1846
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
2149
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2150
+ "circle",
2151
+ {
2152
+ cx: "12",
2153
+ cy: "12",
2154
+ r: "10",
2155
+ stroke: "currentColor",
2156
+ strokeWidth: "4",
2157
+ strokeOpacity: "0.25"
2158
+ }
2159
+ ),
1847
2160
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1848
2161
  "path",
1849
2162
  {
1850
- className: "opacity-75",
1851
2163
  fill: "currentColor",
2164
+ fillOpacity: "0.75",
1852
2165
  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"
1853
2166
  }
1854
2167
  )
@@ -1866,6 +2179,13 @@ async function confirmPayment(input) {
1866
2179
  if (!paymentElement) {
1867
2180
  return { status: "error", message: copy.errors.cardElementMissing };
1868
2181
  }
2182
+ const { error: submitError } = await elements.submit();
2183
+ if (submitError) {
2184
+ return {
2185
+ status: "error",
2186
+ message: submitError.message || copy.errors.paymentUnexpected
2187
+ };
2188
+ }
1869
2189
  const { error: error2, paymentIntent: paymentIntent2 } = await stripe.confirmPayment({
1870
2190
  elements,
1871
2191
  clientSecret,
@@ -2180,7 +2500,11 @@ var PaidInner = ({
2180
2500
  const copy = useCopy();
2181
2501
  const customer = useCustomer();
2182
2502
  const { processPayment } = useSolvaPay();
2183
- const { refetch } = usePurchase();
2503
+ const { refetch, hasPaidPurchase } = usePurchase();
2504
+ const hasPaidPurchaseRef = (0, import_react16.useRef)(hasPaidPurchase);
2505
+ (0, import_react16.useEffect)(() => {
2506
+ hasPaidPurchaseRef.current = hasPaidPurchase;
2507
+ }, [hasPaidPurchase]);
2184
2508
  const [elementKind, setElementKind] = (0, import_react16.useState)(
2185
2509
  children ? null : "payment-element"
2186
2510
  );
@@ -2231,14 +2555,34 @@ var PaidInner = ({
2231
2555
  refetchPurchase: refetch,
2232
2556
  copy
2233
2557
  });
2234
- setIsProcessing(false);
2235
2558
  if (reconcileResult.status === "success") {
2236
2559
  const pi = paymentIntent;
2237
2560
  onSuccess?.(pi);
2238
2561
  const paid = { kind: "paid", paymentIntent: pi };
2239
2562
  onResult?.(paid);
2563
+ const CONFIRMATION_TIMEOUT_MS = 1e4;
2564
+ const startedAt = Date.now();
2565
+ let attempt = 0;
2566
+ while (!hasPaidPurchaseRef.current && Date.now() - startedAt < CONFIRMATION_TIMEOUT_MS) {
2567
+ attempt += 1;
2568
+ await new Promise((r) => setTimeout(r, Math.min(500 * attempt, 1500)));
2569
+ if (hasPaidPurchaseRef.current) break;
2570
+ try {
2571
+ await refetch();
2572
+ } catch {
2573
+ }
2574
+ }
2575
+ if (!hasPaidPurchaseRef.current) {
2576
+ const confirmationMsg = copy.errors.paymentConfirmationDelayed;
2577
+ setError(confirmationMsg);
2578
+ setIsProcessing(false);
2579
+ onError?.(new Error(confirmationMsg));
2580
+ return;
2581
+ }
2582
+ setIsProcessing(false);
2240
2583
  return;
2241
2584
  }
2585
+ setIsProcessing(false);
2242
2586
  const msg = reconcileResult.status === "timeout" ? reconcileResult.error.message : copy.errors.paymentProcessingFailed;
2243
2587
  setError(msg);
2244
2588
  onError?.(reconcileResult.error);
@@ -2710,7 +3054,9 @@ function useTopupAmountSelector(options) {
2710
3054
  if (resolvedAmount < minAmount) {
2711
3055
  setError(
2712
3056
  interpolate(copy.topup.minAmount, {
2713
- amount: `${currencySymbol}${minAmount}`
3057
+ amount: formatPrice(minAmount * getMinorUnitsPerMajor(currency), currency, {
3058
+ free: ""
3059
+ })
2714
3060
  })
2715
3061
  );
2716
3062
  return false;
@@ -2718,14 +3064,16 @@ function useTopupAmountSelector(options) {
2718
3064
  if (resolvedAmount > maxAmount) {
2719
3065
  setError(
2720
3066
  interpolate(copy.topup.maxAmount, {
2721
- amount: `${currencySymbol}${maxAmount.toLocaleString()}`
3067
+ amount: formatPrice(maxAmount * getMinorUnitsPerMajor(currency), currency, {
3068
+ free: ""
3069
+ })
2722
3070
  })
2723
3071
  );
2724
3072
  return false;
2725
3073
  }
2726
3074
  setError(null);
2727
3075
  return true;
2728
- }, [resolvedAmount, minAmount, maxAmount, currencySymbol, copy]);
3076
+ }, [resolvedAmount, minAmount, maxAmount, currency, copy]);
2729
3077
  const reset = (0, import_react17.useCallback)(() => {
2730
3078
  setSelectedAmount(null);
2731
3079
  setCustomAmountRaw("");
@@ -2765,15 +3113,31 @@ function usePickerCtx(part) {
2765
3113
  }
2766
3114
  return ctx;
2767
3115
  }
2768
- var Root4 = (0, import_react19.forwardRef)(function AmountPickerRoot({ currency, minAmount, maxAmount, onChange, asChild, children, ...rest }, forwardedRef) {
3116
+ var Root4 = (0, import_react19.forwardRef)(function AmountPickerRoot({
3117
+ currency,
3118
+ minAmount,
3119
+ maxAmount,
3120
+ emit = "major",
3121
+ selector: externalSelector,
3122
+ onChange,
3123
+ asChild,
3124
+ children,
3125
+ ...rest
3126
+ }, forwardedRef) {
2769
3127
  const solva = (0, import_react19.useContext)(SolvaPayContext);
2770
3128
  if (!solva) throw new MissingProviderError("AmountPicker");
2771
- const selector = useTopupAmountSelector({ currency, minAmount, maxAmount });
3129
+ const internalSelector = useTopupAmountSelector({ currency, minAmount, maxAmount });
3130
+ const selector = externalSelector ?? internalSelector;
2772
3131
  const { creditsPerMinorUnit, displayExchangeRate } = useBalance();
3132
+ const minorPerMajor = getMinorUnitsPerMajor(currency);
3133
+ const resolvedAmountMinor = selector.resolvedAmount != null && selector.resolvedAmount > 0 ? Math.round(selector.resolvedAmount * minorPerMajor) : null;
2773
3134
  (0, import_react19.useEffect)(() => {
2774
- onChange?.(selector.resolvedAmount);
2775
- }, [selector.resolvedAmount, onChange]);
2776
- const resolvedAmountMinor = selector.resolvedAmount != null && selector.resolvedAmount > 0 ? Math.round(selector.resolvedAmount * 100) : null;
3135
+ if (emit === "minor") {
3136
+ onChange?.(resolvedAmountMinor);
3137
+ } else {
3138
+ onChange?.(selector.resolvedAmount);
3139
+ }
3140
+ }, [emit, selector.resolvedAmount, resolvedAmountMinor, onChange]);
2777
3141
  const rate = displayExchangeRate ?? 1;
2778
3142
  const estimatedCredits = creditsPerMinorUnit != null && creditsPerMinorUnit > 0 && resolvedAmountMinor != null ? Math.floor(resolvedAmountMinor / rate * creditsPerMinorUnit) : null;
2779
3143
  const isApproximate = rate !== 1;
@@ -2781,12 +3145,23 @@ var Root4 = (0, import_react19.forwardRef)(function AmountPickerRoot({ currency,
2781
3145
  () => ({
2782
3146
  ...selector,
2783
3147
  currency,
3148
+ emit,
2784
3149
  creditsPerMinorUnit,
2785
3150
  displayExchangeRate,
2786
3151
  estimatedCredits,
2787
- isApproximate
3152
+ isApproximate,
3153
+ resolvedAmountMinor
2788
3154
  }),
2789
- [selector, currency, creditsPerMinorUnit, displayExchangeRate, estimatedCredits, isApproximate]
3155
+ [
3156
+ selector,
3157
+ currency,
3158
+ emit,
3159
+ creditsPerMinorUnit,
3160
+ displayExchangeRate,
3161
+ estimatedCredits,
3162
+ isApproximate,
3163
+ resolvedAmountMinor
3164
+ ]
2790
3165
  );
2791
3166
  const Comp = asChild ? Slot : "div";
2792
3167
  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 }) });
@@ -2805,19 +3180,18 @@ var Option = (0, import_react19.forwardRef)(function AmountPickerOption({ asChil
2805
3180
  }),
2806
3181
  ...rest
2807
3182
  };
3183
+ const defaultLabel = formatPrice(
3184
+ amount * getMinorUnitsPerMajor(ctx.currency),
3185
+ ctx.currency,
3186
+ { free: "" }
3187
+ );
2808
3188
  if (asChild) {
2809
3189
  return (
2810
3190
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2811
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
2812
- ctx.currencySymbol,
2813
- amount.toLocaleString()
2814
- ] }) })
3191
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? defaultLabel })
2815
3192
  );
2816
3193
  }
2817
- 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: [
2818
- ctx.currencySymbol,
2819
- amount.toLocaleString()
2820
- ] }) });
3194
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? defaultLabel });
2821
3195
  });
2822
3196
  var Custom = (0, import_react19.forwardRef)(function AmountPickerCustom({ asChild, onChange, onFocus, ...rest }, forwardedRef) {
2823
3197
  const ctx = usePickerCtx("Custom");
@@ -2849,9 +3223,12 @@ var Confirm = (0, import_react19.forwardRef)(function AmountPickerConfirm({ asCh
2849
3223
  const ctx = usePickerCtx("Confirm");
2850
3224
  const isDisabled = disabled || !ctx.resolvedAmount;
2851
3225
  const handleConfirm = (0, import_react19.useCallback)(() => {
2852
- if (ctx.validate() && ctx.resolvedAmount != null) {
2853
- onConfirm?.(ctx.resolvedAmount);
3226
+ if (!ctx.validate()) return;
3227
+ if (ctx.emit === "minor") {
3228
+ if (ctx.resolvedAmountMinor != null) onConfirm?.(ctx.resolvedAmountMinor);
3229
+ return;
2854
3230
  }
3231
+ if (ctx.resolvedAmount != null) onConfirm?.(ctx.resolvedAmount);
2855
3232
  }, [ctx, onConfirm]);
2856
3233
  const commonProps = {
2857
3234
  "data-solvapay-amount-picker-confirm": "",
@@ -2964,16 +3341,19 @@ var Root5 = (0, import_react20.forwardRef)(function ActivationFlowRoot({
2964
3341
  const retryActivation = (0, import_react20.useCallback)(async () => {
2965
3342
  if (!resolvedPlanRef) return;
2966
3343
  setStep("retrying");
2967
- const amountMinor = Math.round((amountSelector.resolvedAmount || 0) * 100);
3344
+ const amountMinor2 = Math.round(
3345
+ (amountSelector.resolvedAmount || 0) * getMinorUnitsPerMajor(currency)
3346
+ );
2968
3347
  const rate = displayExchangeRate ?? 1;
2969
3348
  if (creditsPerMinorUnit) {
2970
- adjustBalance(Math.floor(amountMinor / rate * creditsPerMinorUnit));
3349
+ adjustBalance(Math.floor(amountMinor2 / rate * creditsPerMinorUnit));
2971
3350
  }
2972
3351
  await new Promise((r) => setTimeout(r, retryDelayMs));
2973
3352
  await activate({ productRef, planRef: resolvedPlanRef });
2974
3353
  }, [
2975
3354
  resolvedPlanRef,
2976
3355
  amountSelector.resolvedAmount,
3356
+ currency,
2977
3357
  displayExchangeRate,
2978
3358
  creditsPerMinorUnit,
2979
3359
  adjustBalance,
@@ -3000,7 +3380,9 @@ var Root5 = (0, import_react20.forwardRef)(function ActivationFlowRoot({
3000
3380
  calledSuccessRef.current = false;
3001
3381
  setStep("summary");
3002
3382
  }, [reset]);
3003
- const amountCents = Math.round((amountSelector.resolvedAmount || 0) * 100);
3383
+ const amountMinor = Math.round(
3384
+ (amountSelector.resolvedAmount || 0) * getMinorUnitsPerMajor(currency)
3385
+ );
3004
3386
  const ctx = (0, import_react20.useMemo)(
3005
3387
  () => ({
3006
3388
  step,
@@ -3008,7 +3390,11 @@ var Root5 = (0, import_react20.forwardRef)(function ActivationFlowRoot({
3008
3390
  productRef,
3009
3391
  planRef: resolvedPlanRef ?? "",
3010
3392
  amountSelector,
3011
- amountCents,
3393
+ amountMinor,
3394
+ // Deprecated alias — same value as amountMinor so legacy consumers
3395
+ // keep working after the zero-decimal fix (previously this was
3396
+ // `resolvedAmount * 100`, which over-billed JPY/KRW by 100x).
3397
+ amountCents: amountMinor,
3012
3398
  currency,
3013
3399
  activate: handleActivate,
3014
3400
  reset: resetFlow,
@@ -3024,7 +3410,7 @@ var Root5 = (0, import_react20.forwardRef)(function ActivationFlowRoot({
3024
3410
  productRef,
3025
3411
  resolvedPlanRef,
3026
3412
  amountSelector,
3027
- amountCents,
3413
+ amountMinor,
3028
3414
  currency,
3029
3415
  handleActivate,
3030
3416
  resetFlow,
@@ -3086,7 +3472,7 @@ var ActivateButton = (0, import_react20.forwardRef)(
3086
3472
  var AmountPickerMount = ({ children }) => {
3087
3473
  const ctx = useFlowCtx("AmountPicker");
3088
3474
  if (ctx.step !== "selectAmount") return null;
3089
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { "data-solvapay-activation-flow-amount-picker": "", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(AmountPicker.Root, { currency: ctx.currency, children }) });
3475
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { "data-solvapay-activation-flow-amount-picker": "", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(AmountPicker.Root, { currency: ctx.currency, selector: ctx.amountSelector, children }) });
3090
3476
  };
3091
3477
  var ContinueButton = (0, import_react20.forwardRef)(
3092
3478
  function ActivationFlowContinueButton({ asChild, onClick, children, ...rest }, forwardedRef) {
@@ -3357,7 +3743,7 @@ function usePurchaseStatus() {
3357
3743
  }, []);
3358
3744
  const purchaseData = (0, import_react24.useMemo)(() => {
3359
3745
  const cancelledPaidPurchases = purchases.filter((p) => {
3360
- return p.status === "active" && p.cancelledAt && isPaidPurchase(p);
3746
+ return p.status === "active" && p.cancelledAt && isPaidPurchase(p) && isPlanPurchase(p);
3361
3747
  });
3362
3748
  const cancelledPurchase = cancelledPaidPurchases.sort(
3363
3749
  (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
@@ -3746,6 +4132,14 @@ var Inner = ({
3746
4132
  }
3747
4133
  setError(null);
3748
4134
  setIsProcessing(true);
4135
+ const { error: submitError } = await elements.submit();
4136
+ if (submitError) {
4137
+ const msg = submitError.message || copy.errors.paymentUnexpected;
4138
+ setError(msg);
4139
+ setIsProcessing(false);
4140
+ onError?.(new Error(msg));
4141
+ return;
4142
+ }
3749
4143
  const { error: confirmError, paymentIntent } = await stripe.confirmPayment({
3750
4144
  elements,
3751
4145
  clientSecret,
@@ -4158,6 +4552,654 @@ var ProductBadgeImpl = (0, import_react30.forwardRef)(function ProductBadge({ as
4158
4552
  });
4159
4553
  var ProductBadge2 = ProductBadgeImpl;
4160
4554
  var PlanBadge = ProductBadgeImpl;
4555
+
4556
+ // src/primitives/PaywallNotice.tsx
4557
+ var import_react32 = require("react");
4558
+
4559
+ // src/hooks/usePaywallResolver.ts
4560
+ var import_react31 = require("react");
4561
+ function usePaywallResolver(content) {
4562
+ const { activePurchase, hasPaidPurchase, refetch: refetchPurchase } = usePurchase();
4563
+ const { credits, refetch: refetchBalance } = useBalance();
4564
+ const resolved = (0, import_react31.useMemo)(() => {
4565
+ if (!content) return false;
4566
+ const productMatches = !content.product || activePurchase?.productRef === content.product || activePurchase?.productName === content.product;
4567
+ if (content.kind === "payment_required") {
4568
+ return Boolean(hasPaidPurchase && productMatches);
4569
+ }
4570
+ const balance = content.balance;
4571
+ if (balance && typeof balance.remainingUnits === "number" && balance.remainingUnits > 0) {
4572
+ return true;
4573
+ }
4574
+ if (balance && credits != null && balance.creditsPerUnit && credits >= balance.creditsPerUnit) {
4575
+ return true;
4576
+ }
4577
+ return Boolean(productMatches && activePurchase?.status === "active");
4578
+ }, [content, hasPaidPurchase, activePurchase, credits]);
4579
+ const refetch = (0, import_react31.useCallback)(async () => {
4580
+ await Promise.all([
4581
+ refetchPurchase().catch(() => void 0),
4582
+ refetchBalance().catch(() => void 0)
4583
+ ]);
4584
+ }, [refetchPurchase, refetchBalance]);
4585
+ return { resolved, refetch };
4586
+ }
4587
+
4588
+ // src/utils/isPayg.ts
4589
+ function isPaygPlan(plan) {
4590
+ if (!plan) return false;
4591
+ const type = plan.planType ?? plan.type;
4592
+ return type === "usage-based" || type === "hybrid";
4593
+ }
4594
+
4595
+ // src/primitives/PaywallNotice.tsx
4596
+ var import_jsx_runtime22 = require("react/jsx-runtime");
4597
+ var PaywallNoticeContext = (0, import_react32.createContext)(null);
4598
+ function usePaywallNoticeCtx(part) {
4599
+ const ctx = (0, import_react32.useContext)(PaywallNoticeContext);
4600
+ if (!ctx) {
4601
+ throw new Error(`PaywallNotice.${part} must be rendered inside <PaywallNotice.Root>.`);
4602
+ }
4603
+ return ctx;
4604
+ }
4605
+ var Root10 = (0, import_react32.forwardRef)(function PaywallNoticeRoot({ content, onResolved, classNames, asChild, children, ...rest }, forwardedRef) {
4606
+ const { resolved, refetch } = usePaywallResolver(content);
4607
+ const classNamesResolved = (0, import_react32.useMemo)(() => classNames ?? {}, [classNames]);
4608
+ (0, import_react32.useEffect)(() => {
4609
+ if (resolved) onResolved?.();
4610
+ }, [resolved, onResolved]);
4611
+ const ctx = (0, import_react32.useMemo)(
4612
+ () => ({
4613
+ content,
4614
+ resolved,
4615
+ refetch,
4616
+ onResolved,
4617
+ classNames: classNamesResolved
4618
+ }),
4619
+ [content, resolved, refetch, onResolved, classNamesResolved]
4620
+ );
4621
+ const Comp = asChild ? Slot : "div";
4622
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PaywallNoticeContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4623
+ Comp,
4624
+ {
4625
+ ref: forwardedRef,
4626
+ "data-solvapay-paywall-notice": "",
4627
+ "data-kind": content.kind,
4628
+ "data-state": resolved ? "resolved" : "pending",
4629
+ className: classNamesResolved.root,
4630
+ ...rest,
4631
+ children
4632
+ }
4633
+ ) });
4634
+ });
4635
+ var Heading4 = (0, import_react32.forwardRef)(function PaywallNoticeHeading({ asChild, children, className, ...rest }, forwardedRef) {
4636
+ const ctx = usePaywallNoticeCtx("Heading");
4637
+ const copy = useCopy();
4638
+ const defaultText = ctx.resolved ? copy.paywall.resolvedHeading : ctx.content.kind === "payment_required" ? copy.paywall.paymentRequiredHeading : copy.paywall.activationRequiredHeading;
4639
+ const Comp = asChild ? Slot : "h2";
4640
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4641
+ Comp,
4642
+ {
4643
+ ref: forwardedRef,
4644
+ "data-solvapay-paywall-heading": "",
4645
+ className: className ?? ctx.classNames.heading,
4646
+ ...rest,
4647
+ children: children ?? defaultText
4648
+ }
4649
+ );
4650
+ });
4651
+ var Message = (0, import_react32.forwardRef)(function PaywallNoticeMessage({ asChild, children, className, ...rest }, forwardedRef) {
4652
+ const ctx = usePaywallNoticeCtx("Message");
4653
+ const copy = useCopy();
4654
+ const Comp = asChild ? Slot : "p";
4655
+ const defaultText = resolvePaywallMessage(ctx.content, copy.paywall);
4656
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4657
+ Comp,
4658
+ {
4659
+ ref: forwardedRef,
4660
+ "data-solvapay-paywall-message": "",
4661
+ className: className ?? ctx.classNames.message,
4662
+ ...rest,
4663
+ children: children ?? defaultText
4664
+ }
4665
+ );
4666
+ });
4667
+ function resolvePaywallMessage(content, paywallCopy) {
4668
+ if (content.kind !== "payment_required") {
4669
+ return content.message;
4670
+ }
4671
+ const balance = content.balance;
4672
+ if (!balance) return content.message;
4673
+ const productName = content.productDetails?.name;
4674
+ const forProduct = productName ? interpolate(paywallCopy.paymentRequiredProductSuffix, { product: productName }) : "";
4675
+ const remaining = balance.remainingUnits ?? 0;
4676
+ if (remaining <= 0) {
4677
+ return interpolate(paywallCopy.paymentRequiredMessage, { forProduct });
4678
+ }
4679
+ return interpolate(paywallCopy.paymentRequiredMessageRemaining, {
4680
+ remaining: String(remaining),
4681
+ pluralSuffix: remaining === 1 ? "" : "s",
4682
+ forProduct
4683
+ });
4684
+ }
4685
+ var ProductContext = (0, import_react32.forwardRef)(function PaywallNoticeProductContext({ asChild, children, className, ...rest }, forwardedRef) {
4686
+ const ctx = usePaywallNoticeCtx("ProductContext");
4687
+ const copy = useCopy();
4688
+ const product = ctx.content.kind === "activation_required" ? ctx.content.productDetails : void 0;
4689
+ if (!product?.name) return null;
4690
+ const label = interpolate(copy.paywall.productContext, { product: product.name });
4691
+ const Comp = asChild ? Slot : "div";
4692
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4693
+ Comp,
4694
+ {
4695
+ ref: forwardedRef,
4696
+ "data-solvapay-paywall-product-context": "",
4697
+ className: className ?? ctx.classNames.productContext,
4698
+ ...rest,
4699
+ children: children ?? label
4700
+ }
4701
+ );
4702
+ });
4703
+ var Balance = (0, import_react32.forwardRef)(function PaywallNoticeBalance({ asChild, children, className, ...rest }, forwardedRef) {
4704
+ const ctx = usePaywallNoticeCtx("Balance");
4705
+ const copy = useCopy();
4706
+ const balance = ctx.content.balance;
4707
+ if (ctx.content.kind !== "activation_required" || !balance) return null;
4708
+ const label = interpolate(copy.paywall.balanceLine, {
4709
+ available: String(balance.remainingUnits ?? 0),
4710
+ required: String(balance.creditsPerUnit ?? 1)
4711
+ });
4712
+ const Comp = asChild ? Slot : "div";
4713
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4714
+ Comp,
4715
+ {
4716
+ ref: forwardedRef,
4717
+ "data-solvapay-paywall-balance": "",
4718
+ className: className ?? ctx.classNames.balance,
4719
+ ...rest,
4720
+ children: children ?? label
4721
+ }
4722
+ );
4723
+ });
4724
+ function Plans({ className, children }) {
4725
+ const ctx = usePaywallNoticeCtx("Plans");
4726
+ if (ctx.content.kind !== "activation_required") return null;
4727
+ if (!ctx.content.product) return null;
4728
+ const defaultClass = className ?? ctx.classNames.plans ?? "solvapay-paywall-plans";
4729
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { "data-solvapay-paywall-plans": "", className: defaultClass, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.Root, { productRef: ctx.content.product, filter: hidesFreePlan, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
4730
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.Grid, { children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(PlanSelector.Card, { children: [
4731
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.CardBadge, {}),
4732
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.CardName, {}),
4733
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.CardPrice, {}),
4734
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.CardInterval, {})
4735
+ ] }) }),
4736
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.Loading, {}),
4737
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.Error, {})
4738
+ ] }) }) });
4739
+ }
4740
+ function hidesFreePlan(plan) {
4741
+ return plan.requiresPayment !== false;
4742
+ }
4743
+ var HostedCheckoutLink = (0, import_react32.forwardRef)(function PaywallNoticeHostedCheckoutLink({ asChild, children, className, ...rest }, forwardedRef) {
4744
+ const ctx = usePaywallNoticeCtx("HostedCheckoutLink");
4745
+ const copy = useCopy();
4746
+ const href = ctx.content.checkoutUrl;
4747
+ if (!href) return null;
4748
+ const Comp = asChild ? Slot : "a";
4749
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4750
+ Comp,
4751
+ {
4752
+ ref: forwardedRef,
4753
+ href,
4754
+ target: "_blank",
4755
+ rel: "noopener noreferrer",
4756
+ "data-solvapay-paywall-hosted-link": "",
4757
+ className: className ?? ctx.classNames.hostedLink,
4758
+ ...rest,
4759
+ children: children ?? copy.paywall.hostedCheckoutButton
4760
+ }
4761
+ );
4762
+ });
4763
+ function EmbeddedCheckout({
4764
+ returnUrl,
4765
+ className,
4766
+ children
4767
+ }) {
4768
+ const ctx = usePaywallNoticeCtx("EmbeddedCheckout");
4769
+ if (!ctx.content.product) return null;
4770
+ const defaultClass = className ?? ctx.classNames.embeddedCheckout ?? "solvapay-paywall-embedded-checkout";
4771
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { "data-solvapay-paywall-embedded-checkout": "", className: defaultClass, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(PlanSelector.Root, { productRef: ctx.content.product, filter: hidesFreePlan, children: [
4772
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.Grid, { children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(PlanSelector.Card, { children: [
4773
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.CardBadge, {}),
4774
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.CardName, {}),
4775
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.CardPrice, {}),
4776
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.CardInterval, {})
4777
+ ] }) }),
4778
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.Loading, {}),
4779
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PlanSelector.Error, {}),
4780
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PaywallSelectedPlanGate, { productRef: ctx.content.product, returnUrl, children })
4781
+ ] }) });
4782
+ }
4783
+ function PaywallSelectedPlanGate({
4784
+ productRef,
4785
+ returnUrl,
4786
+ children
4787
+ }) {
4788
+ const { selectedPlan, selectedPlanRef } = usePlanSelector();
4789
+ if (!selectedPlanRef || !selectedPlan) return null;
4790
+ if (isPaygPlan(selectedPlan)) {
4791
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4792
+ PaywallPaygGate,
4793
+ {
4794
+ plan: selectedPlan,
4795
+ returnUrl
4796
+ },
4797
+ selectedPlanRef
4798
+ );
4799
+ }
4800
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PaywallPaymentFormGate, { productRef, returnUrl, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
4801
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PaymentForm.Summary, {}),
4802
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PaymentForm.Loading, {}),
4803
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PaymentForm.PaymentElement, {}),
4804
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PaymentForm.Error, {}),
4805
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PaymentForm.MandateText, {}),
4806
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PaymentForm.SubmitButton, {})
4807
+ ] }) });
4808
+ }
4809
+ function PaywallPaygGate({
4810
+ plan,
4811
+ returnUrl
4812
+ }) {
4813
+ const ctx = usePaywallNoticeCtx("EmbeddedCheckout");
4814
+ const currency = (plan.currency ?? "USD").toUpperCase();
4815
+ const [amountMinor, setAmountMinor] = (0, import_react32.useState)(null);
4816
+ if (amountMinor == null) {
4817
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { "data-solvapay-paywall-payg-amount": "", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(AmountPicker.Root, { currency, emit: "minor", children: [
4818
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(AmountPicker.Custom, {}),
4819
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(AmountPicker.Confirm, { onConfirm: (minor) => setAmountMinor(minor), children: "Continue" })
4820
+ ] }) });
4821
+ }
4822
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { "data-solvapay-paywall-payg-topup": "", children: [
4823
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4824
+ "button",
4825
+ {
4826
+ type: "button",
4827
+ "data-solvapay-paywall-payg-change-amount": "",
4828
+ onClick: () => setAmountMinor(null),
4829
+ children: "Change amount"
4830
+ }
4831
+ ),
4832
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
4833
+ TopupForm.Root,
4834
+ {
4835
+ amount: amountMinor,
4836
+ currency,
4837
+ returnUrl,
4838
+ onSuccess: () => {
4839
+ void ctx.refetch();
4840
+ },
4841
+ children: [
4842
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TopupForm.Loading, {}),
4843
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TopupForm.PaymentElement, {}),
4844
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TopupForm.Error, {}),
4845
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TopupForm.SubmitButton, {})
4846
+ ]
4847
+ }
4848
+ )
4849
+ ] });
4850
+ }
4851
+ function PaywallPaymentFormGate({
4852
+ productRef,
4853
+ returnUrl,
4854
+ children
4855
+ }) {
4856
+ const { selectedPlanRef } = usePlanSelector();
4857
+ const ctx = usePaywallNoticeCtx("EmbeddedCheckout");
4858
+ if (!selectedPlanRef) return null;
4859
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4860
+ PaymentForm.Root,
4861
+ {
4862
+ planRef: selectedPlanRef,
4863
+ productRef,
4864
+ returnUrl,
4865
+ requireTermsAcceptance: false,
4866
+ onSuccess: () => {
4867
+ void ctx.refetch();
4868
+ },
4869
+ children
4870
+ },
4871
+ selectedPlanRef
4872
+ );
4873
+ }
4874
+ var Retry = (0, import_react32.forwardRef)(
4875
+ function PaywallNoticeRetry({ asChild, children, className, onClick, ...rest }, forwardedRef) {
4876
+ const ctx = usePaywallNoticeCtx("Retry");
4877
+ const copy = useCopy();
4878
+ const Comp = asChild ? Slot : "button";
4879
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4880
+ Comp,
4881
+ {
4882
+ ref: forwardedRef,
4883
+ type: "button",
4884
+ disabled: !ctx.resolved,
4885
+ "data-solvapay-paywall-retry": "",
4886
+ "data-state": ctx.resolved ? "ready" : "waiting",
4887
+ className: className ?? ctx.classNames.retryButton,
4888
+ onClick: (e) => {
4889
+ onClick?.(e);
4890
+ if (!e.defaultPrevented && ctx.resolved) ctx.onResolved?.();
4891
+ },
4892
+ ...rest,
4893
+ children: children ?? copy.paywall.retryButton
4894
+ }
4895
+ );
4896
+ }
4897
+ );
4898
+ var PaywallNotice = Object.assign(Root10, {
4899
+ Root: Root10,
4900
+ Heading: Heading4,
4901
+ Message,
4902
+ ProductContext,
4903
+ Plans,
4904
+ Balance,
4905
+ HostedCheckoutLink,
4906
+ EmbeddedCheckout,
4907
+ Retry
4908
+ });
4909
+ function usePaywallNotice() {
4910
+ return usePaywallNoticeCtx("usePaywallNotice");
4911
+ }
4912
+
4913
+ // src/primitives/UsageMeter.tsx
4914
+ var import_react35 = require("react");
4915
+
4916
+ // src/hooks/useUsage.ts
4917
+ var import_react34 = require("react");
4918
+
4919
+ // src/hooks/useTransport.ts
4920
+ var import_react33 = require("react");
4921
+ function useTransport() {
4922
+ const { _config } = useSolvaPay();
4923
+ return (0, import_react33.useMemo)(() => _config?.transport ?? createHttpTransport(_config), [_config]);
4924
+ }
4925
+
4926
+ // src/hooks/useUsage.ts
4927
+ function deriveUsage(purchase) {
4928
+ if (!purchase) return null;
4929
+ const snap = purchase.planSnapshot;
4930
+ const usage = purchase.usage;
4931
+ const meterRef = snap?.meterRef ?? null;
4932
+ const total = typeof snap?.limit === "number" ? snap.limit : null;
4933
+ if (meterRef === null && !usage) return null;
4934
+ const used = typeof usage?.used === "number" ? usage.used : 0;
4935
+ const remaining = total !== null ? Math.max(0, total - used) : null;
4936
+ const percentUsed = total !== null && total > 0 ? Math.min(100, Math.round(used / total * 1e4) / 100) : null;
4937
+ return {
4938
+ meterRef,
4939
+ total,
4940
+ used,
4941
+ remaining,
4942
+ percentUsed,
4943
+ ...usage?.periodStart ? { periodStart: usage.periodStart } : {},
4944
+ ...usage?.periodEnd ? { periodEnd: usage.periodEnd } : {},
4945
+ ...purchase.reference ? { purchaseRef: purchase.reference } : {}
4946
+ };
4947
+ }
4948
+ function useUsage() {
4949
+ const { activePurchase, refetch: refetchPurchase, loading: purchaseLoading } = usePurchase();
4950
+ const transport = useTransport();
4951
+ const [override, setOverride] = (0, import_react34.useState)(null);
4952
+ const [error, setError] = (0, import_react34.useState)(null);
4953
+ const [transportLoading, setTransportLoading] = (0, import_react34.useState)(false);
4954
+ const derived = (0, import_react34.useMemo)(() => deriveUsage(activePurchase ?? null), [activePurchase]);
4955
+ const activePurchaseRef = activePurchase?.reference ?? null;
4956
+ (0, import_react34.useEffect)(() => {
4957
+ setOverride(null);
4958
+ }, [activePurchaseRef]);
4959
+ const usage = override ?? derived;
4960
+ const refetch = (0, import_react34.useCallback)(async () => {
4961
+ setError(null);
4962
+ if (typeof transport.getUsage === "function") {
4963
+ setTransportLoading(true);
4964
+ try {
4965
+ const next = await transport.getUsage();
4966
+ setOverride(next);
4967
+ } catch (err) {
4968
+ setError(err instanceof Error ? err : new Error("Failed to load usage"));
4969
+ } finally {
4970
+ setTransportLoading(false);
4971
+ }
4972
+ return;
4973
+ }
4974
+ try {
4975
+ await refetchPurchase();
4976
+ } catch (err) {
4977
+ setError(err instanceof Error ? err : new Error("Failed to refetch purchase"));
4978
+ }
4979
+ }, [transport, refetchPurchase]);
4980
+ const percentUsed = usage?.percentUsed ?? null;
4981
+ const isApproachingLimit = percentUsed !== null && percentUsed >= 80 && percentUsed < 100;
4982
+ const isAtLimit = percentUsed !== null && percentUsed >= 100;
4983
+ const isUnlimited = usage !== null && usage.total === null;
4984
+ return {
4985
+ usage,
4986
+ loading: purchaseLoading || transportLoading,
4987
+ error,
4988
+ refetch,
4989
+ percentUsed,
4990
+ isApproachingLimit,
4991
+ isAtLimit,
4992
+ isUnlimited,
4993
+ meterRef: usage?.meterRef ?? null
4994
+ };
4995
+ }
4996
+
4997
+ // src/primitives/UsageMeter.tsx
4998
+ var import_jsx_runtime23 = require("react/jsx-runtime");
4999
+ var UsageMeterContext = (0, import_react35.createContext)(null);
5000
+ function useUsageMeterCtx(part) {
5001
+ const ctx = (0, import_react35.useContext)(UsageMeterContext);
5002
+ if (!ctx) {
5003
+ throw new Error(`UsageMeter.${part} must be rendered inside <UsageMeter.Root>.`);
5004
+ }
5005
+ return ctx;
5006
+ }
5007
+ var Root11 = (0, import_react35.forwardRef)(function UsageMeterRoot({
5008
+ warningAt = 75,
5009
+ criticalAt = 90,
5010
+ classNames,
5011
+ asChild,
5012
+ usageOverride,
5013
+ children,
5014
+ className,
5015
+ ...rest
5016
+ }, forwardedRef) {
5017
+ const hookResult = useUsage();
5018
+ const usage = usageOverride !== void 0 ? usageOverride : hookResult.usage;
5019
+ const loading = hookResult.loading && !usage;
5020
+ const percentUsed = usage?.percentUsed ?? null;
5021
+ const state = loading ? "loading" : percentUsed === null ? "safe" : percentUsed >= criticalAt ? "critical" : percentUsed >= warningAt ? "warning" : "safe";
5022
+ const ctx = (0, import_react35.useMemo)(
5023
+ () => ({
5024
+ usage,
5025
+ loading,
5026
+ error: hookResult.error,
5027
+ percentUsed,
5028
+ isApproachingLimit: hookResult.isApproachingLimit,
5029
+ isAtLimit: hookResult.isAtLimit,
5030
+ isUnlimited: hookResult.isUnlimited,
5031
+ state,
5032
+ warningAt,
5033
+ criticalAt
5034
+ }),
5035
+ [
5036
+ usage,
5037
+ loading,
5038
+ hookResult.error,
5039
+ percentUsed,
5040
+ hookResult.isApproachingLimit,
5041
+ hookResult.isAtLimit,
5042
+ hookResult.isUnlimited,
5043
+ state,
5044
+ warningAt,
5045
+ criticalAt
5046
+ ]
5047
+ );
5048
+ const rootClass = [classNames?.root ?? "solvapay-usage-meter", className].filter(Boolean).join(" ");
5049
+ const Comp = asChild ? Slot : "div";
5050
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(UsageMeterContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5051
+ Comp,
5052
+ {
5053
+ ref: forwardedRef,
5054
+ "data-solvapay-usage-meter": "",
5055
+ "data-state": state,
5056
+ className: rootClass,
5057
+ ...rest,
5058
+ children
5059
+ }
5060
+ ) });
5061
+ });
5062
+ var Bar = (0, import_react35.forwardRef)(function UsageMeterBar({ className, style, ...rest }, forwardedRef) {
5063
+ const ctx = useUsageMeterCtx("Bar");
5064
+ const width = ctx.percentUsed === null ? 0 : Math.max(0, Math.min(100, ctx.percentUsed));
5065
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5066
+ "div",
5067
+ {
5068
+ ref: forwardedRef,
5069
+ "data-solvapay-usage-meter-bar": "",
5070
+ "data-state": ctx.state,
5071
+ role: "progressbar",
5072
+ "aria-valuenow": Math.round(width),
5073
+ "aria-valuemin": 0,
5074
+ "aria-valuemax": 100,
5075
+ className: className ?? "solvapay-usage-meter-bar",
5076
+ style: {
5077
+ // Expose as a CSS custom property so default styles can drive
5078
+ // `width: var(--solvapay-usage-meter-fill)` without inline styling.
5079
+ ["--solvapay-usage-meter-fill"]: `${width}%`,
5080
+ ...style
5081
+ },
5082
+ ...rest
5083
+ }
5084
+ );
5085
+ });
5086
+ var Label = (0, import_react35.forwardRef)(function UsageMeterLabel({ className, children, ...rest }, forwardedRef) {
5087
+ const ctx = useUsageMeterCtx("Label");
5088
+ const copy = useCopy();
5089
+ if (!ctx.usage) return null;
5090
+ if (ctx.isUnlimited) {
5091
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5092
+ "span",
5093
+ {
5094
+ ref: forwardedRef,
5095
+ "data-solvapay-usage-meter-label": "",
5096
+ className: className ?? "solvapay-usage-meter-label",
5097
+ ...rest,
5098
+ children: children ?? copy.usage.unlimitedLabel
5099
+ }
5100
+ );
5101
+ }
5102
+ const unit = ctx.usage.meterRef ?? "units";
5103
+ const label = ctx.usage.total !== null ? interpolate(copy.usage.usedLabel, {
5104
+ used: String(ctx.usage.used),
5105
+ total: String(ctx.usage.total),
5106
+ unit
5107
+ }) : interpolate(copy.usage.remainingLabel, {
5108
+ remaining: String(ctx.usage.remaining ?? 0),
5109
+ unit
5110
+ });
5111
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5112
+ "span",
5113
+ {
5114
+ ref: forwardedRef,
5115
+ "data-solvapay-usage-meter-label": "",
5116
+ className: className ?? "solvapay-usage-meter-label",
5117
+ ...rest,
5118
+ children: children ?? label
5119
+ }
5120
+ );
5121
+ });
5122
+ var Percentage = (0, import_react35.forwardRef)(function UsageMeterPercentage({ className, children, ...rest }, forwardedRef) {
5123
+ const ctx = useUsageMeterCtx("Percentage");
5124
+ const copy = useCopy();
5125
+ if (ctx.percentUsed === null) return null;
5126
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5127
+ "span",
5128
+ {
5129
+ ref: forwardedRef,
5130
+ "data-solvapay-usage-meter-percentage": "",
5131
+ className: className ?? "solvapay-usage-meter-percentage",
5132
+ ...rest,
5133
+ children: children ?? interpolate(copy.usage.percentUsedLabel, { percent: String(Math.round(ctx.percentUsed)) })
5134
+ }
5135
+ );
5136
+ });
5137
+ function formatResetsIn(copy, periodEnd, nowMs = typeof performance !== "undefined" ? performance.timeOrigin + performance.now() : 0) {
5138
+ const end = new Date(periodEnd).getTime();
5139
+ if (Number.isNaN(end)) return null;
5140
+ const daysLeft = Math.max(0, Math.ceil((end - nowMs) / (24 * 60 * 60 * 1e3)));
5141
+ return daysLeft > 0 ? interpolate(copy.usage.resetsInLabel, { days: String(daysLeft) }) : interpolate(copy.usage.resetsOnLabel, { date: new Date(periodEnd).toLocaleDateString() });
5142
+ }
5143
+ var ResetsIn = (0, import_react35.forwardRef)(function UsageMeterResetsIn({ className, children, ...rest }, forwardedRef) {
5144
+ const ctx = useUsageMeterCtx("ResetsIn");
5145
+ const copy = useCopy();
5146
+ const periodEnd = ctx.usage?.periodEnd;
5147
+ if (!periodEnd) return null;
5148
+ const label = formatResetsIn(copy, periodEnd);
5149
+ if (!label) return null;
5150
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5151
+ "span",
5152
+ {
5153
+ ref: forwardedRef,
5154
+ "data-solvapay-usage-meter-resets-in": "",
5155
+ className: className ?? "solvapay-usage-meter-resets-in",
5156
+ ...rest,
5157
+ children: children ?? label
5158
+ }
5159
+ );
5160
+ });
5161
+ var Loading7 = (0, import_react35.forwardRef)(function UsageMeterLoading({ className, children, ...rest }, forwardedRef) {
5162
+ const ctx = useUsageMeterCtx("Loading");
5163
+ const copy = useCopy();
5164
+ if (ctx.state !== "loading") return null;
5165
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5166
+ "div",
5167
+ {
5168
+ ref: forwardedRef,
5169
+ "data-solvapay-usage-meter-loading": "",
5170
+ className: className ?? "solvapay-usage-meter-loading",
5171
+ ...rest,
5172
+ children: children ?? copy.usage.loadingLabel
5173
+ }
5174
+ );
5175
+ });
5176
+ var Empty = (0, import_react35.forwardRef)(function UsageMeterEmpty({ className, children, ...rest }, forwardedRef) {
5177
+ const ctx = useUsageMeterCtx("Empty");
5178
+ const copy = useCopy();
5179
+ if (ctx.loading || ctx.usage !== null) return null;
5180
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5181
+ "div",
5182
+ {
5183
+ ref: forwardedRef,
5184
+ "data-solvapay-usage-meter-empty": "",
5185
+ className: className ?? "solvapay-usage-meter-empty",
5186
+ ...rest,
5187
+ children: children ?? copy.usage.emptyLabel
5188
+ }
5189
+ );
5190
+ });
5191
+ var UsageMeter = Object.assign(Root11, {
5192
+ Root: Root11,
5193
+ Bar,
5194
+ Label,
5195
+ Percentage,
5196
+ ResetsIn,
5197
+ Loading: Loading7,
5198
+ Empty
5199
+ });
5200
+ function useUsageMeter() {
5201
+ return useUsageMeterCtx("useUsageMeter");
5202
+ }
4161
5203
  // Annotate the CommonJS export names for ESM import in node:
4162
5204
  0 && (module.exports = {
4163
5205
  ActivationFlow,
@@ -4212,6 +5254,16 @@ var PlanBadge = ProductBadgeImpl;
4212
5254
  PaymentFormSubmitButton,
4213
5255
  PaymentFormSummary,
4214
5256
  PaymentFormTermsCheckbox,
5257
+ PaywallNotice,
5258
+ PaywallNoticeBalance,
5259
+ PaywallNoticeEmbeddedCheckout,
5260
+ PaywallNoticeHeading,
5261
+ PaywallNoticeHostedCheckoutLink,
5262
+ PaywallNoticeMessage,
5263
+ PaywallNoticePlans,
5264
+ PaywallNoticeProductContext,
5265
+ PaywallNoticeRetry,
5266
+ PaywallNoticeRoot,
4215
5267
  PlanBadge,
4216
5268
  PlanSelector,
4217
5269
  PlanSelectorCard,
@@ -4239,6 +5291,14 @@ var PlanBadge = ProductBadgeImpl;
4239
5291
  TopupFormPaymentElement,
4240
5292
  TopupFormRoot,
4241
5293
  TopupFormSubmitButton,
5294
+ UsageMeter,
5295
+ UsageMeterBar,
5296
+ UsageMeterEmpty,
5297
+ UsageMeterLabel,
5298
+ UsageMeterLoading,
5299
+ UsageMeterPercentage,
5300
+ UsageMeterResetsIn,
5301
+ UsageMeterRoot,
4242
5302
  composeEventHandlers,
4243
5303
  composeRefs,
4244
5304
  setRef,
@@ -4248,7 +5308,9 @@ var PlanBadge = ProductBadgeImpl;
4248
5308
  useCancelledPlanNotice,
4249
5309
  useCheckoutSummary,
4250
5310
  useCreditGate,
5311
+ usePaywallNotice,
4251
5312
  usePlanSelector,
4252
5313
  usePurchaseGate,
4253
- useTopupForm
5314
+ useTopupForm,
5315
+ useUsageMeter
4254
5316
  });