@solvapay/react 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35,13 +35,26 @@ function isPaidPurchase(purchase) {
35
35
  return (purchase.amount ?? 0) > 0;
36
36
  }
37
37
 
38
- // src/SolvaPayProvider.tsx
39
- import { jsx } from "react/jsx-runtime";
40
- var SolvaPayContext = createContext(null);
38
+ // src/utils/headers.ts
41
39
  var CUSTOMER_REF_KEY = "solvapay_customerRef";
42
40
  var CUSTOMER_REF_EXPIRY = "solvapay_customerRef_expiry";
43
41
  var CUSTOMER_REF_USER_ID_KEY = "solvapay_customerRef_userId";
44
- var CACHE_DURATION = 24 * 60 * 60 * 1e3;
42
+ function getAuthAdapter(config) {
43
+ if (config?.auth?.adapter) {
44
+ return config.auth.adapter;
45
+ }
46
+ if (config?.auth?.getToken || config?.auth?.getUserId) {
47
+ return {
48
+ async getToken() {
49
+ return config?.auth?.getToken?.() || null;
50
+ },
51
+ async getUserId() {
52
+ return config?.auth?.getUserId?.() || null;
53
+ }
54
+ };
55
+ }
56
+ return defaultAuthAdapter;
57
+ }
45
58
  function getCachedCustomerRef(userId) {
46
59
  if (typeof window === "undefined") return null;
47
60
  const cached = localStorage.getItem(CUSTOMER_REF_KEY);
@@ -62,6 +75,7 @@ function getCachedCustomerRef(userId) {
62
75
  }
63
76
  return cached;
64
77
  }
78
+ var CACHE_DURATION = 24 * 60 * 60 * 1e3;
65
79
  function setCachedCustomerRef(customerRef, userId) {
66
80
  if (typeof window === "undefined") return;
67
81
  if (userId === void 0 || userId === null) {
@@ -77,41 +91,62 @@ function clearCachedCustomerRef() {
77
91
  localStorage.removeItem(CUSTOMER_REF_EXPIRY);
78
92
  localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
79
93
  }
80
- function getAuthAdapter(config) {
81
- if (config?.auth?.adapter) {
82
- return config.auth.adapter;
94
+ async function buildRequestHeaders(config) {
95
+ const adapter = getAuthAdapter(config);
96
+ const token = await adapter.getToken();
97
+ const userId = await adapter.getUserId();
98
+ const cachedRef = getCachedCustomerRef(userId);
99
+ const headers = {
100
+ "Content-Type": "application/json"
101
+ };
102
+ if (token) {
103
+ headers["Authorization"] = `Bearer ${token}`;
83
104
  }
84
- if (config?.auth?.getToken || config?.auth?.getUserId) {
85
- return {
86
- async getToken() {
87
- return config?.auth?.getToken?.() || null;
88
- },
89
- async getUserId() {
90
- return config?.auth?.getUserId?.() || null;
91
- }
92
- };
105
+ if (cachedRef) {
106
+ headers["x-solvapay-customer-ref"] = cachedRef;
93
107
  }
94
- return defaultAuthAdapter;
108
+ if (config?.headers) {
109
+ const custom = typeof config.headers === "function" ? await config.headers() : config.headers;
110
+ Object.assign(headers, custom);
111
+ }
112
+ return { headers, userId };
95
113
  }
114
+
115
+ // src/SolvaPayProvider.tsx
116
+ import { jsx } from "react/jsx-runtime";
117
+ var SolvaPayContext = createContext(null);
96
118
  var SolvaPayProvider = ({
97
119
  config,
98
120
  createPayment: customCreatePayment,
99
121
  checkPurchase: customCheckPurchase,
100
122
  processPayment: customProcessPayment,
123
+ createTopupPayment: customCreateTopupPayment,
101
124
  children
102
125
  }) => {
103
126
  const [purchaseData, setPurchaseData] = useState({
104
127
  purchases: []
105
128
  });
106
129
  const [loading, setLoading] = useState(false);
130
+ const [isRefetching, setIsRefetching] = useState(false);
131
+ const [purchaseError, setPurchaseError] = useState(null);
107
132
  const [internalCustomerRef, setInternalCustomerRef] = useState(void 0);
108
133
  const [userId, setUserId] = useState(null);
109
134
  const [isAuthenticated, setIsAuthenticated] = useState(false);
135
+ const [creditsValue, setCreditsValue] = useState(null);
136
+ const [displayCurrencyValue, setDisplayCurrencyValue] = useState(null);
137
+ const [creditsPerMinorUnitValue, setCreditsPerMinorUnitValue] = useState(null);
138
+ const [displayExchangeRateValue, setDisplayExchangeRateValue] = useState(null);
139
+ const [balanceLoading, setBalanceLoading] = useState(false);
140
+ const balanceInFlightRef = useRef(false);
141
+ const balanceLoadedRef = useRef(false);
142
+ const optimisticUntilRef = useRef(0);
143
+ const optimisticTimerRef = useRef(null);
144
+ const fetchBalanceRef = useRef(null);
110
145
  const inFlightRef = useRef(null);
111
- const lastFetchedRef = useRef(null);
112
146
  const checkPurchaseRef = useRef(null);
113
147
  const createPaymentRef = useRef(null);
114
148
  const processPaymentRef = useRef(null);
149
+ const createTopupPaymentRef = useRef(null);
115
150
  const configRef = useRef(config);
116
151
  const buildDefaultCheckPurchaseRef = useRef(
117
152
  null
@@ -128,31 +163,15 @@ var SolvaPayProvider = ({
128
163
  useEffect(() => {
129
164
  processPaymentRef.current = customProcessPayment || null;
130
165
  }, [customProcessPayment]);
166
+ useEffect(() => {
167
+ createTopupPaymentRef.current = customCreateTopupPayment || null;
168
+ }, [customCreateTopupPayment]);
131
169
  const buildDefaultCheckPurchase = useCallback(async () => {
132
170
  const currentConfig = configRef.current;
133
- const adapter = getAuthAdapter(currentConfig);
134
- const token = await adapter.getToken();
135
- const detectedUserId = await adapter.getUserId();
171
+ const { headers } = await buildRequestHeaders(currentConfig);
136
172
  const route = currentConfig?.api?.checkPurchase || "/api/check-purchase";
137
173
  const fetchFn = currentConfig?.fetch || fetch;
138
- const cachedRef = getCachedCustomerRef(detectedUserId);
139
- const headers = {
140
- "Content-Type": "application/json"
141
- };
142
- if (token) {
143
- headers["Authorization"] = `Bearer ${token}`;
144
- }
145
- if (cachedRef) {
146
- headers["x-solvapay-customer-ref"] = cachedRef;
147
- }
148
- if (currentConfig?.headers) {
149
- const customHeaders = typeof currentConfig.headers === "function" ? await currentConfig.headers() : currentConfig.headers;
150
- Object.assign(headers, customHeaders);
151
- }
152
- const res = await fetchFn(route, {
153
- method: "GET",
154
- headers
155
- });
174
+ const res = await fetchFn(route, { method: "GET", headers });
156
175
  if (!res.ok) {
157
176
  const error = new Error(`Failed to check purchase: ${res.statusText}`);
158
177
  currentConfig?.onError?.(error, "checkPurchase");
@@ -166,26 +185,13 @@ var SolvaPayProvider = ({
166
185
  const buildDefaultCreatePayment = useCallback(
167
186
  async (params) => {
168
187
  const currentConfig = configRef.current;
169
- const adapter = getAuthAdapter(currentConfig);
170
- const token = await adapter.getToken();
171
- const detectedUserId = await adapter.getUserId();
188
+ const { headers } = await buildRequestHeaders(currentConfig);
172
189
  const route = currentConfig?.api?.createPayment || "/api/create-payment-intent";
173
190
  const fetchFn = currentConfig?.fetch || fetch;
174
- const cachedRef = getCachedCustomerRef(detectedUserId);
175
- const headers = {
176
- "Content-Type": "application/json"
177
- };
178
- if (token) {
179
- headers["Authorization"] = `Bearer ${token}`;
180
- }
181
- if (cachedRef) {
182
- headers["x-solvapay-customer-ref"] = cachedRef;
191
+ const body = {};
192
+ if (params.planRef) {
193
+ body.planRef = params.planRef;
183
194
  }
184
- if (currentConfig?.headers) {
185
- const customHeaders = typeof currentConfig.headers === "function" ? await currentConfig.headers() : currentConfig.headers;
186
- Object.assign(headers, customHeaders);
187
- }
188
- const body = { planRef: params.planRef };
189
195
  if (params.productRef) {
190
196
  body.productRef = params.productRef;
191
197
  }
@@ -206,25 +212,9 @@ var SolvaPayProvider = ({
206
212
  const buildDefaultProcessPayment = useCallback(
207
213
  async (params) => {
208
214
  const currentConfig = configRef.current;
209
- const adapter = getAuthAdapter(currentConfig);
210
- const token = await adapter.getToken();
211
- const detectedUserId = await adapter.getUserId();
215
+ const { headers } = await buildRequestHeaders(currentConfig);
212
216
  const route = currentConfig?.api?.processPayment || "/api/process-payment";
213
217
  const fetchFn = currentConfig?.fetch || fetch;
214
- const cachedRef = getCachedCustomerRef(detectedUserId);
215
- const headers = {
216
- "Content-Type": "application/json"
217
- };
218
- if (token) {
219
- headers["Authorization"] = `Bearer ${token}`;
220
- }
221
- if (cachedRef) {
222
- headers["x-solvapay-customer-ref"] = cachedRef;
223
- }
224
- if (currentConfig?.headers) {
225
- const customHeaders = typeof currentConfig.headers === "function" ? await currentConfig.headers() : currentConfig.headers;
226
- Object.assign(headers, customHeaders);
227
- }
228
218
  const res = await fetchFn(route, {
229
219
  method: "POST",
230
220
  headers,
@@ -239,6 +229,86 @@ var SolvaPayProvider = ({
239
229
  },
240
230
  []
241
231
  );
232
+ const buildDefaultCreateTopupPayment = useCallback(
233
+ async (params) => {
234
+ const currentConfig = configRef.current;
235
+ const { headers } = await buildRequestHeaders(currentConfig);
236
+ const route = currentConfig?.api?.createTopupPayment || "/api/create-topup-payment-intent";
237
+ const fetchFn = currentConfig?.fetch || fetch;
238
+ const res = await fetchFn(route, {
239
+ method: "POST",
240
+ headers,
241
+ body: JSON.stringify({
242
+ amount: params.amount,
243
+ currency: params.currency
244
+ })
245
+ });
246
+ if (!res.ok) {
247
+ const error = new Error(`Failed to create topup payment: ${res.statusText}`);
248
+ currentConfig?.onError?.(error, "createTopupPayment");
249
+ throw error;
250
+ }
251
+ return res.json();
252
+ },
253
+ []
254
+ );
255
+ const fetchBalanceImpl = useCallback(async () => {
256
+ if (optimisticUntilRef.current > Date.now()) return;
257
+ if (!isAuthenticated && !internalCustomerRef) {
258
+ setCreditsValue(null);
259
+ setDisplayCurrencyValue(null);
260
+ setCreditsPerMinorUnitValue(null);
261
+ setDisplayExchangeRateValue(null);
262
+ setBalanceLoading(false);
263
+ balanceLoadedRef.current = false;
264
+ return;
265
+ }
266
+ if (balanceInFlightRef.current) return;
267
+ balanceInFlightRef.current = true;
268
+ if (!balanceLoadedRef.current) {
269
+ setBalanceLoading(true);
270
+ }
271
+ try {
272
+ const currentConfig = configRef.current;
273
+ const { headers } = await buildRequestHeaders(currentConfig);
274
+ const route = currentConfig?.api?.customerBalance || "/api/customer-balance";
275
+ const fetchFn = currentConfig?.fetch || fetch;
276
+ const res = await fetchFn(route, { method: "GET", headers });
277
+ if (!res.ok) {
278
+ console.error("[SolvaPayProvider] Failed to fetch balance:", res.statusText);
279
+ return;
280
+ }
281
+ const data = await res.json();
282
+ setCreditsValue(data.credits ?? null);
283
+ setDisplayCurrencyValue(data.displayCurrency ?? null);
284
+ setCreditsPerMinorUnitValue(data.creditsPerMinorUnit ?? null);
285
+ setDisplayExchangeRateValue(data.displayExchangeRate ?? null);
286
+ balanceLoadedRef.current = true;
287
+ } catch (error) {
288
+ console.error("[SolvaPayProvider] Failed to fetch balance:", error);
289
+ } finally {
290
+ setBalanceLoading(false);
291
+ balanceInFlightRef.current = false;
292
+ }
293
+ }, [isAuthenticated, internalCustomerRef]);
294
+ useEffect(() => {
295
+ fetchBalanceRef.current = fetchBalanceImpl;
296
+ }, [fetchBalanceImpl]);
297
+ useEffect(() => {
298
+ return () => {
299
+ if (optimisticTimerRef.current) clearTimeout(optimisticTimerRef.current);
300
+ };
301
+ }, []);
302
+ const OPTIMISTIC_GRACE_MS = 8e3;
303
+ const adjustBalanceImpl = useCallback((credits) => {
304
+ setCreditsValue((prev) => (prev ?? 0) + credits);
305
+ optimisticUntilRef.current = Date.now() + OPTIMISTIC_GRACE_MS;
306
+ if (optimisticTimerRef.current) clearTimeout(optimisticTimerRef.current);
307
+ optimisticTimerRef.current = setTimeout(() => {
308
+ optimisticUntilRef.current = 0;
309
+ fetchBalanceRef.current?.();
310
+ }, OPTIMISTIC_GRACE_MS);
311
+ }, []);
242
312
  const _checkPurchase = useCallback(async () => {
243
313
  if (checkPurchaseRef.current) {
244
314
  return checkPurchaseRef.current();
@@ -266,6 +336,15 @@ var SolvaPayProvider = ({
266
336
  },
267
337
  [buildDefaultProcessPayment]
268
338
  );
339
+ const createTopupPayment = useCallback(
340
+ async (params) => {
341
+ if (createTopupPaymentRef.current) {
342
+ return createTopupPaymentRef.current(params);
343
+ }
344
+ return buildDefaultCreateTopupPayment(params);
345
+ },
346
+ [buildDefaultCreateTopupPayment]
347
+ );
269
348
  useEffect(() => {
270
349
  const detectAuth = async () => {
271
350
  const currentConfig = configRef.current;
@@ -291,27 +370,30 @@ var SolvaPayProvider = ({
291
370
  }
292
371
  };
293
372
  detectAuth();
294
- const interval = setInterval(detectAuth, 5e3);
373
+ const interval = setInterval(detectAuth, 3e4);
295
374
  return () => clearInterval(interval);
296
375
  }, [userId]);
297
376
  const fetchPurchase = useCallback(
298
377
  async (force = false) => {
299
378
  if (!isAuthenticated && !internalCustomerRef) {
300
379
  setPurchaseData({ purchases: [] });
380
+ setPurchaseError(null);
301
381
  setLoading(false);
382
+ setIsRefetching(false);
302
383
  inFlightRef.current = null;
303
- lastFetchedRef.current = null;
304
384
  return;
305
385
  }
306
386
  const cacheKey = internalCustomerRef || userId || "anonymous";
307
- if (!force && lastFetchedRef.current === cacheKey && inFlightRef.current !== cacheKey) {
308
- return;
309
- }
310
- if (inFlightRef.current === cacheKey) {
387
+ if (inFlightRef.current === cacheKey && !force) {
311
388
  return;
312
389
  }
313
390
  inFlightRef.current = cacheKey;
314
- setLoading(true);
391
+ const hasExistingData = purchaseData.purchases.length > 0;
392
+ if (hasExistingData) {
393
+ setIsRefetching(true);
394
+ } else {
395
+ setLoading(true);
396
+ }
315
397
  try {
316
398
  const checkFn = checkPurchaseRef.current || buildDefaultCheckPurchaseRef.current;
317
399
  if (!checkFn) {
@@ -330,43 +412,111 @@ var SolvaPayProvider = ({
330
412
  purchases: filterPurchases(data.purchases || [])
331
413
  };
332
414
  setPurchaseData(filteredData);
333
- lastFetchedRef.current = cacheKey;
415
+ setPurchaseError(null);
334
416
  }
335
- } catch (error) {
336
- console.error("[SolvaPayProvider] Failed to fetch purchase:", error);
417
+ } catch (err) {
418
+ console.error("[SolvaPayProvider] Failed to fetch purchase:", err);
337
419
  if (inFlightRef.current === cacheKey) {
338
- setPurchaseData({
339
- purchases: []
340
- });
341
- lastFetchedRef.current = cacheKey;
420
+ setPurchaseError(err instanceof Error ? err : new Error(String(err)));
342
421
  }
343
422
  } finally {
344
423
  if (inFlightRef.current === cacheKey) {
345
424
  setLoading(false);
425
+ setIsRefetching(false);
346
426
  inFlightRef.current = null;
347
427
  }
348
428
  }
349
429
  },
350
- [isAuthenticated, internalCustomerRef, userId]
430
+ [isAuthenticated, internalCustomerRef, userId, purchaseData.purchases.length]
351
431
  );
352
432
  const fetchPurchaseRef = useRef(fetchPurchase);
353
433
  useEffect(() => {
354
434
  fetchPurchaseRef.current = fetchPurchase;
355
435
  }, [fetchPurchase]);
356
436
  const refetchPurchase = useCallback(async () => {
357
- lastFetchedRef.current = null;
358
437
  inFlightRef.current = null;
359
- setPurchaseData({ purchases: [] });
360
438
  await fetchPurchaseRef.current(true);
361
439
  }, []);
440
+ const cancelRenewal = useCallback(
441
+ async (params) => {
442
+ const currentConfig = configRef.current;
443
+ const { headers } = await buildRequestHeaders(currentConfig);
444
+ const route = currentConfig?.api?.cancelRenewal || "/api/cancel-renewal";
445
+ const fetchFn = currentConfig?.fetch || fetch;
446
+ const res = await fetchFn(route, {
447
+ method: "POST",
448
+ headers,
449
+ body: JSON.stringify(params)
450
+ });
451
+ if (!res.ok) {
452
+ const data = await res.json().catch(() => ({}));
453
+ const error = new Error(data.error || `Failed to cancel renewal: ${res.statusText}`);
454
+ currentConfig?.onError?.(error, "cancelRenewal");
455
+ throw error;
456
+ }
457
+ const result = await res.json();
458
+ await refetchPurchase();
459
+ return result;
460
+ },
461
+ [refetchPurchase]
462
+ );
463
+ const reactivateRenewal = useCallback(
464
+ async (params) => {
465
+ const currentConfig = configRef.current;
466
+ const { headers } = await buildRequestHeaders(currentConfig);
467
+ const route = currentConfig?.api?.reactivateRenewal || "/api/reactivate-renewal";
468
+ const fetchFn = currentConfig?.fetch || fetch;
469
+ const res = await fetchFn(route, {
470
+ method: "POST",
471
+ headers,
472
+ body: JSON.stringify(params)
473
+ });
474
+ if (!res.ok) {
475
+ const data = await res.json().catch(() => ({}));
476
+ const error = new Error(data.error || `Failed to reactivate renewal: ${res.statusText}`);
477
+ currentConfig?.onError?.(error, "reactivateRenewal");
478
+ throw error;
479
+ }
480
+ const result = await res.json();
481
+ await refetchPurchase();
482
+ return result;
483
+ },
484
+ [refetchPurchase]
485
+ );
486
+ const activatePlan = useCallback(
487
+ async (params) => {
488
+ const currentConfig = configRef.current;
489
+ const { headers } = await buildRequestHeaders(currentConfig);
490
+ const route = currentConfig?.api?.activatePlan || "/api/activate-plan";
491
+ const fetchFn = currentConfig?.fetch || fetch;
492
+ const res = await fetchFn(route, {
493
+ method: "POST",
494
+ headers,
495
+ body: JSON.stringify(params)
496
+ });
497
+ if (!res.ok) {
498
+ const data = await res.json().catch(() => ({}));
499
+ const error = new Error(data.error || `Failed to activate plan: ${res.statusText}`);
500
+ currentConfig?.onError?.(error, "activatePlan");
501
+ throw error;
502
+ }
503
+ const result = await res.json();
504
+ if (result.status === "activated" || result.status === "already_active") {
505
+ await refetchPurchase();
506
+ }
507
+ return result;
508
+ },
509
+ [refetchPurchase]
510
+ );
362
511
  useEffect(() => {
363
- lastFetchedRef.current = null;
364
512
  inFlightRef.current = null;
365
513
  if (isAuthenticated || internalCustomerRef) {
366
514
  fetchPurchase();
367
515
  } else {
368
516
  setPurchaseData({ purchases: [] });
517
+ setPurchaseError(null);
369
518
  setLoading(false);
519
+ setIsRefetching(false);
370
520
  }
371
521
  }, [isAuthenticated, internalCustomerRef, userId]);
372
522
  const updateCustomerRef = useCallback(
@@ -387,6 +537,8 @@ var SolvaPayProvider = ({
387
537
  )[0] || null;
388
538
  return {
389
539
  loading,
540
+ isRefetching,
541
+ error: purchaseError,
390
542
  customerRef: purchaseData.customerRef || internalCustomerRef,
391
543
  email: purchaseData.email,
392
544
  name: purchaseData.name,
@@ -405,24 +557,47 @@ var SolvaPayProvider = ({
405
557
  hasPaidPurchase: activePaidPurchases.length > 0,
406
558
  activePaidPurchase
407
559
  };
408
- }, [loading, purchaseData, internalCustomerRef]);
560
+ }, [loading, isRefetching, purchaseError, purchaseData, internalCustomerRef]);
561
+ const balance = useMemo(
562
+ () => ({
563
+ loading: balanceLoading,
564
+ credits: creditsValue,
565
+ displayCurrency: displayCurrencyValue,
566
+ creditsPerMinorUnit: creditsPerMinorUnitValue,
567
+ displayExchangeRate: displayExchangeRateValue,
568
+ refetch: fetchBalanceImpl,
569
+ adjustBalance: adjustBalanceImpl
570
+ }),
571
+ [balanceLoading, creditsValue, displayCurrencyValue, creditsPerMinorUnitValue, displayExchangeRateValue, fetchBalanceImpl, adjustBalanceImpl]
572
+ );
409
573
  const contextValue = useMemo(
410
574
  () => ({
411
575
  purchase,
412
576
  refetchPurchase,
413
577
  createPayment,
414
578
  processPayment,
579
+ createTopupPayment,
580
+ cancelRenewal,
581
+ reactivateRenewal,
582
+ activatePlan,
415
583
  customerRef: purchaseData.customerRef || internalCustomerRef,
416
- updateCustomerRef
584
+ updateCustomerRef,
585
+ balance,
586
+ _config: configRef.current
417
587
  }),
418
588
  [
419
589
  purchase,
420
590
  refetchPurchase,
421
591
  createPayment,
422
592
  processPayment,
593
+ createTopupPayment,
594
+ cancelRenewal,
595
+ reactivateRenewal,
596
+ activatePlan,
423
597
  purchaseData.customerRef,
424
598
  internalCustomerRef,
425
- updateCustomerRef
599
+ updateCustomerRef,
600
+ balance
426
601
  ]
427
602
  );
428
603
  return /* @__PURE__ */ jsx(SolvaPayContext.Provider, { value: contextValue, children });
@@ -453,29 +628,66 @@ var stripePromiseCache = /* @__PURE__ */ new Map();
453
628
  function getStripeCacheKey(publishableKey, accountId) {
454
629
  return accountId ? `${publishableKey}:${accountId}` : publishableKey;
455
630
  }
631
+ async function resolvePlanRef(productRef, fetchFn, headers, listPlansRoute) {
632
+ const url = `${listPlansRoute}?productRef=${encodeURIComponent(productRef)}`;
633
+ const res = await fetchFn(url, { method: "GET", headers });
634
+ if (!res.ok) {
635
+ throw new Error(`Failed to fetch plans for product "${productRef}": ${res.statusText}`);
636
+ }
637
+ const data = await res.json();
638
+ const allPlans = data.plans ?? [];
639
+ const activePlans = allPlans.filter((p) => p.isActive !== false && p.status !== "inactive");
640
+ if (activePlans.length === 0) {
641
+ throw new Error(
642
+ `No active plans found for product "${productRef}". Configure at least one plan in the SolvaPay Console.`
643
+ );
644
+ }
645
+ if (activePlans.length === 1) {
646
+ return activePlans[0].reference;
647
+ }
648
+ const defaultPlan = activePlans.find((p) => p.default === true);
649
+ if (defaultPlan) {
650
+ return defaultPlan.reference;
651
+ }
652
+ throw new Error(
653
+ `Product "${productRef}" has ${activePlans.length} active plans but none is marked as default. Either pass planRef explicitly, use <PricingSelector> for user selection, or mark one plan as default in the SolvaPay Console.`
654
+ );
655
+ }
456
656
  function useCheckout(options) {
457
657
  const { planRef, productRef } = options;
458
- const { createPayment, customerRef, updateCustomerRef } = useSolvaPay();
658
+ const { createPayment, customerRef, updateCustomerRef, _config } = useSolvaPay();
459
659
  const [loading, setLoading] = useState2(false);
460
- const [error, setError] = useState2(
461
- !planRef || typeof planRef !== "string" ? new Error("useCheckout: planRef parameter is required and must be a string") : null
462
- );
660
+ const [error, setError] = useState2(null);
463
661
  const [stripePromise, setStripePromise] = useState2(null);
464
662
  const [clientSecret, setClientSecret] = useState2(null);
663
+ const [resolvedPlanRef, setResolvedPlanRef] = useState2(planRef || null);
465
664
  const isStartingRef = useRef2(false);
466
665
  const startCheckout = useCallback2(async () => {
467
666
  if (isStartingRef.current || loading) {
468
667
  return;
469
668
  }
470
- if (!planRef || typeof planRef !== "string") {
471
- setError(new Error("useCheckout: planRef parameter is required and must be a string"));
669
+ if (!planRef && !productRef) {
670
+ setError(
671
+ new Error(
672
+ "useCheckout: either planRef or productRef is required. Pass planRef directly, or pass productRef to auto-resolve the plan."
673
+ )
674
+ );
472
675
  return;
473
676
  }
474
677
  isStartingRef.current = true;
475
678
  setLoading(true);
476
679
  setError(null);
477
680
  try {
478
- const result = await createPayment({ planRef, productRef });
681
+ let effectivePlanRef = planRef;
682
+ if (!effectivePlanRef && productRef) {
683
+ const listPlansRoute = _config?.api?.listPlans || "/api/list-plans";
684
+ effectivePlanRef = await resolvePlanRef(productRef, fetch, {}, listPlansRoute);
685
+ setResolvedPlanRef(effectivePlanRef);
686
+ }
687
+ if (!effectivePlanRef) {
688
+ throw new Error("Could not determine plan reference for checkout");
689
+ }
690
+ const result = await createPayment({ planRef: effectivePlanRef, productRef });
479
691
  if (!result || typeof result !== "object") {
480
692
  throw new Error("Invalid payment intent response from server");
481
693
  }
@@ -504,19 +716,21 @@ function useCheckout(options) {
504
716
  setLoading(false);
505
717
  isStartingRef.current = false;
506
718
  }
507
- }, [planRef, productRef, createPayment, updateCustomerRef, loading]);
719
+ }, [planRef, productRef, createPayment, updateCustomerRef, loading, _config]);
508
720
  const reset = useCallback2(() => {
509
721
  isStartingRef.current = false;
510
722
  setLoading(false);
511
723
  setError(null);
512
724
  setStripePromise(null);
513
725
  setClientSecret(null);
514
- }, []);
726
+ setResolvedPlanRef(planRef || null);
727
+ }, [planRef]);
515
728
  return {
516
729
  loading,
517
730
  error,
518
731
  stripePromise,
519
732
  clientSecret,
733
+ resolvedPlanRef,
520
734
  startCheckout,
521
735
  reset
522
736
  };
@@ -684,14 +898,16 @@ var StripePaymentFormWrapper = ({
684
898
  const cardElementOptions = {
685
899
  style: {
686
900
  base: {
687
- fontSize: "16px",
688
- color: "#424770",
901
+ fontSize: "17px",
902
+ lineHeight: "24px",
903
+ color: "#1e293b",
904
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
689
905
  "::placeholder": {
690
- color: "#aab7c4"
906
+ color: "#94a3b8"
691
907
  }
692
908
  },
693
909
  invalid: {
694
- color: "#9e2146"
910
+ color: "#dc2626"
695
911
  }
696
912
  },
697
913
  hidePostalCode: true
@@ -711,22 +927,7 @@ var StripePaymentFormWrapper = ({
711
927
  }
712
928
  }
713
929
  }
714
- ) : /* @__PURE__ */ jsx3(
715
- "div",
716
- {
717
- style: {
718
- padding: "12px",
719
- border: "1px solid #cbd5e1",
720
- borderRadius: "0.375rem",
721
- backgroundColor: "white",
722
- display: "flex",
723
- alignItems: "center",
724
- justifyContent: "center",
725
- minHeight: "40px"
726
- },
727
- children: /* @__PURE__ */ jsx3(Spinner, { size: "sm" })
728
- }
729
- ),
930
+ ) : /* @__PURE__ */ jsx3("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "52px" }, children: /* @__PURE__ */ jsx3(Spinner, { size: "sm" }) }),
730
931
  message && !isSuccess && /* @__PURE__ */ jsx3("div", { role: "alert", "aria-live": "assertive", "aria-atomic": "true", children: message }),
731
932
  /* @__PURE__ */ jsx3(
732
933
  "button",
@@ -758,88 +959,62 @@ var PaymentForm = ({
758
959
  className,
759
960
  buttonClassName
760
961
  }) => {
761
- const validPlanRef = planRef && typeof planRef === "string" ? planRef : "";
762
962
  const {
763
963
  loading: checkoutLoading,
764
964
  error: checkoutError,
765
965
  clientSecret,
766
966
  startCheckout,
767
- stripePromise
768
- } = useCheckout({ planRef: validPlanRef, productRef });
967
+ stripePromise,
968
+ resolvedPlanRef
969
+ } = useCheckout({ planRef, productRef });
769
970
  const { refetch } = usePurchase();
770
971
  const { processPayment } = useSolvaPay();
771
972
  const hasInitializedRef = useRef3(false);
973
+ const hasPlanOrProduct = !!(planRef || productRef);
772
974
  useEffect2(() => {
773
- if (!hasInitializedRef.current && validPlanRef && !checkoutLoading && !checkoutError && !clientSecret) {
975
+ if (!hasInitializedRef.current && hasPlanOrProduct && !checkoutLoading && !checkoutError && !clientSecret) {
774
976
  hasInitializedRef.current = true;
775
977
  startCheckout().catch(() => {
776
978
  hasInitializedRef.current = false;
777
979
  });
778
980
  }
779
- if (validPlanRef && clientSecret) {
981
+ if (hasPlanOrProduct && clientSecret) {
780
982
  hasInitializedRef.current = true;
781
983
  }
782
- }, [validPlanRef, checkoutLoading, checkoutError, clientSecret, startCheckout]);
984
+ }, [hasPlanOrProduct, checkoutLoading, checkoutError, clientSecret, startCheckout]);
985
+ const effectivePlanRef = planRef || resolvedPlanRef;
783
986
  const handleSuccess = useCallback3(
784
987
  async (paymentIntent) => {
785
- let processingTimeout = false;
786
- let processingResult = null;
787
988
  const paymentIntentAny = paymentIntent;
788
989
  if (processPayment && productRef) {
789
990
  try {
790
991
  const result = await processPayment({
791
992
  paymentIntentId: paymentIntentAny.id,
792
993
  productRef,
793
- planRef
994
+ planRef: effectivePlanRef || void 0
794
995
  });
795
- processingResult = result;
796
996
  const isTimeout = result?.status === "timeout";
797
- processingTimeout = isTimeout;
798
997
  if (isTimeout) {
799
998
  for (let attempt = 1; attempt <= 5; attempt++) {
800
- const delay = attempt * 1e3;
801
- await new Promise((resolve) => setTimeout(resolve, delay));
999
+ await new Promise((resolve) => setTimeout(resolve, attempt * 1e3));
802
1000
  await refetch();
803
1001
  }
804
- if (onSuccess) {
805
- await onSuccess({
806
- ...paymentIntentAny,
807
- _processingTimeout: processingTimeout,
808
- _processingResult: processingResult
809
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
810
- });
811
- }
812
- throw new Error("Payment processing timed out");
813
- } else {
814
- await refetch();
1002
+ const err = new Error("Payment processing timed out \u2014 webhooks may not be configured");
1003
+ onError?.(err);
1004
+ return;
815
1005
  }
1006
+ await refetch();
816
1007
  } catch (error) {
817
1008
  console.error("[PaymentForm] Failed to process payment:", error);
818
- if (onSuccess) {
819
- try {
820
- await onSuccess({
821
- ...paymentIntentAny,
822
- _processingError: error
823
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
824
- });
825
- } catch {
826
- }
827
- }
828
- throw error;
1009
+ onError?.(error instanceof Error ? error : new Error(String(error)));
1010
+ return;
829
1011
  }
830
1012
  } else {
831
1013
  await refetch();
832
1014
  }
833
- if (onSuccess && !processingTimeout) {
834
- await onSuccess({
835
- ...paymentIntentAny,
836
- _processingTimeout: processingTimeout,
837
- _processingResult: processingResult
838
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
839
- });
840
- }
1015
+ onSuccess?.(paymentIntentAny);
841
1016
  },
842
- [processPayment, productRef, planRef, refetch, onSuccess]
1017
+ [processPayment, productRef, effectivePlanRef, refetch, onSuccess, onError]
843
1018
  );
844
1019
  const handleError = useCallback3(
845
1020
  (err) => {
@@ -850,7 +1025,6 @@ var PaymentForm = ({
850
1025
  [onError]
851
1026
  );
852
1027
  const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
853
- const isValidPlanRef = planRef && typeof planRef === "string";
854
1028
  const hasError = !!checkoutError;
855
1029
  const hasStripeData = !!(stripePromise && clientSecret);
856
1030
  const elementsOptions = useMemo2(() => {
@@ -864,72 +1038,246 @@ var PaymentForm = ({
864
1038
  }
865
1039
  }, [hasStripeData]);
866
1040
  const shouldRenderElements = hasStripeData || hasMountedElements && stripePromise && clientSecret;
867
- return /* @__PURE__ */ jsx4("div", { className, children: !isValidPlanRef ? /* @__PURE__ */ jsx4("div", { children: "PaymentForm: planRef is required and must be a string" }) : hasError ? /* @__PURE__ */ jsxs3("div", { children: [
1041
+ return /* @__PURE__ */ jsx4("div", { className, children: !hasPlanOrProduct ? /* @__PURE__ */ jsx4("div", { children: "PaymentForm: either planRef or productRef is required" }) : hasError ? /* @__PURE__ */ jsxs3("div", { children: [
868
1042
  /* @__PURE__ */ jsx4("div", { children: "Payment initialization failed" }),
869
1043
  /* @__PURE__ */ jsx4("div", { children: checkoutError?.message || "Unknown error" })
870
- ] }) : shouldRenderElements && elementsOptions ? (
871
- // Once we have Stripe data, always render Elements to maintain hook consistency
872
- // This prevents hook count mismatches when transitioning between states
1044
+ ] }) : shouldRenderElements && elementsOptions ? /* @__PURE__ */ jsx4(
1045
+ Elements,
1046
+ {
1047
+ stripe: stripePromise,
1048
+ options: elementsOptions,
1049
+ children: /* @__PURE__ */ jsx4(
1050
+ StripePaymentFormWrapper,
1051
+ {
1052
+ onSuccess: handleSuccess,
1053
+ onError: handleError,
1054
+ returnUrl: finalReturnUrl,
1055
+ submitButtonText,
1056
+ buttonClassName,
1057
+ clientSecret
1058
+ }
1059
+ )
1060
+ },
1061
+ clientSecret
1062
+ ) : /* @__PURE__ */ jsxs3("div", { children: [
873
1063
  /* @__PURE__ */ jsx4(
874
- Elements,
1064
+ "div",
875
1065
  {
876
- stripe: stripePromise,
877
- options: elementsOptions,
878
- children: /* @__PURE__ */ jsx4(
879
- StripePaymentFormWrapper,
880
- {
881
- onSuccess: handleSuccess,
882
- onError: handleError,
883
- returnUrl: finalReturnUrl,
884
- submitButtonText,
885
- buttonClassName,
886
- clientSecret
887
- }
888
- )
889
- },
890
- clientSecret
1066
+ style: {
1067
+ minHeight: "40px",
1068
+ display: "flex",
1069
+ alignItems: "center",
1070
+ justifyContent: "center"
1071
+ },
1072
+ children: /* @__PURE__ */ jsx4(Spinner, { size: "md" })
1073
+ }
1074
+ ),
1075
+ /* @__PURE__ */ jsx4(
1076
+ "button",
1077
+ {
1078
+ type: "submit",
1079
+ disabled: true,
1080
+ className: buttonClassName,
1081
+ "aria-busy": "false",
1082
+ "aria-disabled": "true",
1083
+ children: submitButtonText
1084
+ }
891
1085
  )
892
- ) : (
893
- // Loading state before Stripe data is available
894
- /* @__PURE__ */ jsxs3("div", { children: [
895
- /* @__PURE__ */ jsx4(
896
- "div",
897
- {
898
- style: {
899
- minHeight: "40px",
900
- display: "flex",
901
- alignItems: "center",
902
- justifyContent: "center"
903
- },
904
- children: /* @__PURE__ */ jsx4(Spinner, { size: "md" })
905
- }
906
- ),
907
- /* @__PURE__ */ jsx4(
908
- "button",
1086
+ ] }) });
1087
+ };
1088
+
1089
+ // src/TopupForm.tsx
1090
+ import { useEffect as useEffect3, useCallback as useCallback5, useRef as useRef5, useMemo as useMemo3 } from "react";
1091
+ import { Elements as Elements2 } from "@stripe/react-stripe-js";
1092
+
1093
+ // src/hooks/useTopup.ts
1094
+ import { useState as useState5, useCallback as useCallback4, useRef as useRef4 } from "react";
1095
+ import { loadStripe as loadStripe2 } from "@stripe/stripe-js";
1096
+ var stripePromiseCache2 = /* @__PURE__ */ new Map();
1097
+ function getStripeCacheKey2(publishableKey, accountId) {
1098
+ return accountId ? `${publishableKey}:${accountId}` : publishableKey;
1099
+ }
1100
+ function useTopup(options) {
1101
+ const { amount, currency } = options;
1102
+ const { createTopupPayment, customerRef, updateCustomerRef } = useSolvaPay();
1103
+ const [loading, setLoading] = useState5(false);
1104
+ const [error, setError] = useState5(null);
1105
+ const [stripePromise, setStripePromise] = useState5(null);
1106
+ const [clientSecret, setClientSecret] = useState5(null);
1107
+ const isStartingRef = useRef4(false);
1108
+ const startTopup = useCallback4(async () => {
1109
+ if (isStartingRef.current || loading) {
1110
+ return;
1111
+ }
1112
+ if (!amount || amount <= 0) {
1113
+ setError(new Error("useTopup: amount must be a positive number"));
1114
+ return;
1115
+ }
1116
+ isStartingRef.current = true;
1117
+ setLoading(true);
1118
+ setError(null);
1119
+ try {
1120
+ const result = await createTopupPayment({ amount, currency });
1121
+ if (!result || typeof result !== "object") {
1122
+ throw new Error("Invalid topup payment intent response from server");
1123
+ }
1124
+ if (!result.clientSecret || typeof result.clientSecret !== "string") {
1125
+ throw new Error("Invalid client secret in topup payment intent response");
1126
+ }
1127
+ if (!result.publishableKey || typeof result.publishableKey !== "string") {
1128
+ throw new Error("Invalid publishable key in topup payment intent response");
1129
+ }
1130
+ if (result.customerRef && result.customerRef !== customerRef && updateCustomerRef) {
1131
+ updateCustomerRef(result.customerRef);
1132
+ }
1133
+ const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
1134
+ const cacheKey = getStripeCacheKey2(result.publishableKey, result.accountId);
1135
+ let stripe = stripePromiseCache2.get(cacheKey);
1136
+ if (!stripe) {
1137
+ stripe = loadStripe2(result.publishableKey, stripeOptions);
1138
+ stripePromiseCache2.set(cacheKey, stripe);
1139
+ }
1140
+ setStripePromise(stripe);
1141
+ setClientSecret(result.clientSecret);
1142
+ } catch (err) {
1143
+ const error2 = err instanceof Error ? err : new Error("Failed to start topup");
1144
+ setError(error2);
1145
+ } finally {
1146
+ setLoading(false);
1147
+ isStartingRef.current = false;
1148
+ }
1149
+ }, [amount, currency, createTopupPayment, customerRef, updateCustomerRef, loading]);
1150
+ const reset = useCallback4(() => {
1151
+ isStartingRef.current = false;
1152
+ setLoading(false);
1153
+ setError(null);
1154
+ setStripePromise(null);
1155
+ setClientSecret(null);
1156
+ }, []);
1157
+ return {
1158
+ loading,
1159
+ error,
1160
+ stripePromise,
1161
+ clientSecret,
1162
+ startTopup,
1163
+ reset
1164
+ };
1165
+ }
1166
+
1167
+ // src/TopupForm.tsx
1168
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1169
+ var TopupForm = ({
1170
+ amount,
1171
+ currency,
1172
+ onSuccess,
1173
+ onError,
1174
+ returnUrl,
1175
+ submitButtonText = "Top Up",
1176
+ className,
1177
+ buttonClassName
1178
+ }) => {
1179
+ const {
1180
+ loading: topupLoading,
1181
+ error: topupError,
1182
+ clientSecret,
1183
+ startTopup,
1184
+ stripePromise
1185
+ } = useTopup({ amount, currency });
1186
+ const hasInitializedRef = useRef5(false);
1187
+ const hasAmount = amount > 0;
1188
+ useEffect3(() => {
1189
+ if (!hasInitializedRef.current && hasAmount && !topupLoading && !topupError && !clientSecret) {
1190
+ hasInitializedRef.current = true;
1191
+ startTopup().catch(() => {
1192
+ hasInitializedRef.current = false;
1193
+ });
1194
+ }
1195
+ if (hasAmount && clientSecret) {
1196
+ hasInitializedRef.current = true;
1197
+ }
1198
+ }, [hasAmount, topupLoading, topupError, clientSecret, startTopup]);
1199
+ const handleSuccess = useCallback5(
1200
+ async (paymentIntent) => {
1201
+ if (onSuccess) {
1202
+ await onSuccess(paymentIntent);
1203
+ }
1204
+ },
1205
+ [onSuccess]
1206
+ );
1207
+ const handleError = useCallback5(
1208
+ (err) => {
1209
+ if (onError) {
1210
+ onError(err);
1211
+ }
1212
+ },
1213
+ [onError]
1214
+ );
1215
+ const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
1216
+ const hasError = !!topupError;
1217
+ const hasStripeData = !!(stripePromise && clientSecret);
1218
+ const elementsOptions = useMemo3(() => {
1219
+ if (!clientSecret) return void 0;
1220
+ return { clientSecret };
1221
+ }, [clientSecret]);
1222
+ return /* @__PURE__ */ jsx5("div", { className, children: !hasAmount ? /* @__PURE__ */ jsx5("div", { children: "TopupForm: amount must be a positive number" }) : hasError ? /* @__PURE__ */ jsxs4("div", { children: [
1223
+ /* @__PURE__ */ jsx5("div", { children: "Top-up initialization failed" }),
1224
+ /* @__PURE__ */ jsx5("div", { children: topupError?.message || "Unknown error" })
1225
+ ] }) : hasStripeData && elementsOptions ? /* @__PURE__ */ jsx5(
1226
+ Elements2,
1227
+ {
1228
+ stripe: stripePromise,
1229
+ options: elementsOptions,
1230
+ children: /* @__PURE__ */ jsx5(
1231
+ StripePaymentFormWrapper,
909
1232
  {
910
- type: "submit",
911
- disabled: true,
912
- className: buttonClassName,
913
- "aria-busy": "false",
914
- "aria-disabled": "true",
915
- children: submitButtonText
1233
+ onSuccess: handleSuccess,
1234
+ onError: handleError,
1235
+ returnUrl: finalReturnUrl,
1236
+ submitButtonText,
1237
+ buttonClassName,
1238
+ clientSecret
916
1239
  }
917
1240
  )
918
- ] })
919
- ) });
1241
+ },
1242
+ clientSecret
1243
+ ) : /* @__PURE__ */ jsxs4("div", { children: [
1244
+ /* @__PURE__ */ jsx5(
1245
+ "div",
1246
+ {
1247
+ style: {
1248
+ minHeight: "40px",
1249
+ display: "flex",
1250
+ alignItems: "center",
1251
+ justifyContent: "center"
1252
+ },
1253
+ children: /* @__PURE__ */ jsx5(Spinner, { size: "md" })
1254
+ }
1255
+ ),
1256
+ /* @__PURE__ */ jsx5(
1257
+ "button",
1258
+ {
1259
+ type: "submit",
1260
+ disabled: true,
1261
+ className: buttonClassName,
1262
+ "aria-busy": "false",
1263
+ "aria-disabled": "true",
1264
+ children: submitButtonText
1265
+ }
1266
+ )
1267
+ ] }) });
920
1268
  };
921
1269
 
922
1270
  // src/components/ProductBadge.tsx
923
- import React4 from "react";
924
- import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
1271
+ import React5 from "react";
1272
+ import { Fragment as Fragment2, jsx as jsx6 } from "react/jsx-runtime";
925
1273
  var ProductBadge = ({
926
1274
  children,
927
1275
  as: Component = "div",
928
1276
  className
929
1277
  }) => {
930
1278
  const { purchases, loading, hasPaidPurchase, activePurchase } = usePurchase();
931
- const [hasLoadedOnce, setHasLoadedOnce] = React4.useState(false);
932
- React4.useEffect(() => {
1279
+ const [hasLoadedOnce, setHasLoadedOnce] = React5.useState(false);
1280
+ React5.useEffect(() => {
933
1281
  if (!loading) {
934
1282
  setHasLoadedOnce(true);
935
1283
  }
@@ -937,13 +1285,13 @@ var ProductBadge = ({
937
1285
  const planToDisplay = activePurchase?.productName || null;
938
1286
  const shouldShow = planToDisplay !== null && (!loading || hasLoadedOnce);
939
1287
  if (children) {
940
- return /* @__PURE__ */ jsx5(Fragment2, { children: children({ purchases, loading, displayPlan: planToDisplay, shouldShow }) });
1288
+ return /* @__PURE__ */ jsx6(Fragment2, { children: children({ purchases, loading, displayPlan: planToDisplay, shouldShow }) });
941
1289
  }
942
1290
  if (!shouldShow) {
943
1291
  return null;
944
1292
  }
945
1293
  const computedClassName = typeof className === "function" ? className({ purchases }) : className;
946
- return /* @__PURE__ */ jsx5(
1294
+ return /* @__PURE__ */ jsx6(
947
1295
  Component,
948
1296
  {
949
1297
  className: computedClassName,
@@ -961,12 +1309,12 @@ var ProductBadge = ({
961
1309
  var PlanBadge = ProductBadge;
962
1310
 
963
1311
  // src/components/PurchaseGate.tsx
964
- import { Fragment as Fragment3, jsx as jsx6 } from "react/jsx-runtime";
1312
+ import { Fragment as Fragment3, jsx as jsx7 } from "react/jsx-runtime";
965
1313
  var PurchaseGate = ({ requirePlan, requireProduct, children }) => {
966
1314
  const { purchases, loading, hasProduct } = usePurchase();
967
1315
  const productToCheck = requireProduct || requirePlan;
968
1316
  const hasAccess = productToCheck ? hasProduct(productToCheck) : purchases.some((p) => p.status === "active");
969
- return /* @__PURE__ */ jsx6(Fragment3, { children: children({
1317
+ return /* @__PURE__ */ jsx7(Fragment3, { children: children({
970
1318
  hasAccess,
971
1319
  purchases,
972
1320
  loading
@@ -974,35 +1322,103 @@ var PurchaseGate = ({ requirePlan, requireProduct, children }) => {
974
1322
  };
975
1323
 
976
1324
  // src/components/PricingSelector.tsx
977
- import { useCallback as useCallback5, useMemo as useMemo4 } from "react";
1325
+ import { useCallback as useCallback7, useMemo as useMemo5 } from "react";
978
1326
 
979
1327
  // src/hooks/usePlans.ts
980
- import { useState as useState5, useEffect as useEffect3, useCallback as useCallback4, useMemo as useMemo3, useRef as useRef4 } from "react";
1328
+ import { useState as useState6, useEffect as useEffect4, useCallback as useCallback6, useMemo as useMemo4, useRef as useRef6 } from "react";
981
1329
  var plansCache = /* @__PURE__ */ new Map();
982
1330
  var CACHE_DURATION2 = 5 * 60 * 1e3;
1331
+ function processPlans(raw, filter, sortBy) {
1332
+ let result = sortBy ? [...raw].sort(sortBy) : raw;
1333
+ if (filter) result = result.filter(filter);
1334
+ return result;
1335
+ }
1336
+ function computeInitialIndex(plans, initialPlanRef, autoSelectFirstPaid) {
1337
+ if (plans.length === 0) return 0;
1338
+ if (initialPlanRef) {
1339
+ const idx = plans.findIndex((p) => p.reference === initialPlanRef);
1340
+ if (idx >= 0) return idx;
1341
+ }
1342
+ if (autoSelectFirstPaid) {
1343
+ const idx = plans.findIndex((p) => p.requiresPayment !== false);
1344
+ return idx >= 0 ? idx : 0;
1345
+ }
1346
+ return 0;
1347
+ }
983
1348
  function usePlans(options) {
984
- const { fetcher, productRef, filter, sortBy, autoSelectFirstPaid = false } = options;
985
- const [plans, setPlans] = useState5([]);
986
- const [loading, setLoading] = useState5(true);
987
- const [error, setError] = useState5(null);
988
- const [selectedPlanIndex, setSelectedPlanIndex] = useState5(0);
989
- const fetcherRef = useRef4(fetcher);
990
- const filterRef = useRef4(filter);
991
- const sortByRef = useRef4(sortBy);
992
- const autoSelectFirstPaidRef = useRef4(autoSelectFirstPaid);
993
- useEffect3(() => {
1349
+ const {
1350
+ fetcher,
1351
+ productRef,
1352
+ filter,
1353
+ sortBy,
1354
+ autoSelectFirstPaid = false,
1355
+ initialPlanRef,
1356
+ selectionReady = true
1357
+ } = options;
1358
+ const fetcherRef = useRef6(fetcher);
1359
+ const filterRef = useRef6(filter);
1360
+ const sortByRef = useRef6(sortBy);
1361
+ const autoSelectFirstPaidRef = useRef6(autoSelectFirstPaid);
1362
+ const initialPlanRefRef = useRef6(initialPlanRef);
1363
+ const selectionReadyRef = useRef6(selectionReady);
1364
+ const hasAppliedInitialRef = useRef6(false);
1365
+ const userHasSelectedRef = useRef6(false);
1366
+ const [selectedPlanIndex, setSelectedPlanIndexState] = useState6(() => {
1367
+ if (!selectionReady || !productRef) return 0;
1368
+ const cached = plansCache.get(productRef);
1369
+ if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
1370
+ return 0;
1371
+ }
1372
+ const processed = processPlans(cached.plans, filter, sortBy);
1373
+ if (processed.length === 0) return 0;
1374
+ const idx = computeInitialIndex(processed, initialPlanRef, autoSelectFirstPaid);
1375
+ hasAppliedInitialRef.current = true;
1376
+ return idx;
1377
+ });
1378
+ const [plans, setPlans] = useState6(() => {
1379
+ if (!productRef) return [];
1380
+ const cached = plansCache.get(productRef);
1381
+ if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
1382
+ return [];
1383
+ }
1384
+ return processPlans(cached.plans, filter, sortBy);
1385
+ });
1386
+ const [loading, setLoading] = useState6(() => plans.length === 0);
1387
+ const [error, setError] = useState6(null);
1388
+ useEffect4(() => {
994
1389
  fetcherRef.current = fetcher;
995
1390
  }, [fetcher]);
996
- useEffect3(() => {
1391
+ useEffect4(() => {
997
1392
  filterRef.current = filter;
998
1393
  }, [filter]);
999
- useEffect3(() => {
1394
+ useEffect4(() => {
1000
1395
  sortByRef.current = sortBy;
1001
1396
  }, [sortBy]);
1002
- useEffect3(() => {
1397
+ useEffect4(() => {
1003
1398
  autoSelectFirstPaidRef.current = autoSelectFirstPaid;
1004
1399
  }, [autoSelectFirstPaid]);
1005
- const fetchPlans = useCallback4(
1400
+ useEffect4(() => {
1401
+ initialPlanRefRef.current = initialPlanRef;
1402
+ }, [initialPlanRef]);
1403
+ useEffect4(() => {
1404
+ selectionReadyRef.current = selectionReady;
1405
+ }, [selectionReady]);
1406
+ const setSelectedPlanIndex = useCallback6((index) => {
1407
+ userHasSelectedRef.current = true;
1408
+ setSelectedPlanIndexState(index);
1409
+ }, []);
1410
+ const applyInitialSelection = useCallback6((processedPlans) => {
1411
+ if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
1412
+ if (!selectionReadyRef.current || processedPlans.length === 0) return;
1413
+ hasAppliedInitialRef.current = true;
1414
+ const idx = computeInitialIndex(
1415
+ processedPlans,
1416
+ initialPlanRefRef.current,
1417
+ autoSelectFirstPaidRef.current
1418
+ );
1419
+ setSelectedPlanIndexState(idx);
1420
+ }, []);
1421
+ const fetchPlans = useCallback6(
1006
1422
  async (force = false) => {
1007
1423
  if (!productRef) {
1008
1424
  setError(new Error("Product reference not configured"));
@@ -1012,34 +1428,29 @@ function usePlans(options) {
1012
1428
  const cached = plansCache.get(productRef);
1013
1429
  const now = Date.now();
1014
1430
  if (!force && cached && now - cached.timestamp < CACHE_DURATION2) {
1015
- const cachedPlans = cached.plans;
1016
- let processedPlans = filterRef.current ? cachedPlans.filter(filterRef.current) : cachedPlans;
1017
- if (sortByRef.current) {
1018
- processedPlans = [...processedPlans].sort(sortByRef.current);
1019
- }
1431
+ const processedPlans = processPlans(
1432
+ cached.plans,
1433
+ filterRef.current,
1434
+ sortByRef.current
1435
+ );
1020
1436
  setPlans(processedPlans);
1021
1437
  setLoading(false);
1022
1438
  setError(null);
1023
- if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1024
- const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1025
- setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1026
- }
1439
+ applyInitialSelection(processedPlans);
1027
1440
  return;
1028
1441
  }
1029
1442
  if (cached?.promise) {
1030
1443
  try {
1031
1444
  setLoading(true);
1032
1445
  const fetchedPlans = await cached.promise;
1033
- let processedPlans = filterRef.current ? fetchedPlans.filter(filterRef.current) : fetchedPlans;
1034
- if (sortByRef.current) {
1035
- processedPlans = [...processedPlans].sort(sortByRef.current);
1036
- }
1446
+ const processedPlans = processPlans(
1447
+ fetchedPlans,
1448
+ filterRef.current,
1449
+ sortByRef.current
1450
+ );
1037
1451
  setPlans(processedPlans);
1038
1452
  setError(null);
1039
- if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1040
- const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1041
- setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1042
- }
1453
+ applyInitialSelection(processedPlans);
1043
1454
  } catch (err) {
1044
1455
  setError(err instanceof Error ? err : new Error("Failed to load plans"));
1045
1456
  } finally {
@@ -1051,26 +1462,16 @@ function usePlans(options) {
1051
1462
  setLoading(true);
1052
1463
  setError(null);
1053
1464
  const fetchPromise = fetcherRef.current(productRef);
1054
- plansCache.set(productRef, {
1055
- plans: [],
1056
- timestamp: now,
1057
- promise: fetchPromise
1058
- });
1465
+ plansCache.set(productRef, { plans: [], timestamp: now, promise: fetchPromise });
1059
1466
  const fetchedPlans = await fetchPromise;
1060
- plansCache.set(productRef, {
1061
- plans: fetchedPlans,
1062
- timestamp: now,
1063
- promise: null
1064
- });
1065
- let processedPlans = filterRef.current ? fetchedPlans.filter(filterRef.current) : fetchedPlans;
1066
- if (sortByRef.current) {
1067
- processedPlans = [...processedPlans].sort(sortByRef.current);
1068
- }
1467
+ plansCache.set(productRef, { plans: fetchedPlans, timestamp: now, promise: null });
1468
+ const processedPlans = processPlans(
1469
+ fetchedPlans,
1470
+ filterRef.current,
1471
+ sortByRef.current
1472
+ );
1069
1473
  setPlans(processedPlans);
1070
- if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1071
- const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1072
- setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1073
- }
1474
+ applyInitialSelection(processedPlans);
1074
1475
  } catch (err) {
1075
1476
  plansCache.delete(productRef);
1076
1477
  setError(err instanceof Error ? err : new Error("Failed to load plans"));
@@ -1078,22 +1479,25 @@ function usePlans(options) {
1078
1479
  setLoading(false);
1079
1480
  }
1080
1481
  },
1081
- [productRef]
1482
+ [productRef, applyInitialSelection]
1082
1483
  );
1083
- useEffect3(() => {
1484
+ useEffect4(() => {
1084
1485
  fetchPlans();
1085
1486
  }, [fetchPlans]);
1086
- const selectedPlan = useMemo3(() => {
1087
- return plans[selectedPlanIndex] || null;
1088
- }, [plans, selectedPlanIndex]);
1089
- const selectPlan = useCallback4(
1487
+ useEffect4(() => {
1488
+ if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
1489
+ if (!selectionReady || plans.length === 0) return;
1490
+ applyInitialSelection(plans);
1491
+ }, [selectionReady, plans, applyInitialSelection]);
1492
+ const selectedPlan = useMemo4(() => plans[selectedPlanIndex] || null, [plans, selectedPlanIndex]);
1493
+ const selectPlan = useCallback6(
1090
1494
  (planRef) => {
1091
1495
  const index = plans.findIndex((p) => p.reference === planRef);
1092
1496
  if (index >= 0) {
1093
1497
  setSelectedPlanIndex(index);
1094
1498
  }
1095
1499
  },
1096
- [plans]
1500
+ [plans, setSelectedPlanIndex]
1097
1501
  );
1098
1502
  return {
1099
1503
  plans,
@@ -1103,13 +1507,13 @@ function usePlans(options) {
1103
1507
  selectedPlan,
1104
1508
  setSelectedPlanIndex,
1105
1509
  selectPlan,
1106
- refetch: () => fetchPlans(true)
1107
- // Force refetch
1510
+ refetch: () => fetchPlans(true),
1511
+ isSelectionReady: hasAppliedInitialRef.current
1108
1512
  };
1109
1513
  }
1110
1514
 
1111
1515
  // src/components/PricingSelector.tsx
1112
- import { Fragment as Fragment4, jsx as jsx7 } from "react/jsx-runtime";
1516
+ import { Fragment as Fragment4, jsx as jsx8 } from "react/jsx-runtime";
1113
1517
  var PricingSelector = ({
1114
1518
  productRef,
1115
1519
  fetcher,
@@ -1127,26 +1531,26 @@ var PricingSelector = ({
1127
1531
  autoSelectFirstPaid
1128
1532
  });
1129
1533
  const { plans } = plansHook;
1130
- const isPaidPlan = useCallback5(
1534
+ const isPaidPlan = useCallback7(
1131
1535
  (planRef) => {
1132
1536
  const plan = plans.find((p) => p.reference === planRef);
1133
- return plan ? (plan.price ?? 0) > 0 && !plan.isFreeTier : true;
1537
+ return plan ? plan.requiresPayment !== false : true;
1134
1538
  },
1135
1539
  [plans]
1136
1540
  );
1137
- const activePurchase = useMemo4(() => {
1541
+ const activePurchase = useMemo5(() => {
1138
1542
  const activePurchases = purchases.filter((p) => p.status === "active");
1139
1543
  return activePurchases.sort(
1140
1544
  (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
1141
1545
  )[0] || null;
1142
1546
  }, [purchases]);
1143
- const isCurrentPlan = useCallback5(
1547
+ const isCurrentPlan = useCallback7(
1144
1548
  (planRef) => {
1145
1549
  return activePurchase?.planSnapshot?.reference === planRef;
1146
1550
  },
1147
1551
  [activePurchase]
1148
1552
  );
1149
- return /* @__PURE__ */ jsx7(Fragment4, { children: children({
1553
+ return /* @__PURE__ */ jsx8(Fragment4, { children: children({
1150
1554
  ...plansHook,
1151
1555
  purchases,
1152
1556
  isPaidPlan,
@@ -1155,14 +1559,58 @@ var PricingSelector = ({
1155
1559
  };
1156
1560
  var PlanSelector = PricingSelector;
1157
1561
 
1562
+ // src/hooks/useBalance.ts
1563
+ import { useEffect as useEffect5 } from "react";
1564
+ function useBalance() {
1565
+ const { balance } = useSolvaPay();
1566
+ useEffect5(() => {
1567
+ balance.refetch();
1568
+ }, [balance.refetch]);
1569
+ return balance;
1570
+ }
1571
+
1572
+ // src/components/BalanceBadge.tsx
1573
+ import { Fragment as Fragment5, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1574
+ function BalanceBadge({ className, numberOnly, children }) {
1575
+ const { credits, displayCurrency, creditsPerMinorUnit, displayExchangeRate, loading } = useBalance();
1576
+ if (children) {
1577
+ return /* @__PURE__ */ jsx9(Fragment5, { children: children({ credits, loading, displayCurrency, creditsPerMinorUnit }) });
1578
+ }
1579
+ if (loading) {
1580
+ return /* @__PURE__ */ jsx9("span", { className, "aria-busy": "true" });
1581
+ }
1582
+ if (credits == null) {
1583
+ return null;
1584
+ }
1585
+ const formattedCredits = new Intl.NumberFormat().format(credits);
1586
+ if (numberOnly) {
1587
+ return /* @__PURE__ */ jsx9("span", { className, children: formattedCredits });
1588
+ }
1589
+ let currencyEquivalent = "";
1590
+ if (displayCurrency && creditsPerMinorUnit) {
1591
+ const usdCents = credits / creditsPerMinorUnit;
1592
+ const displayMajorUnits = usdCents * (displayExchangeRate ?? 1) / 100;
1593
+ currencyEquivalent = ` (~${new Intl.NumberFormat(void 0, {
1594
+ style: "currency",
1595
+ currency: displayCurrency,
1596
+ minimumFractionDigits: 2
1597
+ }).format(displayMajorUnits)})`;
1598
+ }
1599
+ return /* @__PURE__ */ jsxs5("span", { className, children: [
1600
+ formattedCredits,
1601
+ " credits",
1602
+ currencyEquivalent
1603
+ ] });
1604
+ }
1605
+
1158
1606
  // src/hooks/usePurchaseStatus.ts
1159
- import { useMemo as useMemo5, useCallback as useCallback6 } from "react";
1607
+ import { useMemo as useMemo6, useCallback as useCallback8 } from "react";
1160
1608
  function usePurchaseStatus() {
1161
1609
  const { purchases } = usePurchase();
1162
- const isPaidPurchase2 = useCallback6((p) => {
1610
+ const isPaidPurchase2 = useCallback8((p) => {
1163
1611
  return (p.amount ?? 0) > 0;
1164
1612
  }, []);
1165
- const purchaseData = useMemo5(() => {
1613
+ const purchaseData = useMemo6(() => {
1166
1614
  const cancelledPaidPurchases = purchases.filter((p) => {
1167
1615
  return p.status === "active" && p.cancelledAt && isPaidPurchase2(p);
1168
1616
  });
@@ -1175,7 +1623,7 @@ function usePurchaseStatus() {
1175
1623
  shouldShowCancelledNotice
1176
1624
  };
1177
1625
  }, [purchases, isPaidPurchase2]);
1178
- const formatDate = useCallback6((dateString) => {
1626
+ const formatDate = useCallback8((dateString) => {
1179
1627
  if (!dateString) return null;
1180
1628
  return new Date(dateString).toLocaleDateString("en-US", {
1181
1629
  year: "numeric",
@@ -1183,7 +1631,7 @@ function usePurchaseStatus() {
1183
1631
  day: "numeric"
1184
1632
  });
1185
1633
  }, []);
1186
- const getDaysUntilExpiration = useCallback6((endDate) => {
1634
+ const getDaysUntilExpiration = useCallback8((endDate) => {
1187
1635
  if (!endDate) return null;
1188
1636
  const now = /* @__PURE__ */ new Date();
1189
1637
  const expiration = new Date(endDate);
@@ -1198,7 +1646,206 @@ function usePurchaseStatus() {
1198
1646
  getDaysUntilExpiration
1199
1647
  };
1200
1648
  }
1649
+
1650
+ // src/hooks/usePurchaseActions.ts
1651
+ import { useState as useState7, useCallback as useCallback9 } from "react";
1652
+ function usePurchaseActions() {
1653
+ const ctx = useSolvaPay();
1654
+ const [isCancelling, setIsCancelling] = useState7(false);
1655
+ const [isReactivating, setIsReactivating] = useState7(false);
1656
+ const [isActivating, setIsActivating] = useState7(false);
1657
+ const cancelRenewal = useCallback9(
1658
+ async (params) => {
1659
+ setIsCancelling(true);
1660
+ try {
1661
+ return await ctx.cancelRenewal(params);
1662
+ } finally {
1663
+ setIsCancelling(false);
1664
+ }
1665
+ },
1666
+ [ctx.cancelRenewal]
1667
+ );
1668
+ const reactivateRenewal = useCallback9(
1669
+ async (params) => {
1670
+ setIsReactivating(true);
1671
+ try {
1672
+ return await ctx.reactivateRenewal(params);
1673
+ } finally {
1674
+ setIsReactivating(false);
1675
+ }
1676
+ },
1677
+ [ctx.reactivateRenewal]
1678
+ );
1679
+ const activatePlan = useCallback9(
1680
+ async (params) => {
1681
+ setIsActivating(true);
1682
+ try {
1683
+ return await ctx.activatePlan(params);
1684
+ } finally {
1685
+ setIsActivating(false);
1686
+ }
1687
+ },
1688
+ [ctx.activatePlan]
1689
+ );
1690
+ return {
1691
+ cancelRenewal,
1692
+ reactivateRenewal,
1693
+ activatePlan,
1694
+ isCancelling,
1695
+ isReactivating,
1696
+ isActivating
1697
+ };
1698
+ }
1699
+
1700
+ // src/hooks/useActivation.ts
1701
+ import { useState as useState8, useCallback as useCallback10 } from "react";
1702
+ function useActivation() {
1703
+ const { activatePlan } = useSolvaPay();
1704
+ const [state, setState] = useState8("idle");
1705
+ const [error, setError] = useState8(null);
1706
+ const [result, setResult] = useState8(null);
1707
+ const activate = useCallback10(
1708
+ async (params) => {
1709
+ setState("activating");
1710
+ setError(null);
1711
+ setResult(null);
1712
+ try {
1713
+ const data = await activatePlan(params);
1714
+ setResult(data);
1715
+ switch (data.status) {
1716
+ case "activated":
1717
+ case "already_active":
1718
+ setState("activated");
1719
+ break;
1720
+ case "topup_required":
1721
+ setState("topup_required");
1722
+ break;
1723
+ case "payment_required":
1724
+ setError("This plan requires payment. Please select a different plan.");
1725
+ setState("payment_required");
1726
+ break;
1727
+ case "invalid":
1728
+ setError(data.message || "Invalid plan configuration.");
1729
+ setState("error");
1730
+ break;
1731
+ default:
1732
+ setError("Unexpected response from server.");
1733
+ setState("error");
1734
+ }
1735
+ } catch (err) {
1736
+ setError(err instanceof Error ? err.message : "Activation failed");
1737
+ setState("error");
1738
+ }
1739
+ },
1740
+ [activatePlan]
1741
+ );
1742
+ const reset = useCallback10(() => {
1743
+ setState("idle");
1744
+ setError(null);
1745
+ setResult(null);
1746
+ }, []);
1747
+ return { activate, state, error, result, reset };
1748
+ }
1749
+
1750
+ // src/hooks/useTopupAmountSelector.ts
1751
+ import { useState as useState9, useCallback as useCallback11, useMemo as useMemo7 } from "react";
1752
+ function getQuickAmounts(currency) {
1753
+ switch (currency.toUpperCase()) {
1754
+ case "SEK":
1755
+ case "NOK":
1756
+ case "DKK":
1757
+ return [100, 500, 1e3, 5e3];
1758
+ case "JPY":
1759
+ return [1e3, 5e3, 1e4, 5e4];
1760
+ case "KRW":
1761
+ return [1e4, 5e4, 1e5, 5e5];
1762
+ case "ISK":
1763
+ case "HUF":
1764
+ return [1e3, 5e3, 1e4, 5e4];
1765
+ default:
1766
+ return [10, 50, 100, 500];
1767
+ }
1768
+ }
1769
+ function getCurrencySymbol(currency) {
1770
+ try {
1771
+ const parts = new Intl.NumberFormat("en", { style: "currency", currency }).formatToParts(0);
1772
+ const sym = parts.find((p) => p.type === "currency");
1773
+ return sym?.value || currency;
1774
+ } catch {
1775
+ return currency;
1776
+ }
1777
+ }
1778
+ var DEFAULT_MIN = 1;
1779
+ var DEFAULT_MAX = 1e5;
1780
+ function useTopupAmountSelector(options) {
1781
+ const { currency, minAmount = DEFAULT_MIN, maxAmount = DEFAULT_MAX } = options;
1782
+ const [selectedAmount, setSelectedAmount] = useState9(null);
1783
+ const [customAmount, setCustomAmountRaw] = useState9("");
1784
+ const [error, setError] = useState9(null);
1785
+ const quickAmounts = useMemo7(() => getQuickAmounts(currency), [currency]);
1786
+ const currencySymbol = useMemo7(() => getCurrencySymbol(currency), [currency]);
1787
+ const resolvedAmount = useMemo7(() => {
1788
+ if (customAmount) {
1789
+ const parsed = parseFloat(customAmount);
1790
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
1791
+ }
1792
+ return selectedAmount;
1793
+ }, [selectedAmount, customAmount]);
1794
+ const selectQuickAmount = useCallback11(
1795
+ (amount) => {
1796
+ setSelectedAmount(amount);
1797
+ setCustomAmountRaw("");
1798
+ setError(null);
1799
+ },
1800
+ []
1801
+ );
1802
+ const setCustomAmount = useCallback11(
1803
+ (value) => {
1804
+ const sanitized = value.replace(/[^0-9.]/g, "");
1805
+ const dotIndex = sanitized.indexOf(".");
1806
+ const cleaned = dotIndex === -1 ? sanitized : sanitized.slice(0, dotIndex + 1) + sanitized.slice(dotIndex + 1).replace(/\./g, "");
1807
+ setCustomAmountRaw(cleaned);
1808
+ setSelectedAmount(null);
1809
+ setError(null);
1810
+ },
1811
+ []
1812
+ );
1813
+ const validate = useCallback11(() => {
1814
+ if (resolvedAmount == null) {
1815
+ setError("Please select or enter an amount");
1816
+ return false;
1817
+ }
1818
+ if (resolvedAmount < minAmount) {
1819
+ setError(`Minimum amount is ${currencySymbol}${minAmount}`);
1820
+ return false;
1821
+ }
1822
+ if (resolvedAmount > maxAmount) {
1823
+ setError(`Maximum amount is ${currencySymbol}${maxAmount.toLocaleString()}`);
1824
+ return false;
1825
+ }
1826
+ setError(null);
1827
+ return true;
1828
+ }, [resolvedAmount, minAmount, maxAmount, currencySymbol]);
1829
+ const reset = useCallback11(() => {
1830
+ setSelectedAmount(null);
1831
+ setCustomAmountRaw("");
1832
+ setError(null);
1833
+ }, []);
1834
+ return {
1835
+ quickAmounts,
1836
+ selectedAmount,
1837
+ customAmount,
1838
+ resolvedAmount,
1839
+ selectQuickAmount,
1840
+ setCustomAmount,
1841
+ error,
1842
+ validate,
1843
+ reset,
1844
+ currencySymbol
1845
+ };
1846
+ }
1201
1847
  export {
1848
+ BalanceBadge,
1202
1849
  PaymentForm,
1203
1850
  PlanBadge,
1204
1851
  PlanSelector,
@@ -1208,6 +1855,7 @@ export {
1208
1855
  SolvaPayProvider,
1209
1856
  Spinner,
1210
1857
  StripePaymentFormWrapper,
1858
+ TopupForm,
1211
1859
  defaultAuthAdapter,
1212
1860
  filterPurchases,
1213
1861
  getActivePurchases,
@@ -1215,10 +1863,15 @@ export {
1215
1863
  getMostRecentPurchase,
1216
1864
  getPrimaryPurchase,
1217
1865
  isPaidPurchase,
1866
+ useActivation,
1867
+ useBalance,
1218
1868
  useCheckout,
1219
1869
  useCustomer,
1220
1870
  usePlans,
1221
1871
  usePurchase,
1872
+ usePurchaseActions,
1222
1873
  usePurchaseStatus,
1223
- useSolvaPay
1874
+ useSolvaPay,
1875
+ useTopup,
1876
+ useTopupAmountSelector
1224
1877
  };