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