@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.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.tsx
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BalanceBadge: () => BalanceBadge,
33
34
  PaymentForm: () => PaymentForm,
34
35
  PlanBadge: () => PlanBadge,
35
36
  PlanSelector: () => PlanSelector,
@@ -39,6 +40,7 @@ __export(index_exports, {
39
40
  SolvaPayProvider: () => SolvaPayProvider,
40
41
  Spinner: () => Spinner,
41
42
  StripePaymentFormWrapper: () => StripePaymentFormWrapper,
43
+ TopupForm: () => TopupForm,
42
44
  defaultAuthAdapter: () => defaultAuthAdapter,
43
45
  filterPurchases: () => filterPurchases,
44
46
  getActivePurchases: () => getActivePurchases,
@@ -46,12 +48,17 @@ __export(index_exports, {
46
48
  getMostRecentPurchase: () => getMostRecentPurchase,
47
49
  getPrimaryPurchase: () => getPrimaryPurchase,
48
50
  isPaidPurchase: () => isPaidPurchase,
51
+ useActivation: () => useActivation,
52
+ useBalance: () => useBalance,
49
53
  useCheckout: () => useCheckout,
50
54
  useCustomer: () => useCustomer,
51
55
  usePlans: () => usePlans,
52
56
  usePurchase: () => usePurchase,
57
+ usePurchaseActions: () => usePurchaseActions,
53
58
  usePurchaseStatus: () => usePurchaseStatus,
54
- useSolvaPay: () => useSolvaPay
59
+ useSolvaPay: () => useSolvaPay,
60
+ useTopup: () => useTopup,
61
+ useTopupAmountSelector: () => useTopupAmountSelector
55
62
  });
56
63
  module.exports = __toCommonJS(index_exports);
57
64
 
@@ -110,13 +117,26 @@ var defaultAuthAdapter = {
110
117
  }
111
118
  };
112
119
 
113
- // src/SolvaPayProvider.tsx
114
- var import_jsx_runtime = require("react/jsx-runtime");
115
- var SolvaPayContext = (0, import_react.createContext)(null);
120
+ // src/utils/headers.ts
116
121
  var CUSTOMER_REF_KEY = "solvapay_customerRef";
117
122
  var CUSTOMER_REF_EXPIRY = "solvapay_customerRef_expiry";
118
123
  var CUSTOMER_REF_USER_ID_KEY = "solvapay_customerRef_userId";
119
- var CACHE_DURATION = 24 * 60 * 60 * 1e3;
124
+ function getAuthAdapter(config) {
125
+ if (config?.auth?.adapter) {
126
+ return config.auth.adapter;
127
+ }
128
+ if (config?.auth?.getToken || config?.auth?.getUserId) {
129
+ return {
130
+ async getToken() {
131
+ return config?.auth?.getToken?.() || null;
132
+ },
133
+ async getUserId() {
134
+ return config?.auth?.getUserId?.() || null;
135
+ }
136
+ };
137
+ }
138
+ return defaultAuthAdapter;
139
+ }
120
140
  function getCachedCustomerRef(userId) {
121
141
  if (typeof window === "undefined") return null;
122
142
  const cached = localStorage.getItem(CUSTOMER_REF_KEY);
@@ -137,6 +157,7 @@ function getCachedCustomerRef(userId) {
137
157
  }
138
158
  return cached;
139
159
  }
160
+ var CACHE_DURATION = 24 * 60 * 60 * 1e3;
140
161
  function setCachedCustomerRef(customerRef, userId) {
141
162
  if (typeof window === "undefined") return;
142
163
  if (userId === void 0 || userId === null) {
@@ -152,41 +173,62 @@ function clearCachedCustomerRef() {
152
173
  localStorage.removeItem(CUSTOMER_REF_EXPIRY);
153
174
  localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
154
175
  }
155
- function getAuthAdapter(config) {
156
- if (config?.auth?.adapter) {
157
- return config.auth.adapter;
176
+ async function buildRequestHeaders(config) {
177
+ const adapter = getAuthAdapter(config);
178
+ const token = await adapter.getToken();
179
+ const userId = await adapter.getUserId();
180
+ const cachedRef = getCachedCustomerRef(userId);
181
+ const headers = {
182
+ "Content-Type": "application/json"
183
+ };
184
+ if (token) {
185
+ headers["Authorization"] = `Bearer ${token}`;
158
186
  }
159
- if (config?.auth?.getToken || config?.auth?.getUserId) {
160
- return {
161
- async getToken() {
162
- return config?.auth?.getToken?.() || null;
163
- },
164
- async getUserId() {
165
- return config?.auth?.getUserId?.() || null;
166
- }
167
- };
187
+ if (cachedRef) {
188
+ headers["x-solvapay-customer-ref"] = cachedRef;
168
189
  }
169
- return defaultAuthAdapter;
190
+ if (config?.headers) {
191
+ const custom = typeof config.headers === "function" ? await config.headers() : config.headers;
192
+ Object.assign(headers, custom);
193
+ }
194
+ return { headers, userId };
170
195
  }
196
+
197
+ // src/SolvaPayProvider.tsx
198
+ var import_jsx_runtime = require("react/jsx-runtime");
199
+ var SolvaPayContext = (0, import_react.createContext)(null);
171
200
  var SolvaPayProvider = ({
172
201
  config,
173
202
  createPayment: customCreatePayment,
174
203
  checkPurchase: customCheckPurchase,
175
204
  processPayment: customProcessPayment,
205
+ createTopupPayment: customCreateTopupPayment,
176
206
  children
177
207
  }) => {
178
208
  const [purchaseData, setPurchaseData] = (0, import_react.useState)({
179
209
  purchases: []
180
210
  });
181
211
  const [loading, setLoading] = (0, import_react.useState)(false);
212
+ const [isRefetching, setIsRefetching] = (0, import_react.useState)(false);
213
+ const [purchaseError, setPurchaseError] = (0, import_react.useState)(null);
182
214
  const [internalCustomerRef, setInternalCustomerRef] = (0, import_react.useState)(void 0);
183
215
  const [userId, setUserId] = (0, import_react.useState)(null);
184
216
  const [isAuthenticated, setIsAuthenticated] = (0, import_react.useState)(false);
217
+ const [creditsValue, setCreditsValue] = (0, import_react.useState)(null);
218
+ const [displayCurrencyValue, setDisplayCurrencyValue] = (0, import_react.useState)(null);
219
+ const [creditsPerMinorUnitValue, setCreditsPerMinorUnitValue] = (0, import_react.useState)(null);
220
+ const [displayExchangeRateValue, setDisplayExchangeRateValue] = (0, import_react.useState)(null);
221
+ const [balanceLoading, setBalanceLoading] = (0, import_react.useState)(false);
222
+ const balanceInFlightRef = (0, import_react.useRef)(false);
223
+ const balanceLoadedRef = (0, import_react.useRef)(false);
224
+ const optimisticUntilRef = (0, import_react.useRef)(0);
225
+ const optimisticTimerRef = (0, import_react.useRef)(null);
226
+ const fetchBalanceRef = (0, import_react.useRef)(null);
185
227
  const inFlightRef = (0, import_react.useRef)(null);
186
- const lastFetchedRef = (0, import_react.useRef)(null);
187
228
  const checkPurchaseRef = (0, import_react.useRef)(null);
188
229
  const createPaymentRef = (0, import_react.useRef)(null);
189
230
  const processPaymentRef = (0, import_react.useRef)(null);
231
+ const createTopupPaymentRef = (0, import_react.useRef)(null);
190
232
  const configRef = (0, import_react.useRef)(config);
191
233
  const buildDefaultCheckPurchaseRef = (0, import_react.useRef)(
192
234
  null
@@ -203,31 +245,15 @@ var SolvaPayProvider = ({
203
245
  (0, import_react.useEffect)(() => {
204
246
  processPaymentRef.current = customProcessPayment || null;
205
247
  }, [customProcessPayment]);
248
+ (0, import_react.useEffect)(() => {
249
+ createTopupPaymentRef.current = customCreateTopupPayment || null;
250
+ }, [customCreateTopupPayment]);
206
251
  const buildDefaultCheckPurchase = (0, import_react.useCallback)(async () => {
207
252
  const currentConfig = configRef.current;
208
- const adapter = getAuthAdapter(currentConfig);
209
- const token = await adapter.getToken();
210
- const detectedUserId = await adapter.getUserId();
253
+ const { headers } = await buildRequestHeaders(currentConfig);
211
254
  const route = currentConfig?.api?.checkPurchase || "/api/check-purchase";
212
255
  const fetchFn = currentConfig?.fetch || fetch;
213
- const cachedRef = getCachedCustomerRef(detectedUserId);
214
- const headers = {
215
- "Content-Type": "application/json"
216
- };
217
- if (token) {
218
- headers["Authorization"] = `Bearer ${token}`;
219
- }
220
- if (cachedRef) {
221
- headers["x-solvapay-customer-ref"] = cachedRef;
222
- }
223
- if (currentConfig?.headers) {
224
- const customHeaders = typeof currentConfig.headers === "function" ? await currentConfig.headers() : currentConfig.headers;
225
- Object.assign(headers, customHeaders);
226
- }
227
- const res = await fetchFn(route, {
228
- method: "GET",
229
- headers
230
- });
256
+ const res = await fetchFn(route, { method: "GET", headers });
231
257
  if (!res.ok) {
232
258
  const error = new Error(`Failed to check purchase: ${res.statusText}`);
233
259
  currentConfig?.onError?.(error, "checkPurchase");
@@ -241,26 +267,13 @@ var SolvaPayProvider = ({
241
267
  const buildDefaultCreatePayment = (0, import_react.useCallback)(
242
268
  async (params) => {
243
269
  const currentConfig = configRef.current;
244
- const adapter = getAuthAdapter(currentConfig);
245
- const token = await adapter.getToken();
246
- const detectedUserId = await adapter.getUserId();
270
+ const { headers } = await buildRequestHeaders(currentConfig);
247
271
  const route = currentConfig?.api?.createPayment || "/api/create-payment-intent";
248
272
  const fetchFn = currentConfig?.fetch || fetch;
249
- const cachedRef = getCachedCustomerRef(detectedUserId);
250
- const headers = {
251
- "Content-Type": "application/json"
252
- };
253
- if (token) {
254
- headers["Authorization"] = `Bearer ${token}`;
255
- }
256
- if (cachedRef) {
257
- headers["x-solvapay-customer-ref"] = cachedRef;
273
+ const body = {};
274
+ if (params.planRef) {
275
+ body.planRef = params.planRef;
258
276
  }
259
- if (currentConfig?.headers) {
260
- const customHeaders = typeof currentConfig.headers === "function" ? await currentConfig.headers() : currentConfig.headers;
261
- Object.assign(headers, customHeaders);
262
- }
263
- const body = { planRef: params.planRef };
264
277
  if (params.productRef) {
265
278
  body.productRef = params.productRef;
266
279
  }
@@ -281,25 +294,9 @@ var SolvaPayProvider = ({
281
294
  const buildDefaultProcessPayment = (0, import_react.useCallback)(
282
295
  async (params) => {
283
296
  const currentConfig = configRef.current;
284
- const adapter = getAuthAdapter(currentConfig);
285
- const token = await adapter.getToken();
286
- const detectedUserId = await adapter.getUserId();
297
+ const { headers } = await buildRequestHeaders(currentConfig);
287
298
  const route = currentConfig?.api?.processPayment || "/api/process-payment";
288
299
  const fetchFn = currentConfig?.fetch || fetch;
289
- const cachedRef = getCachedCustomerRef(detectedUserId);
290
- const headers = {
291
- "Content-Type": "application/json"
292
- };
293
- if (token) {
294
- headers["Authorization"] = `Bearer ${token}`;
295
- }
296
- if (cachedRef) {
297
- headers["x-solvapay-customer-ref"] = cachedRef;
298
- }
299
- if (currentConfig?.headers) {
300
- const customHeaders = typeof currentConfig.headers === "function" ? await currentConfig.headers() : currentConfig.headers;
301
- Object.assign(headers, customHeaders);
302
- }
303
300
  const res = await fetchFn(route, {
304
301
  method: "POST",
305
302
  headers,
@@ -314,6 +311,86 @@ var SolvaPayProvider = ({
314
311
  },
315
312
  []
316
313
  );
314
+ const buildDefaultCreateTopupPayment = (0, import_react.useCallback)(
315
+ async (params) => {
316
+ const currentConfig = configRef.current;
317
+ const { headers } = await buildRequestHeaders(currentConfig);
318
+ const route = currentConfig?.api?.createTopupPayment || "/api/create-topup-payment-intent";
319
+ const fetchFn = currentConfig?.fetch || fetch;
320
+ const res = await fetchFn(route, {
321
+ method: "POST",
322
+ headers,
323
+ body: JSON.stringify({
324
+ amount: params.amount,
325
+ currency: params.currency
326
+ })
327
+ });
328
+ if (!res.ok) {
329
+ const error = new Error(`Failed to create topup payment: ${res.statusText}`);
330
+ currentConfig?.onError?.(error, "createTopupPayment");
331
+ throw error;
332
+ }
333
+ return res.json();
334
+ },
335
+ []
336
+ );
337
+ const fetchBalanceImpl = (0, import_react.useCallback)(async () => {
338
+ if (optimisticUntilRef.current > Date.now()) return;
339
+ if (!isAuthenticated && !internalCustomerRef) {
340
+ setCreditsValue(null);
341
+ setDisplayCurrencyValue(null);
342
+ setCreditsPerMinorUnitValue(null);
343
+ setDisplayExchangeRateValue(null);
344
+ setBalanceLoading(false);
345
+ balanceLoadedRef.current = false;
346
+ return;
347
+ }
348
+ if (balanceInFlightRef.current) return;
349
+ balanceInFlightRef.current = true;
350
+ if (!balanceLoadedRef.current) {
351
+ setBalanceLoading(true);
352
+ }
353
+ try {
354
+ const currentConfig = configRef.current;
355
+ const { headers } = await buildRequestHeaders(currentConfig);
356
+ const route = currentConfig?.api?.customerBalance || "/api/customer-balance";
357
+ const fetchFn = currentConfig?.fetch || fetch;
358
+ const res = await fetchFn(route, { method: "GET", headers });
359
+ if (!res.ok) {
360
+ console.error("[SolvaPayProvider] Failed to fetch balance:", res.statusText);
361
+ return;
362
+ }
363
+ const data = await res.json();
364
+ setCreditsValue(data.credits ?? null);
365
+ setDisplayCurrencyValue(data.displayCurrency ?? null);
366
+ setCreditsPerMinorUnitValue(data.creditsPerMinorUnit ?? null);
367
+ setDisplayExchangeRateValue(data.displayExchangeRate ?? null);
368
+ balanceLoadedRef.current = true;
369
+ } catch (error) {
370
+ console.error("[SolvaPayProvider] Failed to fetch balance:", error);
371
+ } finally {
372
+ setBalanceLoading(false);
373
+ balanceInFlightRef.current = false;
374
+ }
375
+ }, [isAuthenticated, internalCustomerRef]);
376
+ (0, import_react.useEffect)(() => {
377
+ fetchBalanceRef.current = fetchBalanceImpl;
378
+ }, [fetchBalanceImpl]);
379
+ (0, import_react.useEffect)(() => {
380
+ return () => {
381
+ if (optimisticTimerRef.current) clearTimeout(optimisticTimerRef.current);
382
+ };
383
+ }, []);
384
+ const OPTIMISTIC_GRACE_MS = 8e3;
385
+ const adjustBalanceImpl = (0, import_react.useCallback)((credits) => {
386
+ setCreditsValue((prev) => (prev ?? 0) + credits);
387
+ optimisticUntilRef.current = Date.now() + OPTIMISTIC_GRACE_MS;
388
+ if (optimisticTimerRef.current) clearTimeout(optimisticTimerRef.current);
389
+ optimisticTimerRef.current = setTimeout(() => {
390
+ optimisticUntilRef.current = 0;
391
+ fetchBalanceRef.current?.();
392
+ }, OPTIMISTIC_GRACE_MS);
393
+ }, []);
317
394
  const _checkPurchase = (0, import_react.useCallback)(async () => {
318
395
  if (checkPurchaseRef.current) {
319
396
  return checkPurchaseRef.current();
@@ -341,6 +418,15 @@ var SolvaPayProvider = ({
341
418
  },
342
419
  [buildDefaultProcessPayment]
343
420
  );
421
+ const createTopupPayment = (0, import_react.useCallback)(
422
+ async (params) => {
423
+ if (createTopupPaymentRef.current) {
424
+ return createTopupPaymentRef.current(params);
425
+ }
426
+ return buildDefaultCreateTopupPayment(params);
427
+ },
428
+ [buildDefaultCreateTopupPayment]
429
+ );
344
430
  (0, import_react.useEffect)(() => {
345
431
  const detectAuth = async () => {
346
432
  const currentConfig = configRef.current;
@@ -366,27 +452,30 @@ var SolvaPayProvider = ({
366
452
  }
367
453
  };
368
454
  detectAuth();
369
- const interval = setInterval(detectAuth, 5e3);
455
+ const interval = setInterval(detectAuth, 3e4);
370
456
  return () => clearInterval(interval);
371
457
  }, [userId]);
372
458
  const fetchPurchase = (0, import_react.useCallback)(
373
459
  async (force = false) => {
374
460
  if (!isAuthenticated && !internalCustomerRef) {
375
461
  setPurchaseData({ purchases: [] });
462
+ setPurchaseError(null);
376
463
  setLoading(false);
464
+ setIsRefetching(false);
377
465
  inFlightRef.current = null;
378
- lastFetchedRef.current = null;
379
466
  return;
380
467
  }
381
468
  const cacheKey = internalCustomerRef || userId || "anonymous";
382
- if (!force && lastFetchedRef.current === cacheKey && inFlightRef.current !== cacheKey) {
383
- return;
384
- }
385
- if (inFlightRef.current === cacheKey) {
469
+ if (inFlightRef.current === cacheKey && !force) {
386
470
  return;
387
471
  }
388
472
  inFlightRef.current = cacheKey;
389
- setLoading(true);
473
+ const hasExistingData = purchaseData.purchases.length > 0;
474
+ if (hasExistingData) {
475
+ setIsRefetching(true);
476
+ } else {
477
+ setLoading(true);
478
+ }
390
479
  try {
391
480
  const checkFn = checkPurchaseRef.current || buildDefaultCheckPurchaseRef.current;
392
481
  if (!checkFn) {
@@ -405,43 +494,111 @@ var SolvaPayProvider = ({
405
494
  purchases: filterPurchases(data.purchases || [])
406
495
  };
407
496
  setPurchaseData(filteredData);
408
- lastFetchedRef.current = cacheKey;
497
+ setPurchaseError(null);
409
498
  }
410
- } catch (error) {
411
- console.error("[SolvaPayProvider] Failed to fetch purchase:", error);
499
+ } catch (err) {
500
+ console.error("[SolvaPayProvider] Failed to fetch purchase:", err);
412
501
  if (inFlightRef.current === cacheKey) {
413
- setPurchaseData({
414
- purchases: []
415
- });
416
- lastFetchedRef.current = cacheKey;
502
+ setPurchaseError(err instanceof Error ? err : new Error(String(err)));
417
503
  }
418
504
  } finally {
419
505
  if (inFlightRef.current === cacheKey) {
420
506
  setLoading(false);
507
+ setIsRefetching(false);
421
508
  inFlightRef.current = null;
422
509
  }
423
510
  }
424
511
  },
425
- [isAuthenticated, internalCustomerRef, userId]
512
+ [isAuthenticated, internalCustomerRef, userId, purchaseData.purchases.length]
426
513
  );
427
514
  const fetchPurchaseRef = (0, import_react.useRef)(fetchPurchase);
428
515
  (0, import_react.useEffect)(() => {
429
516
  fetchPurchaseRef.current = fetchPurchase;
430
517
  }, [fetchPurchase]);
431
518
  const refetchPurchase = (0, import_react.useCallback)(async () => {
432
- lastFetchedRef.current = null;
433
519
  inFlightRef.current = null;
434
- setPurchaseData({ purchases: [] });
435
520
  await fetchPurchaseRef.current(true);
436
521
  }, []);
522
+ const cancelRenewal = (0, import_react.useCallback)(
523
+ async (params) => {
524
+ const currentConfig = configRef.current;
525
+ const { headers } = await buildRequestHeaders(currentConfig);
526
+ const route = currentConfig?.api?.cancelRenewal || "/api/cancel-renewal";
527
+ const fetchFn = currentConfig?.fetch || fetch;
528
+ const res = await fetchFn(route, {
529
+ method: "POST",
530
+ headers,
531
+ body: JSON.stringify(params)
532
+ });
533
+ if (!res.ok) {
534
+ const data = await res.json().catch(() => ({}));
535
+ const error = new Error(data.error || `Failed to cancel renewal: ${res.statusText}`);
536
+ currentConfig?.onError?.(error, "cancelRenewal");
537
+ throw error;
538
+ }
539
+ const result = await res.json();
540
+ await refetchPurchase();
541
+ return result;
542
+ },
543
+ [refetchPurchase]
544
+ );
545
+ const reactivateRenewal = (0, import_react.useCallback)(
546
+ async (params) => {
547
+ const currentConfig = configRef.current;
548
+ const { headers } = await buildRequestHeaders(currentConfig);
549
+ const route = currentConfig?.api?.reactivateRenewal || "/api/reactivate-renewal";
550
+ const fetchFn = currentConfig?.fetch || fetch;
551
+ const res = await fetchFn(route, {
552
+ method: "POST",
553
+ headers,
554
+ body: JSON.stringify(params)
555
+ });
556
+ if (!res.ok) {
557
+ const data = await res.json().catch(() => ({}));
558
+ const error = new Error(data.error || `Failed to reactivate renewal: ${res.statusText}`);
559
+ currentConfig?.onError?.(error, "reactivateRenewal");
560
+ throw error;
561
+ }
562
+ const result = await res.json();
563
+ await refetchPurchase();
564
+ return result;
565
+ },
566
+ [refetchPurchase]
567
+ );
568
+ const activatePlan = (0, import_react.useCallback)(
569
+ async (params) => {
570
+ const currentConfig = configRef.current;
571
+ const { headers } = await buildRequestHeaders(currentConfig);
572
+ const route = currentConfig?.api?.activatePlan || "/api/activate-plan";
573
+ const fetchFn = currentConfig?.fetch || fetch;
574
+ const res = await fetchFn(route, {
575
+ method: "POST",
576
+ headers,
577
+ body: JSON.stringify(params)
578
+ });
579
+ if (!res.ok) {
580
+ const data = await res.json().catch(() => ({}));
581
+ const error = new Error(data.error || `Failed to activate plan: ${res.statusText}`);
582
+ currentConfig?.onError?.(error, "activatePlan");
583
+ throw error;
584
+ }
585
+ const result = await res.json();
586
+ if (result.status === "activated" || result.status === "already_active") {
587
+ await refetchPurchase();
588
+ }
589
+ return result;
590
+ },
591
+ [refetchPurchase]
592
+ );
437
593
  (0, import_react.useEffect)(() => {
438
- lastFetchedRef.current = null;
439
594
  inFlightRef.current = null;
440
595
  if (isAuthenticated || internalCustomerRef) {
441
596
  fetchPurchase();
442
597
  } else {
443
598
  setPurchaseData({ purchases: [] });
599
+ setPurchaseError(null);
444
600
  setLoading(false);
601
+ setIsRefetching(false);
445
602
  }
446
603
  }, [isAuthenticated, internalCustomerRef, userId]);
447
604
  const updateCustomerRef = (0, import_react.useCallback)(
@@ -462,6 +619,8 @@ var SolvaPayProvider = ({
462
619
  )[0] || null;
463
620
  return {
464
621
  loading,
622
+ isRefetching,
623
+ error: purchaseError,
465
624
  customerRef: purchaseData.customerRef || internalCustomerRef,
466
625
  email: purchaseData.email,
467
626
  name: purchaseData.name,
@@ -480,24 +639,47 @@ var SolvaPayProvider = ({
480
639
  hasPaidPurchase: activePaidPurchases.length > 0,
481
640
  activePaidPurchase
482
641
  };
483
- }, [loading, purchaseData, internalCustomerRef]);
642
+ }, [loading, isRefetching, purchaseError, purchaseData, internalCustomerRef]);
643
+ const balance = (0, import_react.useMemo)(
644
+ () => ({
645
+ loading: balanceLoading,
646
+ credits: creditsValue,
647
+ displayCurrency: displayCurrencyValue,
648
+ creditsPerMinorUnit: creditsPerMinorUnitValue,
649
+ displayExchangeRate: displayExchangeRateValue,
650
+ refetch: fetchBalanceImpl,
651
+ adjustBalance: adjustBalanceImpl
652
+ }),
653
+ [balanceLoading, creditsValue, displayCurrencyValue, creditsPerMinorUnitValue, displayExchangeRateValue, fetchBalanceImpl, adjustBalanceImpl]
654
+ );
484
655
  const contextValue = (0, import_react.useMemo)(
485
656
  () => ({
486
657
  purchase,
487
658
  refetchPurchase,
488
659
  createPayment,
489
660
  processPayment,
661
+ createTopupPayment,
662
+ cancelRenewal,
663
+ reactivateRenewal,
664
+ activatePlan,
490
665
  customerRef: purchaseData.customerRef || internalCustomerRef,
491
- updateCustomerRef
666
+ updateCustomerRef,
667
+ balance,
668
+ _config: configRef.current
492
669
  }),
493
670
  [
494
671
  purchase,
495
672
  refetchPurchase,
496
673
  createPayment,
497
674
  processPayment,
675
+ createTopupPayment,
676
+ cancelRenewal,
677
+ reactivateRenewal,
678
+ activatePlan,
498
679
  purchaseData.customerRef,
499
680
  internalCustomerRef,
500
- updateCustomerRef
681
+ updateCustomerRef,
682
+ balance
501
683
  ]
502
684
  );
503
685
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SolvaPayContext.Provider, { value: contextValue, children });
@@ -528,29 +710,66 @@ var stripePromiseCache = /* @__PURE__ */ new Map();
528
710
  function getStripeCacheKey(publishableKey, accountId) {
529
711
  return accountId ? `${publishableKey}:${accountId}` : publishableKey;
530
712
  }
713
+ async function resolvePlanRef(productRef, fetchFn, headers, listPlansRoute) {
714
+ const url = `${listPlansRoute}?productRef=${encodeURIComponent(productRef)}`;
715
+ const res = await fetchFn(url, { method: "GET", headers });
716
+ if (!res.ok) {
717
+ throw new Error(`Failed to fetch plans for product "${productRef}": ${res.statusText}`);
718
+ }
719
+ const data = await res.json();
720
+ const allPlans = data.plans ?? [];
721
+ const activePlans = allPlans.filter((p) => p.isActive !== false && p.status !== "inactive");
722
+ if (activePlans.length === 0) {
723
+ throw new Error(
724
+ `No active plans found for product "${productRef}". Configure at least one plan in the SolvaPay Console.`
725
+ );
726
+ }
727
+ if (activePlans.length === 1) {
728
+ return activePlans[0].reference;
729
+ }
730
+ const defaultPlan = activePlans.find((p) => p.default === true);
731
+ if (defaultPlan) {
732
+ return defaultPlan.reference;
733
+ }
734
+ throw new Error(
735
+ `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.`
736
+ );
737
+ }
531
738
  function useCheckout(options) {
532
739
  const { planRef, productRef } = options;
533
- const { createPayment, customerRef, updateCustomerRef } = useSolvaPay();
740
+ const { createPayment, customerRef, updateCustomerRef, _config } = useSolvaPay();
534
741
  const [loading, setLoading] = (0, import_react3.useState)(false);
535
- const [error, setError] = (0, import_react3.useState)(
536
- !planRef || typeof planRef !== "string" ? new Error("useCheckout: planRef parameter is required and must be a string") : null
537
- );
742
+ const [error, setError] = (0, import_react3.useState)(null);
538
743
  const [stripePromise, setStripePromise] = (0, import_react3.useState)(null);
539
744
  const [clientSecret, setClientSecret] = (0, import_react3.useState)(null);
745
+ const [resolvedPlanRef, setResolvedPlanRef] = (0, import_react3.useState)(planRef || null);
540
746
  const isStartingRef = (0, import_react3.useRef)(false);
541
747
  const startCheckout = (0, import_react3.useCallback)(async () => {
542
748
  if (isStartingRef.current || loading) {
543
749
  return;
544
750
  }
545
- if (!planRef || typeof planRef !== "string") {
546
- setError(new Error("useCheckout: planRef parameter is required and must be a string"));
751
+ if (!planRef && !productRef) {
752
+ setError(
753
+ new Error(
754
+ "useCheckout: either planRef or productRef is required. Pass planRef directly, or pass productRef to auto-resolve the plan."
755
+ )
756
+ );
547
757
  return;
548
758
  }
549
759
  isStartingRef.current = true;
550
760
  setLoading(true);
551
761
  setError(null);
552
762
  try {
553
- const result = await createPayment({ planRef, productRef });
763
+ let effectivePlanRef = planRef;
764
+ if (!effectivePlanRef && productRef) {
765
+ const listPlansRoute = _config?.api?.listPlans || "/api/list-plans";
766
+ effectivePlanRef = await resolvePlanRef(productRef, fetch, {}, listPlansRoute);
767
+ setResolvedPlanRef(effectivePlanRef);
768
+ }
769
+ if (!effectivePlanRef) {
770
+ throw new Error("Could not determine plan reference for checkout");
771
+ }
772
+ const result = await createPayment({ planRef: effectivePlanRef, productRef });
554
773
  if (!result || typeof result !== "object") {
555
774
  throw new Error("Invalid payment intent response from server");
556
775
  }
@@ -579,19 +798,21 @@ function useCheckout(options) {
579
798
  setLoading(false);
580
799
  isStartingRef.current = false;
581
800
  }
582
- }, [planRef, productRef, createPayment, updateCustomerRef, loading]);
801
+ }, [planRef, productRef, createPayment, updateCustomerRef, loading, _config]);
583
802
  const reset = (0, import_react3.useCallback)(() => {
584
803
  isStartingRef.current = false;
585
804
  setLoading(false);
586
805
  setError(null);
587
806
  setStripePromise(null);
588
807
  setClientSecret(null);
589
- }, []);
808
+ setResolvedPlanRef(planRef || null);
809
+ }, [planRef]);
590
810
  return {
591
811
  loading,
592
812
  error,
593
813
  stripePromise,
594
814
  clientSecret,
815
+ resolvedPlanRef,
595
816
  startCheckout,
596
817
  reset
597
818
  };
@@ -759,14 +980,16 @@ var StripePaymentFormWrapper = ({
759
980
  const cardElementOptions = {
760
981
  style: {
761
982
  base: {
762
- fontSize: "16px",
763
- color: "#424770",
983
+ fontSize: "17px",
984
+ lineHeight: "24px",
985
+ color: "#1e293b",
986
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
764
987
  "::placeholder": {
765
- color: "#aab7c4"
988
+ color: "#94a3b8"
766
989
  }
767
990
  },
768
991
  invalid: {
769
- color: "#9e2146"
992
+ color: "#dc2626"
770
993
  }
771
994
  },
772
995
  hidePostalCode: true
@@ -786,22 +1009,7 @@ var StripePaymentFormWrapper = ({
786
1009
  }
787
1010
  }
788
1011
  }
789
- ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
790
- "div",
791
- {
792
- style: {
793
- padding: "12px",
794
- border: "1px solid #cbd5e1",
795
- borderRadius: "0.375rem",
796
- backgroundColor: "white",
797
- display: "flex",
798
- alignItems: "center",
799
- justifyContent: "center",
800
- minHeight: "40px"
801
- },
802
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: "sm" })
803
- }
804
- ),
1012
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "52px" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: "sm" }) }),
805
1013
  message && !isSuccess && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", "aria-live": "assertive", "aria-atomic": "true", children: message }),
806
1014
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
807
1015
  "button",
@@ -833,88 +1041,62 @@ var PaymentForm = ({
833
1041
  className,
834
1042
  buttonClassName
835
1043
  }) => {
836
- const validPlanRef = planRef && typeof planRef === "string" ? planRef : "";
837
1044
  const {
838
1045
  loading: checkoutLoading,
839
1046
  error: checkoutError,
840
1047
  clientSecret,
841
1048
  startCheckout,
842
- stripePromise
843
- } = useCheckout({ planRef: validPlanRef, productRef });
1049
+ stripePromise,
1050
+ resolvedPlanRef
1051
+ } = useCheckout({ planRef, productRef });
844
1052
  const { refetch } = usePurchase();
845
1053
  const { processPayment } = useSolvaPay();
846
1054
  const hasInitializedRef = (0, import_react5.useRef)(false);
1055
+ const hasPlanOrProduct = !!(planRef || productRef);
847
1056
  (0, import_react5.useEffect)(() => {
848
- if (!hasInitializedRef.current && validPlanRef && !checkoutLoading && !checkoutError && !clientSecret) {
1057
+ if (!hasInitializedRef.current && hasPlanOrProduct && !checkoutLoading && !checkoutError && !clientSecret) {
849
1058
  hasInitializedRef.current = true;
850
1059
  startCheckout().catch(() => {
851
1060
  hasInitializedRef.current = false;
852
1061
  });
853
1062
  }
854
- if (validPlanRef && clientSecret) {
1063
+ if (hasPlanOrProduct && clientSecret) {
855
1064
  hasInitializedRef.current = true;
856
1065
  }
857
- }, [validPlanRef, checkoutLoading, checkoutError, clientSecret, startCheckout]);
1066
+ }, [hasPlanOrProduct, checkoutLoading, checkoutError, clientSecret, startCheckout]);
1067
+ const effectivePlanRef = planRef || resolvedPlanRef;
858
1068
  const handleSuccess = (0, import_react5.useCallback)(
859
1069
  async (paymentIntent) => {
860
- let processingTimeout = false;
861
- let processingResult = null;
862
1070
  const paymentIntentAny = paymentIntent;
863
1071
  if (processPayment && productRef) {
864
1072
  try {
865
1073
  const result = await processPayment({
866
1074
  paymentIntentId: paymentIntentAny.id,
867
1075
  productRef,
868
- planRef
1076
+ planRef: effectivePlanRef || void 0
869
1077
  });
870
- processingResult = result;
871
1078
  const isTimeout = result?.status === "timeout";
872
- processingTimeout = isTimeout;
873
1079
  if (isTimeout) {
874
1080
  for (let attempt = 1; attempt <= 5; attempt++) {
875
- const delay = attempt * 1e3;
876
- await new Promise((resolve) => setTimeout(resolve, delay));
1081
+ await new Promise((resolve) => setTimeout(resolve, attempt * 1e3));
877
1082
  await refetch();
878
1083
  }
879
- if (onSuccess) {
880
- await onSuccess({
881
- ...paymentIntentAny,
882
- _processingTimeout: processingTimeout,
883
- _processingResult: processingResult
884
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
885
- });
886
- }
887
- throw new Error("Payment processing timed out");
888
- } else {
889
- await refetch();
1084
+ const err = new Error("Payment processing timed out \u2014 webhooks may not be configured");
1085
+ onError?.(err);
1086
+ return;
890
1087
  }
1088
+ await refetch();
891
1089
  } catch (error) {
892
1090
  console.error("[PaymentForm] Failed to process payment:", error);
893
- if (onSuccess) {
894
- try {
895
- await onSuccess({
896
- ...paymentIntentAny,
897
- _processingError: error
898
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
899
- });
900
- } catch {
901
- }
902
- }
903
- throw error;
1091
+ onError?.(error instanceof Error ? error : new Error(String(error)));
1092
+ return;
904
1093
  }
905
1094
  } else {
906
1095
  await refetch();
907
1096
  }
908
- if (onSuccess && !processingTimeout) {
909
- await onSuccess({
910
- ...paymentIntentAny,
911
- _processingTimeout: processingTimeout,
912
- _processingResult: processingResult
913
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
914
- });
915
- }
1097
+ onSuccess?.(paymentIntentAny);
916
1098
  },
917
- [processPayment, productRef, planRef, refetch, onSuccess]
1099
+ [processPayment, productRef, effectivePlanRef, refetch, onSuccess, onError]
918
1100
  );
919
1101
  const handleError = (0, import_react5.useCallback)(
920
1102
  (err) => {
@@ -925,7 +1107,6 @@ var PaymentForm = ({
925
1107
  [onError]
926
1108
  );
927
1109
  const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
928
- const isValidPlanRef = planRef && typeof planRef === "string";
929
1110
  const hasError = !!checkoutError;
930
1111
  const hasStripeData = !!(stripePromise && clientSecret);
931
1112
  const elementsOptions = (0, import_react5.useMemo)(() => {
@@ -939,72 +1120,246 @@ var PaymentForm = ({
939
1120
  }
940
1121
  }, [hasStripeData]);
941
1122
  const shouldRenderElements = hasStripeData || hasMountedElements && stripePromise && clientSecret;
942
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className, children: !isValidPlanRef ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: "PaymentForm: planRef is required and must be a string" }) : hasError ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
1123
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className, children: !hasPlanOrProduct ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: "PaymentForm: either planRef or productRef is required" }) : hasError ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
943
1124
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: "Payment initialization failed" }),
944
1125
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: checkoutError?.message || "Unknown error" })
945
- ] }) : shouldRenderElements && elementsOptions ? (
946
- // Once we have Stripe data, always render Elements to maintain hook consistency
947
- // This prevents hook count mismatches when transitioning between states
1126
+ ] }) : shouldRenderElements && elementsOptions ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1127
+ import_react_stripe_js2.Elements,
1128
+ {
1129
+ stripe: stripePromise,
1130
+ options: elementsOptions,
1131
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1132
+ StripePaymentFormWrapper,
1133
+ {
1134
+ onSuccess: handleSuccess,
1135
+ onError: handleError,
1136
+ returnUrl: finalReturnUrl,
1137
+ submitButtonText,
1138
+ buttonClassName,
1139
+ clientSecret
1140
+ }
1141
+ )
1142
+ },
1143
+ clientSecret
1144
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
948
1145
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
949
- import_react_stripe_js2.Elements,
1146
+ "div",
950
1147
  {
951
- stripe: stripePromise,
952
- options: elementsOptions,
953
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
954
- StripePaymentFormWrapper,
955
- {
956
- onSuccess: handleSuccess,
957
- onError: handleError,
958
- returnUrl: finalReturnUrl,
959
- submitButtonText,
960
- buttonClassName,
961
- clientSecret
962
- }
963
- )
964
- },
965
- clientSecret
1148
+ style: {
1149
+ minHeight: "40px",
1150
+ display: "flex",
1151
+ alignItems: "center",
1152
+ justifyContent: "center"
1153
+ },
1154
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Spinner, { size: "md" })
1155
+ }
1156
+ ),
1157
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1158
+ "button",
1159
+ {
1160
+ type: "submit",
1161
+ disabled: true,
1162
+ className: buttonClassName,
1163
+ "aria-busy": "false",
1164
+ "aria-disabled": "true",
1165
+ children: submitButtonText
1166
+ }
966
1167
  )
967
- ) : (
968
- // Loading state before Stripe data is available
969
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
970
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
971
- "div",
972
- {
973
- style: {
974
- minHeight: "40px",
975
- display: "flex",
976
- alignItems: "center",
977
- justifyContent: "center"
978
- },
979
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Spinner, { size: "md" })
980
- }
981
- ),
982
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
983
- "button",
1168
+ ] }) });
1169
+ };
1170
+
1171
+ // src/TopupForm.tsx
1172
+ var import_react7 = require("react");
1173
+ var import_react_stripe_js3 = require("@stripe/react-stripe-js");
1174
+
1175
+ // src/hooks/useTopup.ts
1176
+ var import_react6 = require("react");
1177
+ var import_stripe_js2 = require("@stripe/stripe-js");
1178
+ var stripePromiseCache2 = /* @__PURE__ */ new Map();
1179
+ function getStripeCacheKey2(publishableKey, accountId) {
1180
+ return accountId ? `${publishableKey}:${accountId}` : publishableKey;
1181
+ }
1182
+ function useTopup(options) {
1183
+ const { amount, currency } = options;
1184
+ const { createTopupPayment, customerRef, updateCustomerRef } = useSolvaPay();
1185
+ const [loading, setLoading] = (0, import_react6.useState)(false);
1186
+ const [error, setError] = (0, import_react6.useState)(null);
1187
+ const [stripePromise, setStripePromise] = (0, import_react6.useState)(null);
1188
+ const [clientSecret, setClientSecret] = (0, import_react6.useState)(null);
1189
+ const isStartingRef = (0, import_react6.useRef)(false);
1190
+ const startTopup = (0, import_react6.useCallback)(async () => {
1191
+ if (isStartingRef.current || loading) {
1192
+ return;
1193
+ }
1194
+ if (!amount || amount <= 0) {
1195
+ setError(new Error("useTopup: amount must be a positive number"));
1196
+ return;
1197
+ }
1198
+ isStartingRef.current = true;
1199
+ setLoading(true);
1200
+ setError(null);
1201
+ try {
1202
+ const result = await createTopupPayment({ amount, currency });
1203
+ if (!result || typeof result !== "object") {
1204
+ throw new Error("Invalid topup payment intent response from server");
1205
+ }
1206
+ if (!result.clientSecret || typeof result.clientSecret !== "string") {
1207
+ throw new Error("Invalid client secret in topup payment intent response");
1208
+ }
1209
+ if (!result.publishableKey || typeof result.publishableKey !== "string") {
1210
+ throw new Error("Invalid publishable key in topup payment intent response");
1211
+ }
1212
+ if (result.customerRef && result.customerRef !== customerRef && updateCustomerRef) {
1213
+ updateCustomerRef(result.customerRef);
1214
+ }
1215
+ const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
1216
+ const cacheKey = getStripeCacheKey2(result.publishableKey, result.accountId);
1217
+ let stripe = stripePromiseCache2.get(cacheKey);
1218
+ if (!stripe) {
1219
+ stripe = (0, import_stripe_js2.loadStripe)(result.publishableKey, stripeOptions);
1220
+ stripePromiseCache2.set(cacheKey, stripe);
1221
+ }
1222
+ setStripePromise(stripe);
1223
+ setClientSecret(result.clientSecret);
1224
+ } catch (err) {
1225
+ const error2 = err instanceof Error ? err : new Error("Failed to start topup");
1226
+ setError(error2);
1227
+ } finally {
1228
+ setLoading(false);
1229
+ isStartingRef.current = false;
1230
+ }
1231
+ }, [amount, currency, createTopupPayment, customerRef, updateCustomerRef, loading]);
1232
+ const reset = (0, import_react6.useCallback)(() => {
1233
+ isStartingRef.current = false;
1234
+ setLoading(false);
1235
+ setError(null);
1236
+ setStripePromise(null);
1237
+ setClientSecret(null);
1238
+ }, []);
1239
+ return {
1240
+ loading,
1241
+ error,
1242
+ stripePromise,
1243
+ clientSecret,
1244
+ startTopup,
1245
+ reset
1246
+ };
1247
+ }
1248
+
1249
+ // src/TopupForm.tsx
1250
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1251
+ var TopupForm = ({
1252
+ amount,
1253
+ currency,
1254
+ onSuccess,
1255
+ onError,
1256
+ returnUrl,
1257
+ submitButtonText = "Top Up",
1258
+ className,
1259
+ buttonClassName
1260
+ }) => {
1261
+ const {
1262
+ loading: topupLoading,
1263
+ error: topupError,
1264
+ clientSecret,
1265
+ startTopup,
1266
+ stripePromise
1267
+ } = useTopup({ amount, currency });
1268
+ const hasInitializedRef = (0, import_react7.useRef)(false);
1269
+ const hasAmount = amount > 0;
1270
+ (0, import_react7.useEffect)(() => {
1271
+ if (!hasInitializedRef.current && hasAmount && !topupLoading && !topupError && !clientSecret) {
1272
+ hasInitializedRef.current = true;
1273
+ startTopup().catch(() => {
1274
+ hasInitializedRef.current = false;
1275
+ });
1276
+ }
1277
+ if (hasAmount && clientSecret) {
1278
+ hasInitializedRef.current = true;
1279
+ }
1280
+ }, [hasAmount, topupLoading, topupError, clientSecret, startTopup]);
1281
+ const handleSuccess = (0, import_react7.useCallback)(
1282
+ async (paymentIntent) => {
1283
+ if (onSuccess) {
1284
+ await onSuccess(paymentIntent);
1285
+ }
1286
+ },
1287
+ [onSuccess]
1288
+ );
1289
+ const handleError = (0, import_react7.useCallback)(
1290
+ (err) => {
1291
+ if (onError) {
1292
+ onError(err);
1293
+ }
1294
+ },
1295
+ [onError]
1296
+ );
1297
+ const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
1298
+ const hasError = !!topupError;
1299
+ const hasStripeData = !!(stripePromise && clientSecret);
1300
+ const elementsOptions = (0, import_react7.useMemo)(() => {
1301
+ if (!clientSecret) return void 0;
1302
+ return { clientSecret };
1303
+ }, [clientSecret]);
1304
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className, children: !hasAmount ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: "TopupForm: amount must be a positive number" }) : hasError ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
1305
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: "Top-up initialization failed" }),
1306
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: topupError?.message || "Unknown error" })
1307
+ ] }) : hasStripeData && elementsOptions ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1308
+ import_react_stripe_js3.Elements,
1309
+ {
1310
+ stripe: stripePromise,
1311
+ options: elementsOptions,
1312
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1313
+ StripePaymentFormWrapper,
984
1314
  {
985
- type: "submit",
986
- disabled: true,
987
- className: buttonClassName,
988
- "aria-busy": "false",
989
- "aria-disabled": "true",
990
- children: submitButtonText
1315
+ onSuccess: handleSuccess,
1316
+ onError: handleError,
1317
+ returnUrl: finalReturnUrl,
1318
+ submitButtonText,
1319
+ buttonClassName,
1320
+ clientSecret
991
1321
  }
992
1322
  )
993
- ] })
994
- ) });
1323
+ },
1324
+ clientSecret
1325
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
1326
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1327
+ "div",
1328
+ {
1329
+ style: {
1330
+ minHeight: "40px",
1331
+ display: "flex",
1332
+ alignItems: "center",
1333
+ justifyContent: "center"
1334
+ },
1335
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Spinner, { size: "md" })
1336
+ }
1337
+ ),
1338
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1339
+ "button",
1340
+ {
1341
+ type: "submit",
1342
+ disabled: true,
1343
+ className: buttonClassName,
1344
+ "aria-busy": "false",
1345
+ "aria-disabled": "true",
1346
+ children: submitButtonText
1347
+ }
1348
+ )
1349
+ ] }) });
995
1350
  };
996
1351
 
997
1352
  // src/components/ProductBadge.tsx
998
- var import_react6 = __toESM(require("react"), 1);
999
- var import_jsx_runtime5 = require("react/jsx-runtime");
1353
+ var import_react8 = __toESM(require("react"), 1);
1354
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1000
1355
  var ProductBadge = ({
1001
1356
  children,
1002
1357
  as: Component = "div",
1003
1358
  className
1004
1359
  }) => {
1005
1360
  const { purchases, loading, hasPaidPurchase, activePurchase } = usePurchase();
1006
- const [hasLoadedOnce, setHasLoadedOnce] = import_react6.default.useState(false);
1007
- import_react6.default.useEffect(() => {
1361
+ const [hasLoadedOnce, setHasLoadedOnce] = import_react8.default.useState(false);
1362
+ import_react8.default.useEffect(() => {
1008
1363
  if (!loading) {
1009
1364
  setHasLoadedOnce(true);
1010
1365
  }
@@ -1012,13 +1367,13 @@ var ProductBadge = ({
1012
1367
  const planToDisplay = activePurchase?.productName || null;
1013
1368
  const shouldShow = planToDisplay !== null && (!loading || hasLoadedOnce);
1014
1369
  if (children) {
1015
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: children({ purchases, loading, displayPlan: planToDisplay, shouldShow }) });
1370
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: children({ purchases, loading, displayPlan: planToDisplay, shouldShow }) });
1016
1371
  }
1017
1372
  if (!shouldShow) {
1018
1373
  return null;
1019
1374
  }
1020
1375
  const computedClassName = typeof className === "function" ? className({ purchases }) : className;
1021
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1376
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1022
1377
  Component,
1023
1378
  {
1024
1379
  className: computedClassName,
@@ -1036,12 +1391,12 @@ var ProductBadge = ({
1036
1391
  var PlanBadge = ProductBadge;
1037
1392
 
1038
1393
  // src/components/PurchaseGate.tsx
1039
- var import_jsx_runtime6 = require("react/jsx-runtime");
1394
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1040
1395
  var PurchaseGate = ({ requirePlan, requireProduct, children }) => {
1041
1396
  const { purchases, loading, hasProduct } = usePurchase();
1042
1397
  const productToCheck = requireProduct || requirePlan;
1043
1398
  const hasAccess = productToCheck ? hasProduct(productToCheck) : purchases.some((p) => p.status === "active");
1044
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: children({
1399
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: children({
1045
1400
  hasAccess,
1046
1401
  purchases,
1047
1402
  loading
@@ -1049,35 +1404,103 @@ var PurchaseGate = ({ requirePlan, requireProduct, children }) => {
1049
1404
  };
1050
1405
 
1051
1406
  // src/components/PricingSelector.tsx
1052
- var import_react8 = require("react");
1407
+ var import_react10 = require("react");
1053
1408
 
1054
1409
  // src/hooks/usePlans.ts
1055
- var import_react7 = require("react");
1410
+ var import_react9 = require("react");
1056
1411
  var plansCache = /* @__PURE__ */ new Map();
1057
1412
  var CACHE_DURATION2 = 5 * 60 * 1e3;
1413
+ function processPlans(raw, filter, sortBy) {
1414
+ let result = sortBy ? [...raw].sort(sortBy) : raw;
1415
+ if (filter) result = result.filter(filter);
1416
+ return result;
1417
+ }
1418
+ function computeInitialIndex(plans, initialPlanRef, autoSelectFirstPaid) {
1419
+ if (plans.length === 0) return 0;
1420
+ if (initialPlanRef) {
1421
+ const idx = plans.findIndex((p) => p.reference === initialPlanRef);
1422
+ if (idx >= 0) return idx;
1423
+ }
1424
+ if (autoSelectFirstPaid) {
1425
+ const idx = plans.findIndex((p) => p.requiresPayment !== false);
1426
+ return idx >= 0 ? idx : 0;
1427
+ }
1428
+ return 0;
1429
+ }
1058
1430
  function usePlans(options) {
1059
- const { fetcher, productRef, filter, sortBy, autoSelectFirstPaid = false } = options;
1060
- const [plans, setPlans] = (0, import_react7.useState)([]);
1061
- const [loading, setLoading] = (0, import_react7.useState)(true);
1062
- const [error, setError] = (0, import_react7.useState)(null);
1063
- const [selectedPlanIndex, setSelectedPlanIndex] = (0, import_react7.useState)(0);
1064
- const fetcherRef = (0, import_react7.useRef)(fetcher);
1065
- const filterRef = (0, import_react7.useRef)(filter);
1066
- const sortByRef = (0, import_react7.useRef)(sortBy);
1067
- const autoSelectFirstPaidRef = (0, import_react7.useRef)(autoSelectFirstPaid);
1068
- (0, import_react7.useEffect)(() => {
1431
+ const {
1432
+ fetcher,
1433
+ productRef,
1434
+ filter,
1435
+ sortBy,
1436
+ autoSelectFirstPaid = false,
1437
+ initialPlanRef,
1438
+ selectionReady = true
1439
+ } = options;
1440
+ const fetcherRef = (0, import_react9.useRef)(fetcher);
1441
+ const filterRef = (0, import_react9.useRef)(filter);
1442
+ const sortByRef = (0, import_react9.useRef)(sortBy);
1443
+ const autoSelectFirstPaidRef = (0, import_react9.useRef)(autoSelectFirstPaid);
1444
+ const initialPlanRefRef = (0, import_react9.useRef)(initialPlanRef);
1445
+ const selectionReadyRef = (0, import_react9.useRef)(selectionReady);
1446
+ const hasAppliedInitialRef = (0, import_react9.useRef)(false);
1447
+ const userHasSelectedRef = (0, import_react9.useRef)(false);
1448
+ const [selectedPlanIndex, setSelectedPlanIndexState] = (0, import_react9.useState)(() => {
1449
+ if (!selectionReady || !productRef) return 0;
1450
+ const cached = plansCache.get(productRef);
1451
+ if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
1452
+ return 0;
1453
+ }
1454
+ const processed = processPlans(cached.plans, filter, sortBy);
1455
+ if (processed.length === 0) return 0;
1456
+ const idx = computeInitialIndex(processed, initialPlanRef, autoSelectFirstPaid);
1457
+ hasAppliedInitialRef.current = true;
1458
+ return idx;
1459
+ });
1460
+ const [plans, setPlans] = (0, import_react9.useState)(() => {
1461
+ if (!productRef) return [];
1462
+ const cached = plansCache.get(productRef);
1463
+ if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
1464
+ return [];
1465
+ }
1466
+ return processPlans(cached.plans, filter, sortBy);
1467
+ });
1468
+ const [loading, setLoading] = (0, import_react9.useState)(() => plans.length === 0);
1469
+ const [error, setError] = (0, import_react9.useState)(null);
1470
+ (0, import_react9.useEffect)(() => {
1069
1471
  fetcherRef.current = fetcher;
1070
1472
  }, [fetcher]);
1071
- (0, import_react7.useEffect)(() => {
1473
+ (0, import_react9.useEffect)(() => {
1072
1474
  filterRef.current = filter;
1073
1475
  }, [filter]);
1074
- (0, import_react7.useEffect)(() => {
1476
+ (0, import_react9.useEffect)(() => {
1075
1477
  sortByRef.current = sortBy;
1076
1478
  }, [sortBy]);
1077
- (0, import_react7.useEffect)(() => {
1479
+ (0, import_react9.useEffect)(() => {
1078
1480
  autoSelectFirstPaidRef.current = autoSelectFirstPaid;
1079
1481
  }, [autoSelectFirstPaid]);
1080
- const fetchPlans = (0, import_react7.useCallback)(
1482
+ (0, import_react9.useEffect)(() => {
1483
+ initialPlanRefRef.current = initialPlanRef;
1484
+ }, [initialPlanRef]);
1485
+ (0, import_react9.useEffect)(() => {
1486
+ selectionReadyRef.current = selectionReady;
1487
+ }, [selectionReady]);
1488
+ const setSelectedPlanIndex = (0, import_react9.useCallback)((index) => {
1489
+ userHasSelectedRef.current = true;
1490
+ setSelectedPlanIndexState(index);
1491
+ }, []);
1492
+ const applyInitialSelection = (0, import_react9.useCallback)((processedPlans) => {
1493
+ if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
1494
+ if (!selectionReadyRef.current || processedPlans.length === 0) return;
1495
+ hasAppliedInitialRef.current = true;
1496
+ const idx = computeInitialIndex(
1497
+ processedPlans,
1498
+ initialPlanRefRef.current,
1499
+ autoSelectFirstPaidRef.current
1500
+ );
1501
+ setSelectedPlanIndexState(idx);
1502
+ }, []);
1503
+ const fetchPlans = (0, import_react9.useCallback)(
1081
1504
  async (force = false) => {
1082
1505
  if (!productRef) {
1083
1506
  setError(new Error("Product reference not configured"));
@@ -1087,34 +1510,29 @@ function usePlans(options) {
1087
1510
  const cached = plansCache.get(productRef);
1088
1511
  const now = Date.now();
1089
1512
  if (!force && cached && now - cached.timestamp < CACHE_DURATION2) {
1090
- const cachedPlans = cached.plans;
1091
- let processedPlans = filterRef.current ? cachedPlans.filter(filterRef.current) : cachedPlans;
1092
- if (sortByRef.current) {
1093
- processedPlans = [...processedPlans].sort(sortByRef.current);
1094
- }
1513
+ const processedPlans = processPlans(
1514
+ cached.plans,
1515
+ filterRef.current,
1516
+ sortByRef.current
1517
+ );
1095
1518
  setPlans(processedPlans);
1096
1519
  setLoading(false);
1097
1520
  setError(null);
1098
- if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1099
- const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1100
- setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1101
- }
1521
+ applyInitialSelection(processedPlans);
1102
1522
  return;
1103
1523
  }
1104
1524
  if (cached?.promise) {
1105
1525
  try {
1106
1526
  setLoading(true);
1107
1527
  const fetchedPlans = await cached.promise;
1108
- let processedPlans = filterRef.current ? fetchedPlans.filter(filterRef.current) : fetchedPlans;
1109
- if (sortByRef.current) {
1110
- processedPlans = [...processedPlans].sort(sortByRef.current);
1111
- }
1528
+ const processedPlans = processPlans(
1529
+ fetchedPlans,
1530
+ filterRef.current,
1531
+ sortByRef.current
1532
+ );
1112
1533
  setPlans(processedPlans);
1113
1534
  setError(null);
1114
- if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1115
- const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1116
- setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1117
- }
1535
+ applyInitialSelection(processedPlans);
1118
1536
  } catch (err) {
1119
1537
  setError(err instanceof Error ? err : new Error("Failed to load plans"));
1120
1538
  } finally {
@@ -1126,26 +1544,16 @@ function usePlans(options) {
1126
1544
  setLoading(true);
1127
1545
  setError(null);
1128
1546
  const fetchPromise = fetcherRef.current(productRef);
1129
- plansCache.set(productRef, {
1130
- plans: [],
1131
- timestamp: now,
1132
- promise: fetchPromise
1133
- });
1547
+ plansCache.set(productRef, { plans: [], timestamp: now, promise: fetchPromise });
1134
1548
  const fetchedPlans = await fetchPromise;
1135
- plansCache.set(productRef, {
1136
- plans: fetchedPlans,
1137
- timestamp: now,
1138
- promise: null
1139
- });
1140
- let processedPlans = filterRef.current ? fetchedPlans.filter(filterRef.current) : fetchedPlans;
1141
- if (sortByRef.current) {
1142
- processedPlans = [...processedPlans].sort(sortByRef.current);
1143
- }
1549
+ plansCache.set(productRef, { plans: fetchedPlans, timestamp: now, promise: null });
1550
+ const processedPlans = processPlans(
1551
+ fetchedPlans,
1552
+ filterRef.current,
1553
+ sortByRef.current
1554
+ );
1144
1555
  setPlans(processedPlans);
1145
- if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1146
- const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1147
- setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1148
- }
1556
+ applyInitialSelection(processedPlans);
1149
1557
  } catch (err) {
1150
1558
  plansCache.delete(productRef);
1151
1559
  setError(err instanceof Error ? err : new Error("Failed to load plans"));
@@ -1153,22 +1561,25 @@ function usePlans(options) {
1153
1561
  setLoading(false);
1154
1562
  }
1155
1563
  },
1156
- [productRef]
1564
+ [productRef, applyInitialSelection]
1157
1565
  );
1158
- (0, import_react7.useEffect)(() => {
1566
+ (0, import_react9.useEffect)(() => {
1159
1567
  fetchPlans();
1160
1568
  }, [fetchPlans]);
1161
- const selectedPlan = (0, import_react7.useMemo)(() => {
1162
- return plans[selectedPlanIndex] || null;
1163
- }, [plans, selectedPlanIndex]);
1164
- const selectPlan = (0, import_react7.useCallback)(
1569
+ (0, import_react9.useEffect)(() => {
1570
+ if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
1571
+ if (!selectionReady || plans.length === 0) return;
1572
+ applyInitialSelection(plans);
1573
+ }, [selectionReady, plans, applyInitialSelection]);
1574
+ const selectedPlan = (0, import_react9.useMemo)(() => plans[selectedPlanIndex] || null, [plans, selectedPlanIndex]);
1575
+ const selectPlan = (0, import_react9.useCallback)(
1165
1576
  (planRef) => {
1166
1577
  const index = plans.findIndex((p) => p.reference === planRef);
1167
1578
  if (index >= 0) {
1168
1579
  setSelectedPlanIndex(index);
1169
1580
  }
1170
1581
  },
1171
- [plans]
1582
+ [plans, setSelectedPlanIndex]
1172
1583
  );
1173
1584
  return {
1174
1585
  plans,
@@ -1178,13 +1589,13 @@ function usePlans(options) {
1178
1589
  selectedPlan,
1179
1590
  setSelectedPlanIndex,
1180
1591
  selectPlan,
1181
- refetch: () => fetchPlans(true)
1182
- // Force refetch
1592
+ refetch: () => fetchPlans(true),
1593
+ isSelectionReady: hasAppliedInitialRef.current
1183
1594
  };
1184
1595
  }
1185
1596
 
1186
1597
  // src/components/PricingSelector.tsx
1187
- var import_jsx_runtime7 = require("react/jsx-runtime");
1598
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1188
1599
  var PricingSelector = ({
1189
1600
  productRef,
1190
1601
  fetcher,
@@ -1202,26 +1613,26 @@ var PricingSelector = ({
1202
1613
  autoSelectFirstPaid
1203
1614
  });
1204
1615
  const { plans } = plansHook;
1205
- const isPaidPlan = (0, import_react8.useCallback)(
1616
+ const isPaidPlan = (0, import_react10.useCallback)(
1206
1617
  (planRef) => {
1207
1618
  const plan = plans.find((p) => p.reference === planRef);
1208
- return plan ? (plan.price ?? 0) > 0 && !plan.isFreeTier : true;
1619
+ return plan ? plan.requiresPayment !== false : true;
1209
1620
  },
1210
1621
  [plans]
1211
1622
  );
1212
- const activePurchase = (0, import_react8.useMemo)(() => {
1623
+ const activePurchase = (0, import_react10.useMemo)(() => {
1213
1624
  const activePurchases = purchases.filter((p) => p.status === "active");
1214
1625
  return activePurchases.sort(
1215
1626
  (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
1216
1627
  )[0] || null;
1217
1628
  }, [purchases]);
1218
- const isCurrentPlan = (0, import_react8.useCallback)(
1629
+ const isCurrentPlan = (0, import_react10.useCallback)(
1219
1630
  (planRef) => {
1220
1631
  return activePurchase?.planSnapshot?.reference === planRef;
1221
1632
  },
1222
1633
  [activePurchase]
1223
1634
  );
1224
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: children({
1635
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_jsx_runtime8.Fragment, { children: children({
1225
1636
  ...plansHook,
1226
1637
  purchases,
1227
1638
  isPaidPlan,
@@ -1230,14 +1641,58 @@ var PricingSelector = ({
1230
1641
  };
1231
1642
  var PlanSelector = PricingSelector;
1232
1643
 
1644
+ // src/hooks/useBalance.ts
1645
+ var import_react11 = require("react");
1646
+ function useBalance() {
1647
+ const { balance } = useSolvaPay();
1648
+ (0, import_react11.useEffect)(() => {
1649
+ balance.refetch();
1650
+ }, [balance.refetch]);
1651
+ return balance;
1652
+ }
1653
+
1654
+ // src/components/BalanceBadge.tsx
1655
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1656
+ function BalanceBadge({ className, numberOnly, children }) {
1657
+ const { credits, displayCurrency, creditsPerMinorUnit, displayExchangeRate, loading } = useBalance();
1658
+ if (children) {
1659
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_jsx_runtime9.Fragment, { children: children({ credits, loading, displayCurrency, creditsPerMinorUnit }) });
1660
+ }
1661
+ if (loading) {
1662
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className, "aria-busy": "true" });
1663
+ }
1664
+ if (credits == null) {
1665
+ return null;
1666
+ }
1667
+ const formattedCredits = new Intl.NumberFormat().format(credits);
1668
+ if (numberOnly) {
1669
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className, children: formattedCredits });
1670
+ }
1671
+ let currencyEquivalent = "";
1672
+ if (displayCurrency && creditsPerMinorUnit) {
1673
+ const usdCents = credits / creditsPerMinorUnit;
1674
+ const displayMajorUnits = usdCents * (displayExchangeRate ?? 1) / 100;
1675
+ currencyEquivalent = ` (~${new Intl.NumberFormat(void 0, {
1676
+ style: "currency",
1677
+ currency: displayCurrency,
1678
+ minimumFractionDigits: 2
1679
+ }).format(displayMajorUnits)})`;
1680
+ }
1681
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className, children: [
1682
+ formattedCredits,
1683
+ " credits",
1684
+ currencyEquivalent
1685
+ ] });
1686
+ }
1687
+
1233
1688
  // src/hooks/usePurchaseStatus.ts
1234
- var import_react9 = require("react");
1689
+ var import_react12 = require("react");
1235
1690
  function usePurchaseStatus() {
1236
1691
  const { purchases } = usePurchase();
1237
- const isPaidPurchase2 = (0, import_react9.useCallback)((p) => {
1692
+ const isPaidPurchase2 = (0, import_react12.useCallback)((p) => {
1238
1693
  return (p.amount ?? 0) > 0;
1239
1694
  }, []);
1240
- const purchaseData = (0, import_react9.useMemo)(() => {
1695
+ const purchaseData = (0, import_react12.useMemo)(() => {
1241
1696
  const cancelledPaidPurchases = purchases.filter((p) => {
1242
1697
  return p.status === "active" && p.cancelledAt && isPaidPurchase2(p);
1243
1698
  });
@@ -1250,7 +1705,7 @@ function usePurchaseStatus() {
1250
1705
  shouldShowCancelledNotice
1251
1706
  };
1252
1707
  }, [purchases, isPaidPurchase2]);
1253
- const formatDate = (0, import_react9.useCallback)((dateString) => {
1708
+ const formatDate = (0, import_react12.useCallback)((dateString) => {
1254
1709
  if (!dateString) return null;
1255
1710
  return new Date(dateString).toLocaleDateString("en-US", {
1256
1711
  year: "numeric",
@@ -1258,7 +1713,7 @@ function usePurchaseStatus() {
1258
1713
  day: "numeric"
1259
1714
  });
1260
1715
  }, []);
1261
- const getDaysUntilExpiration = (0, import_react9.useCallback)((endDate) => {
1716
+ const getDaysUntilExpiration = (0, import_react12.useCallback)((endDate) => {
1262
1717
  if (!endDate) return null;
1263
1718
  const now = /* @__PURE__ */ new Date();
1264
1719
  const expiration = new Date(endDate);
@@ -1273,8 +1728,207 @@ function usePurchaseStatus() {
1273
1728
  getDaysUntilExpiration
1274
1729
  };
1275
1730
  }
1731
+
1732
+ // src/hooks/usePurchaseActions.ts
1733
+ var import_react13 = require("react");
1734
+ function usePurchaseActions() {
1735
+ const ctx = useSolvaPay();
1736
+ const [isCancelling, setIsCancelling] = (0, import_react13.useState)(false);
1737
+ const [isReactivating, setIsReactivating] = (0, import_react13.useState)(false);
1738
+ const [isActivating, setIsActivating] = (0, import_react13.useState)(false);
1739
+ const cancelRenewal = (0, import_react13.useCallback)(
1740
+ async (params) => {
1741
+ setIsCancelling(true);
1742
+ try {
1743
+ return await ctx.cancelRenewal(params);
1744
+ } finally {
1745
+ setIsCancelling(false);
1746
+ }
1747
+ },
1748
+ [ctx.cancelRenewal]
1749
+ );
1750
+ const reactivateRenewal = (0, import_react13.useCallback)(
1751
+ async (params) => {
1752
+ setIsReactivating(true);
1753
+ try {
1754
+ return await ctx.reactivateRenewal(params);
1755
+ } finally {
1756
+ setIsReactivating(false);
1757
+ }
1758
+ },
1759
+ [ctx.reactivateRenewal]
1760
+ );
1761
+ const activatePlan = (0, import_react13.useCallback)(
1762
+ async (params) => {
1763
+ setIsActivating(true);
1764
+ try {
1765
+ return await ctx.activatePlan(params);
1766
+ } finally {
1767
+ setIsActivating(false);
1768
+ }
1769
+ },
1770
+ [ctx.activatePlan]
1771
+ );
1772
+ return {
1773
+ cancelRenewal,
1774
+ reactivateRenewal,
1775
+ activatePlan,
1776
+ isCancelling,
1777
+ isReactivating,
1778
+ isActivating
1779
+ };
1780
+ }
1781
+
1782
+ // src/hooks/useActivation.ts
1783
+ var import_react14 = require("react");
1784
+ function useActivation() {
1785
+ const { activatePlan } = useSolvaPay();
1786
+ const [state, setState] = (0, import_react14.useState)("idle");
1787
+ const [error, setError] = (0, import_react14.useState)(null);
1788
+ const [result, setResult] = (0, import_react14.useState)(null);
1789
+ const activate = (0, import_react14.useCallback)(
1790
+ async (params) => {
1791
+ setState("activating");
1792
+ setError(null);
1793
+ setResult(null);
1794
+ try {
1795
+ const data = await activatePlan(params);
1796
+ setResult(data);
1797
+ switch (data.status) {
1798
+ case "activated":
1799
+ case "already_active":
1800
+ setState("activated");
1801
+ break;
1802
+ case "topup_required":
1803
+ setState("topup_required");
1804
+ break;
1805
+ case "payment_required":
1806
+ setError("This plan requires payment. Please select a different plan.");
1807
+ setState("payment_required");
1808
+ break;
1809
+ case "invalid":
1810
+ setError(data.message || "Invalid plan configuration.");
1811
+ setState("error");
1812
+ break;
1813
+ default:
1814
+ setError("Unexpected response from server.");
1815
+ setState("error");
1816
+ }
1817
+ } catch (err) {
1818
+ setError(err instanceof Error ? err.message : "Activation failed");
1819
+ setState("error");
1820
+ }
1821
+ },
1822
+ [activatePlan]
1823
+ );
1824
+ const reset = (0, import_react14.useCallback)(() => {
1825
+ setState("idle");
1826
+ setError(null);
1827
+ setResult(null);
1828
+ }, []);
1829
+ return { activate, state, error, result, reset };
1830
+ }
1831
+
1832
+ // src/hooks/useTopupAmountSelector.ts
1833
+ var import_react15 = require("react");
1834
+ function getQuickAmounts(currency) {
1835
+ switch (currency.toUpperCase()) {
1836
+ case "SEK":
1837
+ case "NOK":
1838
+ case "DKK":
1839
+ return [100, 500, 1e3, 5e3];
1840
+ case "JPY":
1841
+ return [1e3, 5e3, 1e4, 5e4];
1842
+ case "KRW":
1843
+ return [1e4, 5e4, 1e5, 5e5];
1844
+ case "ISK":
1845
+ case "HUF":
1846
+ return [1e3, 5e3, 1e4, 5e4];
1847
+ default:
1848
+ return [10, 50, 100, 500];
1849
+ }
1850
+ }
1851
+ function getCurrencySymbol(currency) {
1852
+ try {
1853
+ const parts = new Intl.NumberFormat("en", { style: "currency", currency }).formatToParts(0);
1854
+ const sym = parts.find((p) => p.type === "currency");
1855
+ return sym?.value || currency;
1856
+ } catch {
1857
+ return currency;
1858
+ }
1859
+ }
1860
+ var DEFAULT_MIN = 1;
1861
+ var DEFAULT_MAX = 1e5;
1862
+ function useTopupAmountSelector(options) {
1863
+ const { currency, minAmount = DEFAULT_MIN, maxAmount = DEFAULT_MAX } = options;
1864
+ const [selectedAmount, setSelectedAmount] = (0, import_react15.useState)(null);
1865
+ const [customAmount, setCustomAmountRaw] = (0, import_react15.useState)("");
1866
+ const [error, setError] = (0, import_react15.useState)(null);
1867
+ const quickAmounts = (0, import_react15.useMemo)(() => getQuickAmounts(currency), [currency]);
1868
+ const currencySymbol = (0, import_react15.useMemo)(() => getCurrencySymbol(currency), [currency]);
1869
+ const resolvedAmount = (0, import_react15.useMemo)(() => {
1870
+ if (customAmount) {
1871
+ const parsed = parseFloat(customAmount);
1872
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
1873
+ }
1874
+ return selectedAmount;
1875
+ }, [selectedAmount, customAmount]);
1876
+ const selectQuickAmount = (0, import_react15.useCallback)(
1877
+ (amount) => {
1878
+ setSelectedAmount(amount);
1879
+ setCustomAmountRaw("");
1880
+ setError(null);
1881
+ },
1882
+ []
1883
+ );
1884
+ const setCustomAmount = (0, import_react15.useCallback)(
1885
+ (value) => {
1886
+ const sanitized = value.replace(/[^0-9.]/g, "");
1887
+ const dotIndex = sanitized.indexOf(".");
1888
+ const cleaned = dotIndex === -1 ? sanitized : sanitized.slice(0, dotIndex + 1) + sanitized.slice(dotIndex + 1).replace(/\./g, "");
1889
+ setCustomAmountRaw(cleaned);
1890
+ setSelectedAmount(null);
1891
+ setError(null);
1892
+ },
1893
+ []
1894
+ );
1895
+ const validate = (0, import_react15.useCallback)(() => {
1896
+ if (resolvedAmount == null) {
1897
+ setError("Please select or enter an amount");
1898
+ return false;
1899
+ }
1900
+ if (resolvedAmount < minAmount) {
1901
+ setError(`Minimum amount is ${currencySymbol}${minAmount}`);
1902
+ return false;
1903
+ }
1904
+ if (resolvedAmount > maxAmount) {
1905
+ setError(`Maximum amount is ${currencySymbol}${maxAmount.toLocaleString()}`);
1906
+ return false;
1907
+ }
1908
+ setError(null);
1909
+ return true;
1910
+ }, [resolvedAmount, minAmount, maxAmount, currencySymbol]);
1911
+ const reset = (0, import_react15.useCallback)(() => {
1912
+ setSelectedAmount(null);
1913
+ setCustomAmountRaw("");
1914
+ setError(null);
1915
+ }, []);
1916
+ return {
1917
+ quickAmounts,
1918
+ selectedAmount,
1919
+ customAmount,
1920
+ resolvedAmount,
1921
+ selectQuickAmount,
1922
+ setCustomAmount,
1923
+ error,
1924
+ validate,
1925
+ reset,
1926
+ currencySymbol
1927
+ };
1928
+ }
1276
1929
  // Annotate the CommonJS export names for ESM import in node:
1277
1930
  0 && (module.exports = {
1931
+ BalanceBadge,
1278
1932
  PaymentForm,
1279
1933
  PlanBadge,
1280
1934
  PlanSelector,
@@ -1284,6 +1938,7 @@ function usePurchaseStatus() {
1284
1938
  SolvaPayProvider,
1285
1939
  Spinner,
1286
1940
  StripePaymentFormWrapper,
1941
+ TopupForm,
1287
1942
  defaultAuthAdapter,
1288
1943
  filterPurchases,
1289
1944
  getActivePurchases,
@@ -1291,10 +1946,15 @@ function usePurchaseStatus() {
1291
1946
  getMostRecentPurchase,
1292
1947
  getPrimaryPurchase,
1293
1948
  isPaidPurchase,
1949
+ useActivation,
1950
+ useBalance,
1294
1951
  useCheckout,
1295
1952
  useCustomer,
1296
1953
  usePlans,
1297
1954
  usePurchase,
1955
+ usePurchaseActions,
1298
1956
  usePurchaseStatus,
1299
- useSolvaPay
1957
+ useSolvaPay,
1958
+ useTopup,
1959
+ useTopupAmountSelector
1300
1960
  });