@solvapay/react 1.0.0-preview.2 → 1.0.0-preview.21

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
@@ -1,194 +1,673 @@
1
+ import {
2
+ defaultAuthAdapter
3
+ } from "./chunk-OUSEQRCT.js";
4
+
1
5
  // src/SolvaPayProvider.tsx
2
- import { useState, useEffect } from "react";
3
- import { Elements } from "@stripe/react-stripe-js";
4
- import { loadStripe } from "@stripe/stripe-js";
5
- import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { createContext, useState, useEffect, useCallback, useMemo, useRef } from "react";
7
+
8
+ // src/utils/purchases.ts
9
+ function filterPurchases(purchases) {
10
+ return purchases.filter((purchase) => purchase.status === "active");
11
+ }
12
+ function getActivePurchases(purchases) {
13
+ return purchases.filter((purchase) => purchase.status === "active");
14
+ }
15
+ function getCancelledPurchasesWithEndDate(purchases) {
16
+ const now = /* @__PURE__ */ new Date();
17
+ return purchases.filter((purchase) => {
18
+ return purchase.status === "active" && purchase.cancelledAt && purchase.endDate && new Date(purchase.endDate) > now;
19
+ });
20
+ }
21
+ function getMostRecentPurchase(purchases) {
22
+ if (purchases.length === 0) return null;
23
+ return purchases.reduce((latest, current) => {
24
+ return new Date(current.startDate) > new Date(latest.startDate) ? current : latest;
25
+ });
26
+ }
27
+ function getPrimaryPurchase(purchases) {
28
+ const filtered = filterPurchases(purchases);
29
+ if (filtered.length > 0) {
30
+ return getMostRecentPurchase(filtered);
31
+ }
32
+ return null;
33
+ }
34
+ function isPaidPurchase(purchase) {
35
+ return (purchase.amount ?? 0) > 0;
36
+ }
37
+
38
+ // src/SolvaPayProvider.tsx
39
+ import { jsx } from "react/jsx-runtime";
40
+ var SolvaPayContext = createContext(null);
41
+ var CUSTOMER_REF_KEY = "solvapay_customerRef";
42
+ var CUSTOMER_REF_EXPIRY = "solvapay_customerRef_expiry";
43
+ var CUSTOMER_REF_USER_ID_KEY = "solvapay_customerRef_userId";
44
+ var CACHE_DURATION = 24 * 60 * 60 * 1e3;
45
+ function getCachedCustomerRef(userId) {
46
+ if (typeof window === "undefined") return null;
47
+ const cached = localStorage.getItem(CUSTOMER_REF_KEY);
48
+ const expiry = localStorage.getItem(CUSTOMER_REF_EXPIRY);
49
+ const cachedUserId = localStorage.getItem(CUSTOMER_REF_USER_ID_KEY);
50
+ if (!cached || !expiry) return null;
51
+ if (Date.now() > parseInt(expiry)) {
52
+ localStorage.removeItem(CUSTOMER_REF_KEY);
53
+ localStorage.removeItem(CUSTOMER_REF_EXPIRY);
54
+ localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
55
+ return null;
56
+ }
57
+ if (userId !== void 0 && userId !== null) {
58
+ if (cachedUserId !== userId) {
59
+ clearCachedCustomerRef();
60
+ return null;
61
+ }
62
+ }
63
+ return cached;
64
+ }
65
+ function setCachedCustomerRef(customerRef, userId) {
66
+ if (typeof window === "undefined") return;
67
+ if (userId === void 0 || userId === null) {
68
+ return;
69
+ }
70
+ localStorage.setItem(CUSTOMER_REF_KEY, customerRef);
71
+ localStorage.setItem(CUSTOMER_REF_EXPIRY, String(Date.now() + CACHE_DURATION));
72
+ localStorage.setItem(CUSTOMER_REF_USER_ID_KEY, userId);
73
+ }
74
+ function clearCachedCustomerRef() {
75
+ if (typeof window === "undefined") return;
76
+ localStorage.removeItem(CUSTOMER_REF_KEY);
77
+ localStorage.removeItem(CUSTOMER_REF_EXPIRY);
78
+ localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
79
+ }
80
+ function getAuthAdapter(config) {
81
+ if (config?.auth?.adapter) {
82
+ return config.auth.adapter;
83
+ }
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
+ };
93
+ }
94
+ return defaultAuthAdapter;
95
+ }
6
96
  var SolvaPayProvider = ({
7
- children,
8
- amount = 999,
9
- // Default $9.99
10
- currency = "USD",
11
- planRef,
12
- agentRef,
13
- apiBaseUrl = "/api/solvapay",
14
- onPaymentReady
97
+ config,
98
+ createPayment: customCreatePayment,
99
+ checkPurchase: customCheckPurchase,
100
+ processPayment: customProcessPayment,
101
+ children
15
102
  }) => {
16
- const [stripePromise, setStripePromise] = useState(null);
17
- const [clientSecret, setClientSecret] = useState("");
103
+ const [purchaseData, setPurchaseData] = useState({
104
+ purchases: []
105
+ });
18
106
  const [loading, setLoading] = useState(false);
19
- const [error, setError] = useState(null);
20
- const createPayment = async (paymentAmount) => {
21
- setLoading(true);
22
- setError(null);
23
- try {
24
- console.log("Creating SolvaPay payment intent for amount:", paymentAmount);
25
- const response = await fetch(`${apiBaseUrl}/create-payment-intent`, {
107
+ const [internalCustomerRef, setInternalCustomerRef] = useState(void 0);
108
+ const [userId, setUserId] = useState(null);
109
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
110
+ const inFlightRef = useRef(null);
111
+ const lastFetchedRef = useRef(null);
112
+ const checkPurchaseRef = useRef(null);
113
+ const createPaymentRef = useRef(null);
114
+ const processPaymentRef = useRef(null);
115
+ const configRef = useRef(config);
116
+ const buildDefaultCheckPurchaseRef = useRef(
117
+ null
118
+ );
119
+ useEffect(() => {
120
+ configRef.current = config;
121
+ }, [config]);
122
+ useEffect(() => {
123
+ checkPurchaseRef.current = customCheckPurchase || null;
124
+ }, [customCheckPurchase]);
125
+ useEffect(() => {
126
+ createPaymentRef.current = customCreatePayment || null;
127
+ }, [customCreatePayment]);
128
+ useEffect(() => {
129
+ processPaymentRef.current = customProcessPayment || null;
130
+ }, [customProcessPayment]);
131
+ const buildDefaultCheckPurchase = useCallback(async () => {
132
+ const currentConfig = configRef.current;
133
+ const adapter = getAuthAdapter(currentConfig);
134
+ const token = await adapter.getToken();
135
+ const detectedUserId = await adapter.getUserId();
136
+ const route = currentConfig?.api?.checkPurchase || "/api/check-purchase";
137
+ 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
+ });
156
+ if (!res.ok) {
157
+ const error = new Error(`Failed to check purchase: ${res.statusText}`);
158
+ currentConfig?.onError?.(error, "checkPurchase");
159
+ throw error;
160
+ }
161
+ return res.json();
162
+ }, []);
163
+ useEffect(() => {
164
+ buildDefaultCheckPurchaseRef.current = buildDefaultCheckPurchase;
165
+ }, [buildDefaultCheckPurchase]);
166
+ const buildDefaultCreatePayment = useCallback(
167
+ async (params) => {
168
+ const currentConfig = configRef.current;
169
+ const adapter = getAuthAdapter(currentConfig);
170
+ const token = await adapter.getToken();
171
+ const detectedUserId = await adapter.getUserId();
172
+ const route = currentConfig?.api?.createPayment || "/api/create-payment-intent";
173
+ 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;
183
+ }
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
+ if (params.productRef) {
190
+ body.productRef = params.productRef;
191
+ }
192
+ const res = await fetchFn(route, {
26
193
  method: "POST",
27
- headers: {
28
- "Content-Type": "application/json"
29
- },
30
- body: JSON.stringify({
31
- planRef,
32
- agentRef,
33
- amount: paymentAmount,
34
- currency,
35
- description: "SolvaPay - Plan Purchase"
36
- })
194
+ headers,
195
+ body: JSON.stringify(body)
37
196
  });
38
- if (!response.ok) {
39
- const errorText = await response.text();
40
- throw new Error(`Payment intent creation failed: ${response.status} ${errorText}`);
41
- }
42
- const result = await response.json();
43
- if (!result || !result.clientSecret || !result.publishableKey) {
44
- throw new Error("Invalid response from server: missing required fields");
45
- }
46
- console.log("Payment intent received:", {
47
- hasClientSecret: !!result.clientSecret,
48
- hasPublishableKey: !!result.publishableKey,
49
- hasAccountId: !!result.accountId,
50
- clientSecretLength: result.clientSecret?.length || 0,
51
- publishableKeyLength: result.publishableKey?.length || 0
197
+ if (!res.ok) {
198
+ const error = new Error(`Failed to create payment: ${res.statusText}`);
199
+ currentConfig?.onError?.(error, "createPayment");
200
+ throw error;
201
+ }
202
+ return res.json();
203
+ },
204
+ []
205
+ );
206
+ const buildDefaultProcessPayment = useCallback(
207
+ async (params) => {
208
+ const currentConfig = configRef.current;
209
+ const adapter = getAuthAdapter(currentConfig);
210
+ const token = await adapter.getToken();
211
+ const detectedUserId = await adapter.getUserId();
212
+ const route = currentConfig?.api?.processPayment || "/api/process-payment";
213
+ 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
+ const res = await fetchFn(route, {
229
+ method: "POST",
230
+ headers,
231
+ body: JSON.stringify(params)
52
232
  });
233
+ if (!res.ok) {
234
+ const error = new Error(`Failed to process payment: ${res.statusText}`);
235
+ currentConfig?.onError?.(error, "processPayment");
236
+ throw error;
237
+ }
238
+ return res.json();
239
+ },
240
+ []
241
+ );
242
+ const _checkPurchase = useCallback(async () => {
243
+ if (checkPurchaseRef.current) {
244
+ return checkPurchaseRef.current();
245
+ }
246
+ if (buildDefaultCheckPurchaseRef.current) {
247
+ return buildDefaultCheckPurchaseRef.current();
248
+ }
249
+ return buildDefaultCheckPurchase();
250
+ }, [buildDefaultCheckPurchase]);
251
+ const createPayment = useCallback(
252
+ async (params) => {
253
+ if (createPaymentRef.current) {
254
+ return createPaymentRef.current(params);
255
+ }
256
+ return buildDefaultCreatePayment(params);
257
+ },
258
+ [buildDefaultCreatePayment]
259
+ );
260
+ const processPayment = useCallback(
261
+ async (params) => {
262
+ if (processPaymentRef.current) {
263
+ return processPaymentRef.current(params);
264
+ }
265
+ return buildDefaultProcessPayment(params);
266
+ },
267
+ [buildDefaultProcessPayment]
268
+ );
269
+ useEffect(() => {
270
+ const detectAuth = async () => {
271
+ const currentConfig = configRef.current;
272
+ const adapter = getAuthAdapter(currentConfig);
273
+ const token = await adapter.getToken();
274
+ const detectedUserId = await adapter.getUserId();
275
+ const prevUserId = userId;
276
+ setIsAuthenticated(!!token);
277
+ setUserId(detectedUserId);
278
+ if (prevUserId !== null && detectedUserId !== prevUserId) {
279
+ clearCachedCustomerRef();
280
+ setInternalCustomerRef(void 0);
281
+ return;
282
+ }
283
+ const cachedRef = getCachedCustomerRef(detectedUserId);
284
+ if (cachedRef && token) {
285
+ setInternalCustomerRef(cachedRef);
286
+ } else if (!token) {
287
+ clearCachedCustomerRef();
288
+ setInternalCustomerRef(void 0);
289
+ } else if (token && !cachedRef) {
290
+ setInternalCustomerRef(void 0);
291
+ }
292
+ };
293
+ detectAuth();
294
+ const interval = setInterval(detectAuth, 5e3);
295
+ return () => clearInterval(interval);
296
+ }, [userId]);
297
+ const fetchPurchase = useCallback(
298
+ async (force = false) => {
299
+ if (!isAuthenticated && !internalCustomerRef) {
300
+ setPurchaseData({ purchases: [] });
301
+ setLoading(false);
302
+ inFlightRef.current = null;
303
+ lastFetchedRef.current = null;
304
+ return;
305
+ }
306
+ const cacheKey = internalCustomerRef || userId || "anonymous";
307
+ if (!force && lastFetchedRef.current === cacheKey && inFlightRef.current !== cacheKey) {
308
+ return;
309
+ }
310
+ if (inFlightRef.current === cacheKey) {
311
+ return;
312
+ }
313
+ inFlightRef.current = cacheKey;
314
+ setLoading(true);
315
+ try {
316
+ const checkFn = checkPurchaseRef.current || buildDefaultCheckPurchaseRef.current;
317
+ if (!checkFn) {
318
+ throw new Error("checkPurchase function not available");
319
+ }
320
+ const data = await checkFn();
321
+ if (data.customerRef) {
322
+ setInternalCustomerRef(data.customerRef);
323
+ const currentAdapter = getAuthAdapter(configRef.current);
324
+ const currentUserId = await currentAdapter.getUserId();
325
+ setCachedCustomerRef(data.customerRef, currentUserId);
326
+ }
327
+ if (inFlightRef.current === cacheKey) {
328
+ const filteredData = {
329
+ ...data,
330
+ purchases: filterPurchases(data.purchases || [])
331
+ };
332
+ setPurchaseData(filteredData);
333
+ lastFetchedRef.current = cacheKey;
334
+ }
335
+ } catch (error) {
336
+ console.error("[SolvaPayProvider] Failed to fetch purchase:", error);
337
+ if (inFlightRef.current === cacheKey) {
338
+ setPurchaseData({
339
+ purchases: []
340
+ });
341
+ lastFetchedRef.current = cacheKey;
342
+ }
343
+ } finally {
344
+ if (inFlightRef.current === cacheKey) {
345
+ setLoading(false);
346
+ inFlightRef.current = null;
347
+ }
348
+ }
349
+ },
350
+ [isAuthenticated, internalCustomerRef, userId]
351
+ );
352
+ const fetchPurchaseRef = useRef(fetchPurchase);
353
+ useEffect(() => {
354
+ fetchPurchaseRef.current = fetchPurchase;
355
+ }, [fetchPurchase]);
356
+ const refetchPurchase = useCallback(async () => {
357
+ lastFetchedRef.current = null;
358
+ inFlightRef.current = null;
359
+ setPurchaseData({ purchases: [] });
360
+ await fetchPurchaseRef.current(true);
361
+ }, []);
362
+ useEffect(() => {
363
+ lastFetchedRef.current = null;
364
+ inFlightRef.current = null;
365
+ if (isAuthenticated || internalCustomerRef) {
366
+ fetchPurchase();
367
+ } else {
368
+ setPurchaseData({ purchases: [] });
369
+ setLoading(false);
370
+ }
371
+ }, [isAuthenticated, internalCustomerRef, userId]);
372
+ const updateCustomerRef = useCallback(
373
+ (newCustomerRef) => {
374
+ setInternalCustomerRef(newCustomerRef);
375
+ setCachedCustomerRef(newCustomerRef, userId);
376
+ fetchPurchase(true);
377
+ },
378
+ [fetchPurchase, userId]
379
+ );
380
+ const purchase = useMemo(() => {
381
+ const activePurchase = getPrimaryPurchase(purchaseData.purchases);
382
+ const activePaidPurchases = purchaseData.purchases.filter(
383
+ (p) => p.status === "active" && isPaidPurchase(p)
384
+ );
385
+ const activePaidPurchase = activePaidPurchases.sort(
386
+ (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
387
+ )[0] || null;
388
+ return {
389
+ loading,
390
+ customerRef: purchaseData.customerRef || internalCustomerRef,
391
+ email: purchaseData.email,
392
+ name: purchaseData.name,
393
+ purchases: purchaseData.purchases,
394
+ hasProduct: (productName) => {
395
+ return purchaseData.purchases.some(
396
+ (p) => p.productName.toLowerCase() === productName.toLowerCase() && p.status === "active"
397
+ );
398
+ },
399
+ hasPlan: (productName) => {
400
+ return purchaseData.purchases.some(
401
+ (p) => p.productName.toLowerCase() === productName.toLowerCase() && p.status === "active"
402
+ );
403
+ },
404
+ activePurchase,
405
+ hasPaidPurchase: activePaidPurchases.length > 0,
406
+ activePaidPurchase
407
+ };
408
+ }, [loading, purchaseData, internalCustomerRef]);
409
+ const contextValue = useMemo(
410
+ () => ({
411
+ purchase,
412
+ refetchPurchase,
413
+ createPayment,
414
+ processPayment,
415
+ customerRef: purchaseData.customerRef || internalCustomerRef,
416
+ updateCustomerRef
417
+ }),
418
+ [
419
+ purchase,
420
+ refetchPurchase,
421
+ createPayment,
422
+ processPayment,
423
+ purchaseData.customerRef,
424
+ internalCustomerRef,
425
+ updateCustomerRef
426
+ ]
427
+ );
428
+ return /* @__PURE__ */ jsx(SolvaPayContext.Provider, { value: contextValue, children });
429
+ };
430
+
431
+ // src/PaymentForm.tsx
432
+ import { useEffect as useEffect2, useCallback as useCallback3, useRef as useRef3, useMemo as useMemo2, useState as useState4 } from "react";
433
+ import { Elements } from "@stripe/react-stripe-js";
434
+
435
+ // src/hooks/useCheckout.ts
436
+ import { useState as useState2, useCallback as useCallback2, useRef as useRef2 } from "react";
437
+ import { loadStripe } from "@stripe/stripe-js";
438
+
439
+ // src/hooks/useSolvaPay.ts
440
+ import { useContext } from "react";
441
+ function useSolvaPay() {
442
+ const context = useContext(SolvaPayContext);
443
+ if (!context) {
444
+ throw new Error(
445
+ "useSolvaPay must be used within a SolvaPayProvider. Wrap your component tree with <SolvaPayProvider> to use this hook."
446
+ );
447
+ }
448
+ return context;
449
+ }
450
+
451
+ // src/hooks/useCheckout.ts
452
+ var stripePromiseCache = /* @__PURE__ */ new Map();
453
+ function getStripeCacheKey(publishableKey, accountId) {
454
+ return accountId ? `${publishableKey}:${accountId}` : publishableKey;
455
+ }
456
+ function useCheckout(options) {
457
+ const { planRef, productRef } = options;
458
+ const { createPayment, customerRef, updateCustomerRef } = useSolvaPay();
459
+ 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
+ );
463
+ const [stripePromise, setStripePromise] = useState2(null);
464
+ const [clientSecret, setClientSecret] = useState2(null);
465
+ const isStartingRef = useRef2(false);
466
+ const startCheckout = useCallback2(async () => {
467
+ if (isStartingRef.current || loading) {
468
+ return;
469
+ }
470
+ if (!planRef || typeof planRef !== "string") {
471
+ setError(new Error("useCheckout: planRef parameter is required and must be a string"));
472
+ return;
473
+ }
474
+ isStartingRef.current = true;
475
+ setLoading(true);
476
+ setError(null);
477
+ try {
478
+ const result = await createPayment({ planRef, productRef });
479
+ if (!result || typeof result !== "object") {
480
+ throw new Error("Invalid payment intent response from server");
481
+ }
482
+ if (!result.clientSecret || typeof result.clientSecret !== "string") {
483
+ throw new Error("Invalid client secret in payment intent response");
484
+ }
485
+ if (!result.publishableKey || typeof result.publishableKey !== "string") {
486
+ throw new Error("Invalid publishable key in payment intent response");
487
+ }
488
+ if (result.customerRef && result.customerRef !== customerRef && updateCustomerRef) {
489
+ updateCustomerRef(result.customerRef);
490
+ }
53
491
  const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
54
- const stripe = await loadStripe(result.publishableKey, stripeOptions);
492
+ const cacheKey = getStripeCacheKey(result.publishableKey, result.accountId);
493
+ let stripe = stripePromiseCache.get(cacheKey);
55
494
  if (!stripe) {
56
- throw new Error("Failed to initialize Stripe. Please check your configuration.");
495
+ stripe = loadStripe(result.publishableKey, stripeOptions);
496
+ stripePromiseCache.set(cacheKey, stripe);
57
497
  }
58
498
  setStripePromise(stripe);
59
499
  setClientSecret(result.clientSecret);
60
- if (onPaymentReady) {
61
- onPaymentReady(stripe, result.clientSecret);
62
- }
63
- } catch (error2) {
64
- console.error("Failed to create payment:", error2);
65
- let errorMessage = "Payment creation failed";
66
- if (error2 instanceof Error) {
67
- if (error2.message.includes("publishable key")) {
68
- errorMessage = "Server configuration error: Missing Stripe publishable key";
69
- } else if (error2.message.includes("client secret")) {
70
- errorMessage = "Server error: Payment intent creation failed";
71
- } else if (error2.message.includes("Failed to initialize Stripe")) {
72
- errorMessage = "Stripe initialization failed. Please check your configuration.";
73
- } else {
74
- errorMessage = error2.message;
75
- }
76
- }
77
- setError(errorMessage);
500
+ } catch (err) {
501
+ const error2 = err instanceof Error ? err : new Error("Failed to start checkout");
502
+ setError(error2);
78
503
  } finally {
79
504
  setLoading(false);
505
+ isStartingRef.current = false;
80
506
  }
507
+ }, [planRef, productRef, createPayment, updateCustomerRef, loading]);
508
+ const reset = useCallback2(() => {
509
+ isStartingRef.current = false;
510
+ setLoading(false);
511
+ setError(null);
512
+ setStripePromise(null);
513
+ setClientSecret(null);
514
+ }, []);
515
+ return {
516
+ loading,
517
+ error,
518
+ stripePromise,
519
+ clientSecret,
520
+ startCheckout,
521
+ reset
81
522
  };
82
- useEffect(() => {
83
- if (amount && !stripePromise && !loading && !error) {
84
- createPayment(amount);
85
- }
86
- }, [amount]);
87
- if (error) {
88
- return /* @__PURE__ */ jsxs("div", { style: {
89
- padding: "1rem",
90
- backgroundColor: "#fed7d7",
91
- color: "#742a2a",
92
- borderRadius: "0.375rem",
93
- marginBottom: "1rem"
94
- }, children: [
95
- /* @__PURE__ */ jsx("strong", { children: "Payment Setup Error:" }),
96
- " ",
97
- error,
98
- /* @__PURE__ */ jsx(
99
- "button",
100
- {
101
- onClick: () => createPayment(amount),
102
- style: {
103
- marginLeft: "1rem",
104
- padding: "0.5rem 1rem",
105
- backgroundColor: "#742a2a",
106
- color: "white",
107
- border: "none",
108
- borderRadius: "0.25rem",
109
- cursor: "pointer"
110
- },
111
- children: "Retry"
112
- }
113
- )
114
- ] });
115
- }
116
- if (loading) {
117
- return /* @__PURE__ */ jsx("div", { style: {
118
- padding: "1rem",
119
- textAlign: "center",
120
- color: "#4a5568"
121
- }, children: "Setting up payment..." });
122
- }
123
- if (!stripePromise || !clientSecret) {
124
- return /* @__PURE__ */ jsx("div", { style: {
125
- padding: "1rem",
126
- textAlign: "center",
127
- color: "#4a5568"
128
- }, children: /* @__PURE__ */ jsx(
129
- "button",
130
- {
131
- onClick: () => createPayment(amount),
132
- style: {
133
- padding: "0.75rem 1.5rem",
134
- backgroundColor: "#3182ce",
135
- color: "white",
136
- border: "none",
137
- borderRadius: "0.375rem",
138
- cursor: "pointer",
139
- fontSize: "1rem"
140
- },
141
- children: "Initialize Payment"
142
- }
143
- ) });
144
- }
145
- return /* @__PURE__ */ jsx(Elements, { stripe: stripePromise, options: { clientSecret }, children });
523
+ }
524
+
525
+ // src/hooks/usePurchase.ts
526
+ function usePurchase() {
527
+ const { purchase, refetchPurchase } = useSolvaPay();
528
+ return {
529
+ ...purchase,
530
+ refetch: refetchPurchase
531
+ };
532
+ }
533
+
534
+ // src/components/Spinner.tsx
535
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
536
+ var Spinner = ({
537
+ className = "",
538
+ size = "md"
539
+ }) => {
540
+ const sizeClasses = {
541
+ sm: "h-4 w-4",
542
+ md: "h-5 w-5",
543
+ lg: "h-6 w-6"
544
+ };
545
+ return /* @__PURE__ */ jsxs(
546
+ "svg",
547
+ {
548
+ className: `animate-spin ${sizeClasses[size]} ${className}`,
549
+ xmlns: "http://www.w3.org/2000/svg",
550
+ fill: "none",
551
+ viewBox: "0 0 24 24",
552
+ role: "status",
553
+ "aria-busy": "true",
554
+ children: [
555
+ /* @__PURE__ */ jsx2("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
556
+ /* @__PURE__ */ jsx2(
557
+ "path",
558
+ {
559
+ className: "opacity-75",
560
+ fill: "currentColor",
561
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
562
+ }
563
+ )
564
+ ]
565
+ }
566
+ );
146
567
  };
147
568
 
148
- // src/PaymentForm.tsx
149
- import { useState as useState2 } from "react";
150
- import { useStripe, useElements, PaymentElement } from "@stripe/react-stripe-js";
151
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
152
- var PaymentForm = ({
569
+ // src/components/StripePaymentFormWrapper.tsx
570
+ import { useState as useState3 } from "react";
571
+ import { useStripe, useElements, CardElement } from "@stripe/react-stripe-js";
572
+
573
+ // src/hooks/useCustomer.ts
574
+ function useCustomer() {
575
+ const { purchase, customerRef } = useSolvaPay();
576
+ return {
577
+ customerRef: purchase.customerRef || customerRef,
578
+ email: purchase.email,
579
+ name: purchase.name,
580
+ loading: purchase.loading
581
+ };
582
+ }
583
+
584
+ // src/components/StripePaymentFormWrapper.tsx
585
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
586
+ var StripePaymentFormWrapper = ({
153
587
  onSuccess,
154
588
  onError,
155
- returnUrl,
589
+ returnUrl: _returnUrl,
156
590
  submitButtonText = "Pay Now",
157
- className
591
+ buttonClassName,
592
+ clientSecret
158
593
  }) => {
159
594
  const stripe = useStripe();
160
595
  const elements = useElements();
161
- const [isProcessing, setIsProcessing] = useState2(false);
162
- const [message, setMessage] = useState2(null);
596
+ const customer = useCustomer();
597
+ const [isProcessing, setIsProcessing] = useState3(false);
598
+ const [message, setMessage] = useState3(null);
599
+ const [cardComplete, setCardComplete] = useState3(false);
163
600
  const handleSubmit = async (event) => {
164
601
  event.preventDefault();
165
602
  if (!stripe || !elements) {
603
+ const errorMessage = "Stripe is not available. Please refresh the page.";
604
+ setMessage(errorMessage);
605
+ if (onError) {
606
+ onError(new Error(errorMessage));
607
+ }
608
+ return;
609
+ }
610
+ if (!clientSecret) {
611
+ const errorMessage = "Payment intent not available. Please refresh the page.";
612
+ setMessage(errorMessage);
613
+ if (onError) {
614
+ onError(new Error(errorMessage));
615
+ }
166
616
  return;
167
617
  }
168
618
  setIsProcessing(true);
169
619
  setMessage(null);
170
620
  try {
171
- const { error, paymentIntent } = await stripe.confirmPayment({
172
- elements,
173
- confirmParams: {
174
- return_url: returnUrl || `${window.location.origin}/checkout/complete`
175
- },
176
- redirect: "if_required"
177
- // Only redirect if required by payment method
621
+ const cardElement = elements.getElement(CardElement);
622
+ if (!cardElement) {
623
+ const errorMessage = "Card element not found";
624
+ setMessage(errorMessage);
625
+ setIsProcessing(false);
626
+ if (onError) {
627
+ onError(new Error(errorMessage));
628
+ }
629
+ return;
630
+ }
631
+ const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
632
+ payment_method: {
633
+ card: cardElement,
634
+ billing_details: {
635
+ email: customer.email,
636
+ name: customer.name
637
+ }
638
+ }
178
639
  });
179
640
  if (error) {
180
641
  const errorMessage = error.message || "An unexpected error occurred.";
181
642
  setMessage(errorMessage);
643
+ setIsProcessing(false);
182
644
  if (onError) {
183
645
  onError(new Error(errorMessage));
184
646
  }
185
647
  } else if (paymentIntent && paymentIntent.status === "succeeded") {
186
- setMessage("Payment successful!");
648
+ setMessage(null);
187
649
  if (onSuccess) {
188
- onSuccess(paymentIntent);
650
+ try {
651
+ await onSuccess(paymentIntent);
652
+ setMessage("Payment successful!");
653
+ } catch (err) {
654
+ const error2 = err instanceof Error ? err : new Error("Payment processing failed");
655
+ setMessage("Payment processing failed. Please try again or contact support.");
656
+ setIsProcessing(false);
657
+ if (onError) {
658
+ onError(error2);
659
+ }
660
+ return;
661
+ }
662
+ } else {
663
+ setMessage("Payment successful!");
189
664
  }
190
- } else {
191
- setMessage(`Payment status: ${paymentIntent?.status || "processing"}`);
665
+ } else if (paymentIntent && paymentIntent.status === "requires_action") {
666
+ setMessage("Payment requires additional authentication. Please complete the verification.");
667
+ setIsProcessing(false);
668
+ } else if (paymentIntent) {
669
+ setMessage(`Payment status: ${paymentIntent.status || "processing"}`);
670
+ setIsProcessing(false);
192
671
  }
193
672
  } catch (err) {
194
673
  const error = err instanceof Error ? err : new Error("Unknown error occurred");
@@ -200,45 +679,579 @@ var PaymentForm = ({
200
679
  setIsProcessing(false);
201
680
  }
202
681
  };
203
- return /* @__PURE__ */ jsxs2("form", { onSubmit: handleSubmit, className, children: [
204
- /* @__PURE__ */ jsx2(PaymentElement, {}),
205
- message && /* @__PURE__ */ jsx2(
682
+ const isSuccess = message?.includes("successful") ?? false;
683
+ const isReady = !!(stripe && elements);
684
+ const cardElementOptions = {
685
+ style: {
686
+ base: {
687
+ fontSize: "16px",
688
+ color: "#424770",
689
+ "::placeholder": {
690
+ color: "#aab7c4"
691
+ }
692
+ },
693
+ invalid: {
694
+ color: "#9e2146"
695
+ }
696
+ },
697
+ hidePostalCode: true
698
+ };
699
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
700
+ isReady ? /* @__PURE__ */ jsx3(
701
+ CardElement,
702
+ {
703
+ options: cardElementOptions,
704
+ onChange: (e) => {
705
+ if (e.error) {
706
+ setMessage(e.error.message);
707
+ setCardComplete(false);
708
+ } else {
709
+ setMessage(null);
710
+ setCardComplete(e.complete);
711
+ }
712
+ }
713
+ }
714
+ ) : /* @__PURE__ */ jsx3(
206
715
  "div",
207
716
  {
208
717
  style: {
209
- marginTop: "1rem",
210
- padding: "0.75rem",
718
+ padding: "12px",
719
+ border: "1px solid #cbd5e1",
211
720
  borderRadius: "0.375rem",
212
- backgroundColor: message.includes("successful") ? "#d1fae5" : "#fee2e2",
213
- color: message.includes("successful") ? "#065f46" : "#7f1d1d"
721
+ backgroundColor: "white",
722
+ display: "flex",
723
+ alignItems: "center",
724
+ justifyContent: "center",
725
+ minHeight: "40px"
214
726
  },
215
- children: message
727
+ children: /* @__PURE__ */ jsx3(Spinner, { size: "sm" })
216
728
  }
217
729
  ),
218
- /* @__PURE__ */ jsx2(
730
+ message && !isSuccess && /* @__PURE__ */ jsx3("div", { role: "alert", "aria-live": "assertive", "aria-atomic": "true", children: message }),
731
+ /* @__PURE__ */ jsx3(
219
732
  "button",
220
733
  {
221
734
  type: "submit",
222
- disabled: !stripe || isProcessing,
223
- style: {
224
- marginTop: "1.5rem",
225
- width: "100%",
226
- padding: "0.75rem 1.5rem",
227
- backgroundColor: isProcessing || !stripe ? "#9ca3af" : "#3b82f6",
228
- color: "white",
229
- border: "none",
230
- borderRadius: "0.375rem",
231
- fontSize: "1rem",
232
- fontWeight: "500",
233
- cursor: isProcessing || !stripe ? "not-allowed" : "pointer",
234
- transition: "background-color 0.2s"
235
- },
236
- children: isProcessing ? "Processing..." : submitButtonText
735
+ disabled: !isReady || !cardComplete || isProcessing || !clientSecret,
736
+ className: buttonClassName,
737
+ "aria-busy": isProcessing,
738
+ "aria-disabled": !isReady || !cardComplete || isProcessing || !clientSecret,
739
+ onClick: handleSubmit,
740
+ children: isProcessing ? /* @__PURE__ */ jsxs2(Fragment, { children: [
741
+ /* @__PURE__ */ jsx3(Spinner, { size: "sm" }),
742
+ /* @__PURE__ */ jsx3("span", { children: "Processing..." })
743
+ ] }) : submitButtonText
237
744
  }
238
745
  )
239
746
  ] });
240
747
  };
748
+
749
+ // src/PaymentForm.tsx
750
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
751
+ var PaymentForm = ({
752
+ planRef,
753
+ productRef,
754
+ onSuccess,
755
+ onError,
756
+ returnUrl,
757
+ submitButtonText = "Pay Now",
758
+ className,
759
+ buttonClassName
760
+ }) => {
761
+ const validPlanRef = planRef && typeof planRef === "string" ? planRef : "";
762
+ const {
763
+ loading: checkoutLoading,
764
+ error: checkoutError,
765
+ clientSecret,
766
+ startCheckout,
767
+ stripePromise
768
+ } = useCheckout({ planRef: validPlanRef, productRef });
769
+ const { refetch } = usePurchase();
770
+ const { processPayment } = useSolvaPay();
771
+ const hasInitializedRef = useRef3(false);
772
+ useEffect2(() => {
773
+ if (!hasInitializedRef.current && validPlanRef && !checkoutLoading && !checkoutError && !clientSecret) {
774
+ hasInitializedRef.current = true;
775
+ startCheckout().catch(() => {
776
+ hasInitializedRef.current = false;
777
+ });
778
+ }
779
+ if (validPlanRef && clientSecret) {
780
+ hasInitializedRef.current = true;
781
+ }
782
+ }, [validPlanRef, checkoutLoading, checkoutError, clientSecret, startCheckout]);
783
+ const handleSuccess = useCallback3(
784
+ async (paymentIntent) => {
785
+ let processingTimeout = false;
786
+ let processingResult = null;
787
+ const paymentIntentAny = paymentIntent;
788
+ if (processPayment && productRef) {
789
+ try {
790
+ const result = await processPayment({
791
+ paymentIntentId: paymentIntentAny.id,
792
+ productRef,
793
+ planRef
794
+ });
795
+ processingResult = result;
796
+ const isTimeout = result?.status === "timeout";
797
+ processingTimeout = isTimeout;
798
+ if (isTimeout) {
799
+ for (let attempt = 1; attempt <= 5; attempt++) {
800
+ const delay = attempt * 1e3;
801
+ await new Promise((resolve) => setTimeout(resolve, delay));
802
+ await refetch();
803
+ }
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();
815
+ }
816
+ } catch (error) {
817
+ 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;
829
+ }
830
+ } else {
831
+ await refetch();
832
+ }
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
+ }
841
+ },
842
+ [processPayment, productRef, planRef, refetch, onSuccess]
843
+ );
844
+ const handleError = useCallback3(
845
+ (err) => {
846
+ if (onError) {
847
+ onError(err);
848
+ }
849
+ },
850
+ [onError]
851
+ );
852
+ const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
853
+ const isValidPlanRef = planRef && typeof planRef === "string";
854
+ const hasError = !!checkoutError;
855
+ const hasStripeData = !!(stripePromise && clientSecret);
856
+ const elementsOptions = useMemo2(() => {
857
+ if (!clientSecret) return void 0;
858
+ return { clientSecret };
859
+ }, [clientSecret]);
860
+ const [hasMountedElements, setHasMountedElements] = useState4(false);
861
+ useEffect2(() => {
862
+ if (hasStripeData) {
863
+ setHasMountedElements(true);
864
+ }
865
+ }, [hasStripeData]);
866
+ 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: [
868
+ /* @__PURE__ */ jsx4("div", { children: "Payment initialization failed" }),
869
+ /* @__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
873
+ /* @__PURE__ */ jsx4(
874
+ Elements,
875
+ {
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
891
+ )
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",
909
+ {
910
+ type: "submit",
911
+ disabled: true,
912
+ className: buttonClassName,
913
+ "aria-busy": "false",
914
+ "aria-disabled": "true",
915
+ children: submitButtonText
916
+ }
917
+ )
918
+ ] })
919
+ ) });
920
+ };
921
+
922
+ // src/components/ProductBadge.tsx
923
+ import { useState as useState5, useEffect as useEffect3, useRef as useRef4 } from "react";
924
+ import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
925
+ var ProductBadge = ({
926
+ children,
927
+ as: Component = "div",
928
+ className
929
+ }) => {
930
+ const { purchases, loading, hasPaidPurchase, activePurchase } = usePurchase();
931
+ const [displayPlan, setDisplayPlan] = useState5(null);
932
+ const [hasLoadedOnce, setHasLoadedOnce] = useState5(false);
933
+ const lastPlanRef = useRef4(null);
934
+ const lastLoadingRef = useRef4(true);
935
+ const previousPurchasesRef = useRef4(purchases);
936
+ const currentPlanName = activePurchase?.productName || null;
937
+ const effectivePlanName = currentPlanName;
938
+ useEffect3(() => {
939
+ if (!loading && !hasLoadedOnce) {
940
+ setHasLoadedOnce(true);
941
+ }
942
+ lastLoadingRef.current = loading;
943
+ }, [loading, hasLoadedOnce]);
944
+ useEffect3(() => {
945
+ const previousPurchases = previousPurchasesRef.current;
946
+ if (previousPurchases !== purchases) {
947
+ if (loading) {
948
+ setHasLoadedOnce(false);
949
+ }
950
+ setDisplayPlan(null);
951
+ lastPlanRef.current = null;
952
+ previousPurchasesRef.current = purchases;
953
+ }
954
+ }, [purchases, loading]);
955
+ useEffect3(() => {
956
+ const currentPlan = effectivePlanName;
957
+ const previousPlan = lastPlanRef.current;
958
+ if (currentPlan !== previousPlan) {
959
+ if (currentPlan !== null) {
960
+ lastPlanRef.current = currentPlan;
961
+ setDisplayPlan(currentPlan);
962
+ } else {
963
+ lastPlanRef.current = null;
964
+ setDisplayPlan(null);
965
+ }
966
+ } else if (currentPlan !== null && displayPlan === null) {
967
+ setDisplayPlan(currentPlan);
968
+ }
969
+ }, [effectivePlanName, displayPlan]);
970
+ const shouldShow = effectivePlanName !== null && hasLoadedOnce;
971
+ const planToDisplay = displayPlan ?? effectivePlanName;
972
+ if (children) {
973
+ return /* @__PURE__ */ jsx5(Fragment2, { children: children({ purchases, loading, displayPlan: planToDisplay, shouldShow }) });
974
+ }
975
+ if (!shouldShow) {
976
+ return null;
977
+ }
978
+ const computedClassName = typeof className === "function" ? className({ purchases }) : className;
979
+ return /* @__PURE__ */ jsx5(
980
+ Component,
981
+ {
982
+ className: computedClassName,
983
+ "data-loading": loading,
984
+ "data-has-purchase": !!activePurchase,
985
+ "data-has-paid-purchase": hasPaidPurchase,
986
+ role: "status",
987
+ "aria-live": "polite",
988
+ "aria-busy": loading,
989
+ "aria-label": `Current product: ${planToDisplay}`,
990
+ children: planToDisplay
991
+ }
992
+ );
993
+ };
994
+ var PlanBadge = ProductBadge;
995
+
996
+ // src/components/PurchaseGate.tsx
997
+ import { Fragment as Fragment3, jsx as jsx6 } from "react/jsx-runtime";
998
+ var PurchaseGate = ({ requirePlan, requireProduct, children }) => {
999
+ const { purchases, loading, hasProduct } = usePurchase();
1000
+ const productToCheck = requireProduct || requirePlan;
1001
+ const hasAccess = productToCheck ? hasProduct(productToCheck) : purchases.some((p) => p.status === "active");
1002
+ return /* @__PURE__ */ jsx6(Fragment3, { children: children({
1003
+ hasAccess,
1004
+ purchases,
1005
+ loading
1006
+ }) });
1007
+ };
1008
+
1009
+ // src/components/PricingSelector.tsx
1010
+ import { useCallback as useCallback5, useMemo as useMemo4 } from "react";
1011
+
1012
+ // src/hooks/usePlans.ts
1013
+ import { useState as useState6, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo3, useRef as useRef5 } from "react";
1014
+ var plansCache = /* @__PURE__ */ new Map();
1015
+ var CACHE_DURATION2 = 5 * 60 * 1e3;
1016
+ function usePlans(options) {
1017
+ const { fetcher, productRef, filter, sortBy, autoSelectFirstPaid = false } = options;
1018
+ const [plans, setPlans] = useState6([]);
1019
+ const [loading, setLoading] = useState6(true);
1020
+ const [error, setError] = useState6(null);
1021
+ const [selectedPlanIndex, setSelectedPlanIndex] = useState6(0);
1022
+ const fetcherRef = useRef5(fetcher);
1023
+ const filterRef = useRef5(filter);
1024
+ const sortByRef = useRef5(sortBy);
1025
+ const autoSelectFirstPaidRef = useRef5(autoSelectFirstPaid);
1026
+ useEffect4(() => {
1027
+ fetcherRef.current = fetcher;
1028
+ }, [fetcher]);
1029
+ useEffect4(() => {
1030
+ filterRef.current = filter;
1031
+ }, [filter]);
1032
+ useEffect4(() => {
1033
+ sortByRef.current = sortBy;
1034
+ }, [sortBy]);
1035
+ useEffect4(() => {
1036
+ autoSelectFirstPaidRef.current = autoSelectFirstPaid;
1037
+ }, [autoSelectFirstPaid]);
1038
+ const fetchPlans = useCallback4(
1039
+ async (force = false) => {
1040
+ if (!productRef) {
1041
+ setError(new Error("Product reference not configured"));
1042
+ setLoading(false);
1043
+ return;
1044
+ }
1045
+ const cached = plansCache.get(productRef);
1046
+ const now = Date.now();
1047
+ if (!force && cached && now - cached.timestamp < CACHE_DURATION2) {
1048
+ const cachedPlans = cached.plans;
1049
+ let processedPlans = filterRef.current ? cachedPlans.filter(filterRef.current) : cachedPlans;
1050
+ if (sortByRef.current) {
1051
+ processedPlans = [...processedPlans].sort(sortByRef.current);
1052
+ }
1053
+ setPlans(processedPlans);
1054
+ setLoading(false);
1055
+ setError(null);
1056
+ if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1057
+ const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1058
+ setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1059
+ }
1060
+ return;
1061
+ }
1062
+ if (cached?.promise) {
1063
+ try {
1064
+ setLoading(true);
1065
+ const fetchedPlans = await cached.promise;
1066
+ let processedPlans = filterRef.current ? fetchedPlans.filter(filterRef.current) : fetchedPlans;
1067
+ if (sortByRef.current) {
1068
+ processedPlans = [...processedPlans].sort(sortByRef.current);
1069
+ }
1070
+ setPlans(processedPlans);
1071
+ setError(null);
1072
+ if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1073
+ const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1074
+ setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1075
+ }
1076
+ } catch (err) {
1077
+ setError(err instanceof Error ? err : new Error("Failed to load plans"));
1078
+ } finally {
1079
+ setLoading(false);
1080
+ }
1081
+ return;
1082
+ }
1083
+ try {
1084
+ setLoading(true);
1085
+ setError(null);
1086
+ const fetchPromise = fetcherRef.current(productRef);
1087
+ plansCache.set(productRef, {
1088
+ plans: [],
1089
+ timestamp: now,
1090
+ promise: fetchPromise
1091
+ });
1092
+ const fetchedPlans = await fetchPromise;
1093
+ plansCache.set(productRef, {
1094
+ plans: fetchedPlans,
1095
+ timestamp: now,
1096
+ promise: null
1097
+ });
1098
+ let processedPlans = filterRef.current ? fetchedPlans.filter(filterRef.current) : fetchedPlans;
1099
+ if (sortByRef.current) {
1100
+ processedPlans = [...processedPlans].sort(sortByRef.current);
1101
+ }
1102
+ setPlans(processedPlans);
1103
+ if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1104
+ const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1105
+ setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1106
+ }
1107
+ } catch (err) {
1108
+ plansCache.delete(productRef);
1109
+ setError(err instanceof Error ? err : new Error("Failed to load plans"));
1110
+ } finally {
1111
+ setLoading(false);
1112
+ }
1113
+ },
1114
+ [productRef]
1115
+ );
1116
+ useEffect4(() => {
1117
+ fetchPlans();
1118
+ }, [fetchPlans]);
1119
+ const selectedPlan = useMemo3(() => {
1120
+ return plans[selectedPlanIndex] || null;
1121
+ }, [plans, selectedPlanIndex]);
1122
+ const selectPlan = useCallback4(
1123
+ (planRef) => {
1124
+ const index = plans.findIndex((p) => p.reference === planRef);
1125
+ if (index >= 0) {
1126
+ setSelectedPlanIndex(index);
1127
+ }
1128
+ },
1129
+ [plans]
1130
+ );
1131
+ return {
1132
+ plans,
1133
+ loading,
1134
+ error,
1135
+ selectedPlanIndex,
1136
+ selectedPlan,
1137
+ setSelectedPlanIndex,
1138
+ selectPlan,
1139
+ refetch: () => fetchPlans(true)
1140
+ // Force refetch
1141
+ };
1142
+ }
1143
+
1144
+ // src/components/PricingSelector.tsx
1145
+ import { Fragment as Fragment4, jsx as jsx7 } from "react/jsx-runtime";
1146
+ var PricingSelector = ({
1147
+ productRef,
1148
+ fetcher,
1149
+ filter,
1150
+ sortBy,
1151
+ autoSelectFirstPaid,
1152
+ children
1153
+ }) => {
1154
+ const { purchases } = usePurchase();
1155
+ const plansHook = usePlans({
1156
+ productRef,
1157
+ fetcher,
1158
+ filter,
1159
+ sortBy,
1160
+ autoSelectFirstPaid
1161
+ });
1162
+ const { plans } = plansHook;
1163
+ const isPaidPlan = useCallback5(
1164
+ (planRef) => {
1165
+ const plan = plans.find((p) => p.reference === planRef);
1166
+ return plan ? (plan.price ?? 0) > 0 && !plan.isFreeTier : true;
1167
+ },
1168
+ [plans]
1169
+ );
1170
+ const activePurchase = useMemo4(() => {
1171
+ const activePurchases = purchases.filter((p) => p.status === "active");
1172
+ return activePurchases.sort(
1173
+ (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
1174
+ )[0] || null;
1175
+ }, [purchases]);
1176
+ const isCurrentPlan = useCallback5(
1177
+ (planRef) => {
1178
+ return activePurchase?.planSnapshot?.reference === planRef;
1179
+ },
1180
+ [activePurchase]
1181
+ );
1182
+ return /* @__PURE__ */ jsx7(Fragment4, { children: children({
1183
+ ...plansHook,
1184
+ purchases,
1185
+ isPaidPlan,
1186
+ isCurrentPlan
1187
+ }) });
1188
+ };
1189
+ var PlanSelector = PricingSelector;
1190
+
1191
+ // src/hooks/usePurchaseStatus.ts
1192
+ import { useMemo as useMemo5, useCallback as useCallback6 } from "react";
1193
+ function usePurchaseStatus() {
1194
+ const { purchases } = usePurchase();
1195
+ const isPaidPurchase2 = useCallback6((p) => {
1196
+ return (p.amount ?? 0) > 0;
1197
+ }, []);
1198
+ const purchaseData = useMemo5(() => {
1199
+ const cancelledPaidPurchases = purchases.filter((p) => {
1200
+ return p.status === "active" && p.cancelledAt && isPaidPurchase2(p);
1201
+ });
1202
+ const cancelledPurchase = cancelledPaidPurchases.sort(
1203
+ (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
1204
+ )[0] || null;
1205
+ const shouldShowCancelledNotice = !!cancelledPurchase;
1206
+ return {
1207
+ cancelledPurchase,
1208
+ shouldShowCancelledNotice
1209
+ };
1210
+ }, [purchases, isPaidPurchase2]);
1211
+ const formatDate = useCallback6((dateString) => {
1212
+ if (!dateString) return null;
1213
+ return new Date(dateString).toLocaleDateString("en-US", {
1214
+ year: "numeric",
1215
+ month: "long",
1216
+ day: "numeric"
1217
+ });
1218
+ }, []);
1219
+ const getDaysUntilExpiration = useCallback6((endDate) => {
1220
+ if (!endDate) return null;
1221
+ const now = /* @__PURE__ */ new Date();
1222
+ const expiration = new Date(endDate);
1223
+ const diffTime = expiration.getTime() - now.getTime();
1224
+ const diffDays = Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
1225
+ return diffDays > 0 ? diffDays : 0;
1226
+ }, []);
1227
+ return {
1228
+ cancelledPurchase: purchaseData.cancelledPurchase,
1229
+ shouldShowCancelledNotice: purchaseData.shouldShowCancelledNotice,
1230
+ formatDate,
1231
+ getDaysUntilExpiration
1232
+ };
1233
+ }
241
1234
  export {
242
1235
  PaymentForm,
243
- SolvaPayProvider
1236
+ PlanBadge,
1237
+ PlanSelector,
1238
+ PricingSelector,
1239
+ ProductBadge,
1240
+ PurchaseGate,
1241
+ SolvaPayProvider,
1242
+ Spinner,
1243
+ StripePaymentFormWrapper,
1244
+ defaultAuthAdapter,
1245
+ filterPurchases,
1246
+ getActivePurchases,
1247
+ getCancelledPurchasesWithEndDate,
1248
+ getMostRecentPurchase,
1249
+ getPrimaryPurchase,
1250
+ isPaidPurchase,
1251
+ useCheckout,
1252
+ useCustomer,
1253
+ usePlans,
1254
+ usePurchase,
1255
+ usePurchaseStatus,
1256
+ useSolvaPay
244
1257
  };