@solvapay/react 1.0.0-preview.8 → 1.0.1-preview.1

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,215 +1,430 @@
1
+ import {
2
+ defaultAuthAdapter
3
+ } from "./chunk-OUSEQRCT.js";
4
+
1
5
  // src/SolvaPayProvider.tsx
2
6
  import { createContext, useState, useEffect, useCallback, useMemo, useRef } from "react";
3
7
 
4
- // src/utils/subscriptions.ts
5
- function filterSubscriptions(subscriptions) {
6
- const now = /* @__PURE__ */ new Date();
7
- const filtered = subscriptions.filter((sub) => {
8
- const subAny = sub;
9
- const isCancelled = sub.status === "cancelled" || subAny.cancelledAt;
10
- if (!isCancelled) {
11
- return true;
12
- }
13
- if (isCancelled && subAny.endDate) {
14
- const endDate = new Date(subAny.endDate);
15
- const isFuture = endDate > now;
16
- return isFuture;
17
- }
18
- return false;
19
- });
20
- return filtered;
8
+ // src/utils/purchases.ts
9
+ function filterPurchases(purchases) {
10
+ return purchases.filter((purchase) => purchase.status === "active");
21
11
  }
22
- function getActiveSubscriptions(subscriptions) {
23
- return subscriptions.filter((sub) => {
24
- const subAny = sub;
25
- return sub.status === "active" && !subAny.cancelledAt;
26
- });
12
+ function getActivePurchases(purchases) {
13
+ return purchases.filter((purchase) => purchase.status === "active");
27
14
  }
28
- function getCancelledSubscriptionsWithEndDate(subscriptions) {
29
- return subscriptions.filter((sub) => {
30
- const subAny = sub;
31
- const isCancelled = sub.status === "cancelled" || subAny.cancelledAt;
32
- return isCancelled && subAny.endDate && new Date(subAny.endDate) > /* @__PURE__ */ new Date();
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;
33
19
  });
34
20
  }
35
- function getMostRecentSubscription(subscriptions) {
36
- if (subscriptions.length === 0) return null;
37
- return subscriptions.reduce((latest, current) => {
21
+ function getMostRecentPurchase(purchases) {
22
+ if (purchases.length === 0) return null;
23
+ return purchases.reduce((latest, current) => {
38
24
  return new Date(current.startDate) > new Date(latest.startDate) ? current : latest;
39
25
  });
40
26
  }
41
- function getPrimarySubscription(subscriptions) {
42
- const filtered = filterSubscriptions(subscriptions);
43
- const activeSubs = getActiveSubscriptions(filtered);
44
- if (activeSubs.length > 0) {
45
- return getMostRecentSubscription(activeSubs);
46
- }
47
- const cancelledWithEndDate = getCancelledSubscriptionsWithEndDate(filtered);
48
- if (cancelledWithEndDate.length > 0) {
49
- return getMostRecentSubscription(cancelledWithEndDate);
27
+ function getPrimaryPurchase(purchases) {
28
+ const filtered = filterPurchases(purchases);
29
+ if (filtered.length > 0) {
30
+ return getMostRecentPurchase(filtered);
50
31
  }
51
32
  return null;
52
33
  }
53
- function hasActivePaidSubscription(subscriptions, isPaidPlan) {
54
- const filtered = filterSubscriptions(subscriptions);
55
- const activePaid = getActiveSubscriptions(filtered).some((sub) => isPaidPlan(sub.planName));
56
- if (activePaid) return true;
57
- const cancelledPaid = getCancelledSubscriptionsWithEndDate(filtered).some((sub) => isPaidPlan(sub.planName));
58
- return cancelledPaid;
34
+ function isPaidPurchase(purchase) {
35
+ return (purchase.amount ?? 0) > 0;
59
36
  }
60
37
 
61
38
  // src/SolvaPayProvider.tsx
62
39
  import { jsx } from "react/jsx-runtime";
63
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
+ }
64
96
  var SolvaPayProvider = ({
65
- createPayment,
66
- checkSubscription,
67
- processPayment,
68
- customerRef,
69
- onCustomerRefUpdate,
97
+ config,
98
+ createPayment: customCreatePayment,
99
+ checkPurchase: customCheckPurchase,
100
+ processPayment: customProcessPayment,
70
101
  children
71
102
  }) => {
72
- if (!createPayment || typeof createPayment !== "function") {
73
- throw new Error("SolvaPayProvider: createPayment prop is required and must be a function");
74
- }
75
- if (!checkSubscription || typeof checkSubscription !== "function") {
76
- throw new Error("SolvaPayProvider: checkSubscription prop is required and must be a function");
77
- }
78
- const [subscriptionData, setSubscriptionData] = useState({
79
- subscriptions: []
103
+ const [purchaseData, setPurchaseData] = useState({
104
+ purchases: []
80
105
  });
81
106
  const [loading, setLoading] = useState(false);
82
- const [internalCustomerRef, setInternalCustomerRef] = useState(customerRef);
107
+ const [internalCustomerRef, setInternalCustomerRef] = useState(void 0);
108
+ const [userId, setUserId] = useState(null);
109
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
83
110
  const inFlightRef = useRef(null);
84
111
  const lastFetchedRef = useRef(null);
85
- const checkSubscriptionRef = useRef(checkSubscription);
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
+ );
86
119
  useEffect(() => {
87
- checkSubscriptionRef.current = checkSubscription;
88
- }, [checkSubscription]);
89
- const fetchSubscription = useCallback(async (force = false) => {
90
- const currentCustomerRef = internalCustomerRef;
91
- if (!currentCustomerRef) {
92
- setSubscriptionData({ subscriptions: [] });
93
- setLoading(false);
94
- inFlightRef.current = null;
95
- lastFetchedRef.current = null;
96
- return;
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}`;
97
144
  }
98
- if (!force && lastFetchedRef.current === currentCustomerRef && inFlightRef.current !== currentCustomerRef) {
99
- return;
145
+ if (cachedRef) {
146
+ headers["x-solvapay-customer-ref"] = cachedRef;
100
147
  }
101
- if (inFlightRef.current === currentCustomerRef) {
102
- return;
148
+ if (currentConfig?.headers) {
149
+ const customHeaders = typeof currentConfig.headers === "function" ? await currentConfig.headers() : currentConfig.headers;
150
+ Object.assign(headers, customHeaders);
103
151
  }
104
- inFlightRef.current = currentCustomerRef;
105
- setLoading(true);
106
- try {
107
- const data = await checkSubscriptionRef.current(currentCustomerRef);
108
- if (inFlightRef.current === currentCustomerRef) {
109
- const filteredData = {
110
- ...data,
111
- subscriptions: filterSubscriptions(data.subscriptions || [])
112
- };
113
- setSubscriptionData(filteredData);
114
- lastFetchedRef.current = currentCustomerRef;
115
- }
116
- } catch (error) {
117
- console.error("[SolvaPayProvider] Failed to fetch subscription:", error);
118
- if (inFlightRef.current === currentCustomerRef) {
119
- setSubscriptionData({
120
- customerRef: currentCustomerRef,
121
- subscriptions: []
122
- });
123
- lastFetchedRef.current = currentCustomerRef;
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}`;
124
180
  }
125
- } finally {
126
- if (inFlightRef.current === currentCustomerRef) {
127
- setLoading(false);
128
- inFlightRef.current = null;
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;
129
191
  }
192
+ const res = await fetchFn(route, {
193
+ method: "POST",
194
+ headers,
195
+ body: JSON.stringify(body)
196
+ });
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)
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();
130
245
  }
131
- }, [internalCustomerRef]);
132
- const refetchSubscription = useCallback(async () => {
133
- const currentCustomerRef = internalCustomerRef;
134
- lastFetchedRef.current = null;
135
- inFlightRef.current = null;
136
- await fetchSubscription(true);
137
- }, [fetchSubscription, internalCustomerRef]);
138
- useEffect(() => {
139
- if (customerRef !== internalCustomerRef) {
140
- setInternalCustomerRef(customerRef);
246
+ if (buildDefaultCheckPurchaseRef.current) {
247
+ return buildDefaultCheckPurchaseRef.current();
141
248
  }
142
- }, [customerRef, internalCustomerRef]);
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
+ );
143
269
  useEffect(() => {
144
- if (!customerRef) {
145
- setSubscriptionData({ subscriptions: [] });
146
- setLoading(false);
147
- return;
148
- }
149
- if (customerRef === lastFetchedRef.current || customerRef === inFlightRef.current) {
150
- return;
151
- }
152
- const doFetch = async () => {
153
- inFlightRef.current = customerRef;
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;
154
314
  setLoading(true);
155
315
  try {
156
- const data = await checkSubscriptionRef.current(customerRef);
157
- if (inFlightRef.current === customerRef) {
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) {
158
328
  const filteredData = {
159
329
  ...data,
160
- subscriptions: filterSubscriptions(data.subscriptions || [])
330
+ purchases: filterPurchases(data.purchases || [])
161
331
  };
162
- setSubscriptionData(filteredData);
163
- lastFetchedRef.current = customerRef;
332
+ setPurchaseData(filteredData);
333
+ lastFetchedRef.current = cacheKey;
164
334
  }
165
335
  } catch (error) {
166
- console.error("[SolvaPayProvider] Failed to fetch subscription:", error);
167
- if (inFlightRef.current === customerRef) {
168
- setSubscriptionData({
169
- customerRef,
170
- subscriptions: []
336
+ console.error("[SolvaPayProvider] Failed to fetch purchase:", error);
337
+ if (inFlightRef.current === cacheKey) {
338
+ setPurchaseData({
339
+ purchases: []
171
340
  });
172
- lastFetchedRef.current = customerRef;
341
+ lastFetchedRef.current = cacheKey;
173
342
  }
174
343
  } finally {
175
- if (inFlightRef.current === customerRef) {
344
+ if (inFlightRef.current === cacheKey) {
176
345
  setLoading(false);
177
346
  inFlightRef.current = null;
178
347
  }
179
348
  }
180
- };
181
- doFetch();
182
- }, [customerRef]);
183
- const updateCustomerRef = useCallback((newCustomerRef) => {
184
- const previousRef = internalCustomerRef;
185
- setInternalCustomerRef(newCustomerRef);
186
- if (onCustomerRefUpdate) {
187
- onCustomerRefUpdate(newCustomerRef);
188
- }
189
- }, [onCustomerRefUpdate, internalCustomerRef]);
190
- const subscription = useMemo(() => ({
191
- loading,
192
- customerRef: subscriptionData.customerRef,
193
- email: subscriptionData.email,
194
- name: subscriptionData.name,
195
- subscriptions: subscriptionData.subscriptions,
196
- hasActiveSubscription: subscriptionData.subscriptions.some(
197
- (sub) => sub.status === "active"
198
- ),
199
- hasPlan: (planName) => {
200
- return subscriptionData.subscriptions.some(
201
- (sub) => sub.planName.toLowerCase() === planName.toLowerCase() && sub.status === "active"
202
- );
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);
203
370
  }
204
- }), [loading, subscriptionData]);
205
- const contextValue = useMemo(() => ({
206
- subscription,
207
- refetchSubscription,
208
- createPayment,
209
- processPayment,
210
- customerRef: internalCustomerRef,
211
- updateCustomerRef
212
- }), [subscription, refetchSubscription, createPayment, processPayment, internalCustomerRef, updateCustomerRef]);
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
+ );
213
428
  return /* @__PURE__ */ jsx(SolvaPayContext.Provider, { value: contextValue, children });
214
429
  };
215
430
 
@@ -238,7 +453,8 @@ var stripePromiseCache = /* @__PURE__ */ new Map();
238
453
  function getStripeCacheKey(publishableKey, accountId) {
239
454
  return accountId ? `${publishableKey}:${accountId}` : publishableKey;
240
455
  }
241
- function useCheckout(planRef) {
456
+ function useCheckout(options) {
457
+ const { planRef, productRef } = options;
242
458
  const { createPayment, customerRef, updateCustomerRef } = useSolvaPay();
243
459
  const [loading, setLoading] = useState2(false);
244
460
  const [error, setError] = useState2(
@@ -255,15 +471,11 @@ function useCheckout(planRef) {
255
471
  setError(new Error("useCheckout: planRef parameter is required and must be a string"));
256
472
  return;
257
473
  }
258
- if (!customerRef) {
259
- setError(new Error("No customer reference available. Please ensure you are logged in."));
260
- return;
261
- }
262
474
  isStartingRef.current = true;
263
475
  setLoading(true);
264
476
  setError(null);
265
477
  try {
266
- const result = await createPayment({ planRef, customerRef });
478
+ const result = await createPayment({ planRef, productRef });
267
479
  if (!result || typeof result !== "object") {
268
480
  throw new Error("Invalid payment intent response from server");
269
481
  }
@@ -292,7 +504,7 @@ function useCheckout(planRef) {
292
504
  setLoading(false);
293
505
  isStartingRef.current = false;
294
506
  }
295
- }, [planRef, customerRef, createPayment, updateCustomerRef, loading]);
507
+ }, [planRef, productRef, createPayment, updateCustomerRef, loading]);
296
508
  const reset = useCallback2(() => {
297
509
  isStartingRef.current = false;
298
510
  setLoading(false);
@@ -310,52 +522,81 @@ function useCheckout(planRef) {
310
522
  };
311
523
  }
312
524
 
313
- // src/hooks/useSubscription.ts
314
- function useSubscription() {
315
- const { subscription, refetchSubscription } = useSolvaPay();
525
+ // src/hooks/usePurchase.ts
526
+ function usePurchase() {
527
+ const { purchase, refetchPurchase } = useSolvaPay();
316
528
  return {
317
- ...subscription,
318
- refetch: refetchSubscription
529
+ ...purchase,
530
+ refetch: refetchPurchase
319
531
  };
320
532
  }
321
533
 
322
534
  // src/components/Spinner.tsx
323
- import { jsx as jsx2 } from "react/jsx-runtime";
535
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
324
536
  var Spinner = ({
325
537
  className = "",
326
538
  size = "md"
327
539
  }) => {
328
540
  const sizeClasses = {
329
- sm: "w-4 h-4 border-2",
330
- md: "w-6 h-6 border-2",
331
- lg: "w-8 h-8 border-2"
541
+ sm: "h-4 w-4",
542
+ md: "h-5 w-5",
543
+ lg: "h-6 w-6"
332
544
  };
333
- return /* @__PURE__ */ jsx2(
334
- "div",
545
+ return /* @__PURE__ */ jsxs(
546
+ "svg",
335
547
  {
336
- className: `inline-block border-gray-300 border-t-gray-600 rounded-full animate-spin ${sizeClasses[size]} ${className}`,
548
+ className: `animate-spin ${sizeClasses[size]} ${className}`,
549
+ xmlns: "http://www.w3.org/2000/svg",
550
+ fill: "none",
551
+ viewBox: "0 0 24 24",
337
552
  role: "status",
338
- "aria-busy": "true"
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
+ ]
339
565
  }
340
566
  );
341
567
  };
342
568
 
343
569
  // src/components/StripePaymentFormWrapper.tsx
344
570
  import { useState as useState3 } from "react";
345
- import { useStripe, useElements, PaymentElement } from "@stripe/react-stripe-js";
346
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
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";
347
586
  var StripePaymentFormWrapper = ({
348
587
  onSuccess,
349
588
  onError,
350
- returnUrl,
589
+ returnUrl: _returnUrl,
351
590
  submitButtonText = "Pay Now",
352
- buttonClassName
591
+ buttonClassName,
592
+ clientSecret
353
593
  }) => {
354
594
  const stripe = useStripe();
355
595
  const elements = useElements();
596
+ const customer = useCustomer();
356
597
  const [isProcessing, setIsProcessing] = useState3(false);
357
598
  const [message, setMessage] = useState3(null);
358
- const [isPaymentElementReady, setIsPaymentElementReady] = useState3(false);
599
+ const [cardComplete, setCardComplete] = useState3(false);
359
600
  const handleSubmit = async (event) => {
360
601
  event.preventDefault();
361
602
  if (!stripe || !elements) {
@@ -366,31 +607,67 @@ var StripePaymentFormWrapper = ({
366
607
  }
367
608
  return;
368
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
+ }
616
+ return;
617
+ }
369
618
  setIsProcessing(true);
370
619
  setMessage(null);
371
620
  try {
372
- const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
373
- const { error, paymentIntent } = await stripe.confirmPayment({
374
- elements,
375
- confirmParams: {
376
- return_url: finalReturnUrl
377
- },
378
- redirect: "if_required"
379
- // 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
+ }
380
639
  });
381
640
  if (error) {
382
641
  const errorMessage = error.message || "An unexpected error occurred.";
383
642
  setMessage(errorMessage);
643
+ setIsProcessing(false);
384
644
  if (onError) {
385
645
  onError(new Error(errorMessage));
386
646
  }
387
647
  } else if (paymentIntent && paymentIntent.status === "succeeded") {
388
- setMessage("Payment successful!");
648
+ setMessage(null);
389
649
  if (onSuccess) {
390
- 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!");
391
664
  }
665
+ } else if (paymentIntent && paymentIntent.status === "requires_action") {
666
+ setMessage("Payment requires additional authentication. Please complete the verification.");
667
+ setIsProcessing(false);
392
668
  } else if (paymentIntent) {
393
669
  setMessage(`Payment status: ${paymentIntent.status || "processing"}`);
670
+ setIsProcessing(false);
394
671
  }
395
672
  } catch (err) {
396
673
  const error = err instanceof Error ? err : new Error("Unknown error occurred");
@@ -404,53 +681,76 @@ var StripePaymentFormWrapper = ({
404
681
  };
405
682
  const isSuccess = message?.includes("successful") ?? false;
406
683
  const isReady = !!(stripe && elements);
407
- return /* @__PURE__ */ jsxs(
408
- "form",
409
- {
410
- onSubmit: handleSubmit,
411
- className: "space-y-6",
412
- noValidate: true,
413
- children: [
414
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
415
- (!isPaymentElementReady || !isReady) && /* @__PURE__ */ jsx3("div", { className: "absolute inset-0 z-10 p-4 bg-white border border-gray-300 rounded-lg min-h-[200px] flex items-center justify-center", children: /* @__PURE__ */ jsx3(Spinner, { size: "md" }) }),
416
- isReady && /* @__PURE__ */ jsx3("div", { className: isPaymentElementReady ? "block" : "opacity-0 pointer-events-none", children: /* @__PURE__ */ jsx3(
417
- PaymentElement,
418
- {
419
- onReady: () => setIsPaymentElementReady(true)
420
- }
421
- ) })
422
- ] }),
423
- message && /* @__PURE__ */ jsx3(
424
- "div",
425
- {
426
- className: `mt-6 p-4 rounded-lg ${isSuccess ? "bg-green-50 border border-green-400 text-green-700" : "bg-red-50 border border-red-400 text-red-700"}`,
427
- role: isSuccess ? "status" : "alert",
428
- "aria-live": isSuccess ? "polite" : "assertive",
429
- "aria-atomic": "true",
430
- children: message
431
- }
432
- ),
433
- /* @__PURE__ */ jsx3(
434
- "button",
435
- {
436
- type: "submit",
437
- disabled: !isReady || !isPaymentElementReady || isProcessing,
438
- className: buttonClassName,
439
- "aria-busy": isProcessing || !isPaymentElementReady || !isReady,
440
- "aria-disabled": !isReady || !isPaymentElementReady || isProcessing,
441
- children: isProcessing ? /* @__PURE__ */ jsx3("span", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx3(Spinner, { size: "sm" }) }) : !isReady || !isPaymentElementReady ? /* @__PURE__ */ jsx3("span", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx3(Spinner, { size: "sm" }) }) : submitButtonText
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);
442
711
  }
443
- )
444
- ]
445
- }
446
- );
712
+ }
713
+ }
714
+ ) : /* @__PURE__ */ jsx3(
715
+ "div",
716
+ {
717
+ style: {
718
+ padding: "12px",
719
+ border: "1px solid #cbd5e1",
720
+ borderRadius: "0.375rem",
721
+ backgroundColor: "white",
722
+ display: "flex",
723
+ alignItems: "center",
724
+ justifyContent: "center",
725
+ minHeight: "40px"
726
+ },
727
+ children: /* @__PURE__ */ jsx3(Spinner, { size: "sm" })
728
+ }
729
+ ),
730
+ message && !isSuccess && /* @__PURE__ */ jsx3("div", { role: "alert", "aria-live": "assertive", "aria-atomic": "true", children: message }),
731
+ /* @__PURE__ */ jsx3(
732
+ "button",
733
+ {
734
+ type: "submit",
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
744
+ }
745
+ )
746
+ ] });
447
747
  };
448
748
 
449
749
  // src/PaymentForm.tsx
450
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
750
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
451
751
  var PaymentForm = ({
452
752
  planRef,
453
- agentRef,
753
+ productRef,
454
754
  onSuccess,
455
755
  onError,
456
756
  returnUrl,
@@ -459,72 +759,121 @@ var PaymentForm = ({
459
759
  buttonClassName
460
760
  }) => {
461
761
  const validPlanRef = planRef && typeof planRef === "string" ? planRef : "";
462
- const checkout = useCheckout(validPlanRef);
463
- const { refetch } = useSubscription();
464
- const { processPayment, customerRef } = useSolvaPay();
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();
465
771
  const hasInitializedRef = useRef3(false);
466
772
  useEffect2(() => {
467
- if (!hasInitializedRef.current && validPlanRef && !checkout.loading && !checkout.error && !checkout.clientSecret) {
773
+ if (!hasInitializedRef.current && validPlanRef && !checkoutLoading && !checkoutError && !clientSecret) {
468
774
  hasInitializedRef.current = true;
469
- checkout.startCheckout().catch(() => {
775
+ startCheckout().catch(() => {
470
776
  hasInitializedRef.current = false;
471
777
  });
472
778
  }
473
- if (validPlanRef && checkout.clientSecret) {
779
+ if (validPlanRef && clientSecret) {
474
780
  hasInitializedRef.current = true;
475
781
  }
476
- }, [validPlanRef, checkout.loading, checkout.error, checkout.clientSecret, checkout.startCheckout]);
477
- const handleSuccess = useCallback3(async (paymentIntent) => {
478
- if (processPayment && customerRef && agentRef) {
479
- try {
480
- await processPayment({
481
- paymentIntentId: paymentIntent.id,
482
- agentRef,
483
- customerRef,
484
- planRef
485
- });
486
- await refetch();
487
- } catch (error) {
488
- console.error("[PaymentForm] Failed to process payment:", error);
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 {
489
831
  await refetch();
490
832
  }
491
- } else {
492
- await refetch();
493
- }
494
- if (onSuccess) {
495
- onSuccess(paymentIntent);
496
- }
497
- }, [processPayment, customerRef, agentRef, planRef, refetch, onSuccess]);
498
- const handleError = useCallback3((err) => {
499
- if (onError) {
500
- onError(err);
501
- }
502
- }, [onError]);
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
+ );
503
852
  const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
504
853
  const isValidPlanRef = planRef && typeof planRef === "string";
505
- const hasError = !!checkout.error;
506
- const hasStripeData = !!(checkout.stripePromise && checkout.clientSecret);
854
+ const hasError = !!checkoutError;
855
+ const hasStripeData = !!(stripePromise && clientSecret);
507
856
  const elementsOptions = useMemo2(() => {
508
- if (!checkout.clientSecret) return void 0;
509
- return { clientSecret: checkout.clientSecret };
510
- }, [checkout.clientSecret]);
857
+ if (!clientSecret) return void 0;
858
+ return { clientSecret };
859
+ }, [clientSecret]);
511
860
  const [hasMountedElements, setHasMountedElements] = useState4(false);
512
861
  useEffect2(() => {
513
862
  if (hasStripeData) {
514
863
  setHasMountedElements(true);
515
864
  }
516
865
  }, [hasStripeData]);
517
- const shouldRenderElements = hasStripeData || hasMountedElements && checkout.stripePromise && checkout.clientSecret;
518
- return /* @__PURE__ */ jsx4("div", { className, children: !isValidPlanRef ? /* @__PURE__ */ jsx4("div", { className: "p-4 bg-red-50 border border-red-400 rounded-lg text-red-700", children: "PaymentForm: planRef is required and must be a string" }) : hasError ? /* @__PURE__ */ jsxs2("div", { className: "p-4 bg-red-50 border border-red-400 rounded-lg text-red-700", children: [
519
- /* @__PURE__ */ jsx4("div", { className: "font-medium mb-1", children: "Payment initialization failed" }),
520
- /* @__PURE__ */ jsx4("div", { className: "text-sm", children: checkout.error?.message || "Unknown error" })
521
- ] }) : shouldRenderElements && checkout.stripePromise && elementsOptions ? (
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 ? (
522
871
  // Once we have Stripe data, always render Elements to maintain hook consistency
523
872
  // This prevents hook count mismatches when transitioning between states
524
873
  /* @__PURE__ */ jsx4(
525
874
  Elements,
526
875
  {
527
- stripe: checkout.stripePromise,
876
+ stripe: stripePromise,
528
877
  options: elementsOptions,
529
878
  children: /* @__PURE__ */ jsx4(
530
879
  StripePaymentFormWrapper,
@@ -533,168 +882,212 @@ var PaymentForm = ({
533
882
  onError: handleError,
534
883
  returnUrl: finalReturnUrl,
535
884
  submitButtonText,
536
- buttonClassName
885
+ buttonClassName,
886
+ clientSecret
537
887
  }
538
888
  )
539
889
  },
540
- checkout.clientSecret
890
+ clientSecret
541
891
  )
542
892
  ) : (
543
893
  // Loading state before Stripe data is available
544
- /* @__PURE__ */ jsxs2("div", { className: "space-y-4", children: [
545
- /* @__PURE__ */ jsx4("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center min-h-[200px]", children: /* @__PURE__ */ jsx4(Spinner, { size: "md" }) }),
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
+ ),
546
907
  /* @__PURE__ */ jsx4(
547
908
  "button",
548
909
  {
910
+ type: "submit",
549
911
  disabled: true,
550
912
  className: buttonClassName,
551
- "aria-busy": "true",
552
- children: /* @__PURE__ */ jsx4("span", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx4(Spinner, { size: "sm" }) })
913
+ "aria-busy": "false",
914
+ "aria-disabled": "true",
915
+ children: submitButtonText
553
916
  }
554
917
  )
555
918
  ] })
556
919
  ) });
557
920
  };
558
921
 
559
- // src/components/PlanBadge.tsx
560
- import { useState as useState5, useEffect as useEffect3, useRef as useRef4 } from "react";
561
- import { Fragment, jsx as jsx5 } from "react/jsx-runtime";
562
- var PlanBadge = ({
922
+ // src/components/ProductBadge.tsx
923
+ import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
924
+ var ProductBadge = ({
563
925
  children,
564
926
  as: Component = "div",
565
927
  className
566
928
  }) => {
567
- const { subscriptions, loading } = useSubscription();
568
- const [displayPlan, setDisplayPlan] = useState5(null);
569
- const [hasLoadedOnce, setHasLoadedOnce] = useState5(false);
570
- const lastPlanRef = useRef4(null);
571
- const lastLoadingRef = useRef4(true);
572
- const previousSubscriptionsRef = useRef4(subscriptions);
573
- const primarySubscription = getPrimarySubscription(subscriptions);
574
- const currentPlanName = primarySubscription?.planName || null;
575
- const fallbackPlanName = subscriptions.length > 0 && !currentPlanName ? subscriptions[0]?.planName || null : null;
576
- const effectivePlanName = currentPlanName || fallbackPlanName;
577
- useEffect3(() => {
578
- if (!loading && !hasLoadedOnce) {
579
- setHasLoadedOnce(true);
580
- }
581
- lastLoadingRef.current = loading;
582
- }, [loading, hasLoadedOnce]);
583
- useEffect3(() => {
584
- const previousSubs = previousSubscriptionsRef.current;
585
- if (previousSubs !== subscriptions) {
586
- if (loading) {
587
- setHasLoadedOnce(false);
588
- }
589
- setDisplayPlan(null);
590
- lastPlanRef.current = null;
591
- previousSubscriptionsRef.current = subscriptions;
592
- }
593
- }, [subscriptions, loading]);
594
- useEffect3(() => {
595
- const currentPlan = effectivePlanName;
596
- const previousPlan = lastPlanRef.current;
597
- if (currentPlan !== previousPlan) {
598
- if (currentPlan !== null) {
599
- lastPlanRef.current = currentPlan;
600
- setDisplayPlan(currentPlan);
601
- } else {
602
- lastPlanRef.current = null;
603
- setDisplayPlan(null);
604
- }
605
- } else if (currentPlan !== null && displayPlan === null) {
606
- setDisplayPlan(currentPlan);
607
- }
608
- }, [effectivePlanName, displayPlan]);
609
- const shouldShow = effectivePlanName !== null && hasLoadedOnce;
610
- const planToDisplay = displayPlan ?? effectivePlanName;
929
+ const { purchases, loading, hasPaidPurchase, activePurchase } = usePurchase();
930
+ const planToDisplay = activePurchase?.productName || null;
931
+ const shouldShow = !loading && planToDisplay !== null;
611
932
  if (children) {
612
- return /* @__PURE__ */ jsx5(Fragment, { children: children({ subscriptions, loading, displayPlan: planToDisplay, shouldShow }) });
933
+ return /* @__PURE__ */ jsx5(Fragment2, { children: children({ purchases, loading, displayPlan: planToDisplay, shouldShow }) });
613
934
  }
614
935
  if (!shouldShow) {
615
936
  return null;
616
937
  }
617
- const computedClassName = typeof className === "function" ? className({ subscriptions }) : className;
938
+ const computedClassName = typeof className === "function" ? className({ purchases }) : className;
618
939
  return /* @__PURE__ */ jsx5(
619
940
  Component,
620
941
  {
621
942
  className: computedClassName,
622
943
  "data-loading": loading,
623
- "data-has-subscription": !!primarySubscription,
944
+ "data-has-purchase": !!activePurchase,
945
+ "data-has-paid-purchase": hasPaidPurchase,
624
946
  role: "status",
625
947
  "aria-live": "polite",
626
948
  "aria-busy": loading,
627
- "aria-label": `Current plan: ${planToDisplay}`,
949
+ "aria-label": `Current product: ${planToDisplay}`,
628
950
  children: planToDisplay
629
951
  }
630
952
  );
631
953
  };
954
+ var PlanBadge = ProductBadge;
632
955
 
633
- // src/components/SubscriptionGate.tsx
634
- import { Fragment as Fragment2, jsx as jsx6 } from "react/jsx-runtime";
635
- var SubscriptionGate = ({
636
- requirePlan,
637
- children
638
- }) => {
639
- const { subscriptions, loading, hasPlan } = useSubscription();
640
- const hasAccess = requirePlan ? hasPlan(requirePlan) : subscriptions.some((sub) => sub.status === "active");
641
- return /* @__PURE__ */ jsx6(Fragment2, { children: children({
956
+ // src/components/PurchaseGate.tsx
957
+ import { Fragment as Fragment3, jsx as jsx6 } from "react/jsx-runtime";
958
+ var PurchaseGate = ({ requirePlan, requireProduct, children }) => {
959
+ const { purchases, loading, hasProduct } = usePurchase();
960
+ const productToCheck = requireProduct || requirePlan;
961
+ const hasAccess = productToCheck ? hasProduct(productToCheck) : purchases.some((p) => p.status === "active");
962
+ return /* @__PURE__ */ jsx6(Fragment3, { children: children({
642
963
  hasAccess,
643
- subscriptions,
964
+ purchases,
644
965
  loading
645
966
  }) });
646
967
  };
647
968
 
648
- // src/components/PlanSelector.tsx
969
+ // src/components/PricingSelector.tsx
649
970
  import { useCallback as useCallback5, useMemo as useMemo4 } from "react";
650
971
 
651
972
  // src/hooks/usePlans.ts
652
- import { useState as useState6, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo3 } from "react";
973
+ import { useState as useState5, useEffect as useEffect3, useCallback as useCallback4, useMemo as useMemo3, useRef as useRef4 } from "react";
974
+ var plansCache = /* @__PURE__ */ new Map();
975
+ var CACHE_DURATION2 = 5 * 60 * 1e3;
653
976
  function usePlans(options) {
654
- const { fetcher, agentRef, filter, sortBy, autoSelectFirstPaid = false } = options;
655
- const [plans, setPlans] = useState6([]);
656
- const [loading, setLoading] = useState6(true);
657
- const [error, setError] = useState6(null);
658
- const [selectedPlanIndex, setSelectedPlanIndex] = useState6(0);
659
- const fetchPlans = useCallback4(async () => {
660
- if (!agentRef) {
661
- setError(new Error("Agent reference not configured"));
662
- setLoading(false);
663
- return;
664
- }
665
- try {
666
- setLoading(true);
667
- setError(null);
668
- const fetchedPlans = await fetcher(agentRef);
669
- let processedPlans = filter ? fetchedPlans.filter(filter) : fetchedPlans;
670
- if (sortBy) {
671
- processedPlans = [...processedPlans].sort(sortBy);
672
- }
673
- setPlans(processedPlans);
674
- if (autoSelectFirstPaid && processedPlans.length > 0) {
675
- const firstPaidIndex = processedPlans.findIndex(
676
- (plan) => plan.price && plan.price > 0
677
- );
678
- setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
977
+ const { fetcher, productRef, filter, sortBy, autoSelectFirstPaid = false } = options;
978
+ const [plans, setPlans] = useState5([]);
979
+ const [loading, setLoading] = useState5(true);
980
+ const [error, setError] = useState5(null);
981
+ const [selectedPlanIndex, setSelectedPlanIndex] = useState5(0);
982
+ const fetcherRef = useRef4(fetcher);
983
+ const filterRef = useRef4(filter);
984
+ const sortByRef = useRef4(sortBy);
985
+ const autoSelectFirstPaidRef = useRef4(autoSelectFirstPaid);
986
+ useEffect3(() => {
987
+ fetcherRef.current = fetcher;
988
+ }, [fetcher]);
989
+ useEffect3(() => {
990
+ filterRef.current = filter;
991
+ }, [filter]);
992
+ useEffect3(() => {
993
+ sortByRef.current = sortBy;
994
+ }, [sortBy]);
995
+ useEffect3(() => {
996
+ autoSelectFirstPaidRef.current = autoSelectFirstPaid;
997
+ }, [autoSelectFirstPaid]);
998
+ const fetchPlans = useCallback4(
999
+ async (force = false) => {
1000
+ if (!productRef) {
1001
+ setError(new Error("Product reference not configured"));
1002
+ setLoading(false);
1003
+ return;
679
1004
  }
680
- } catch (err) {
681
- setError(err instanceof Error ? err : new Error("Failed to load plans"));
682
- } finally {
683
- setLoading(false);
684
- }
685
- }, [agentRef, fetcher, filter, sortBy, autoSelectFirstPaid]);
686
- useEffect4(() => {
1005
+ const cached = plansCache.get(productRef);
1006
+ const now = Date.now();
1007
+ if (!force && cached && now - cached.timestamp < CACHE_DURATION2) {
1008
+ const cachedPlans = cached.plans;
1009
+ let processedPlans = filterRef.current ? cachedPlans.filter(filterRef.current) : cachedPlans;
1010
+ if (sortByRef.current) {
1011
+ processedPlans = [...processedPlans].sort(sortByRef.current);
1012
+ }
1013
+ setPlans(processedPlans);
1014
+ setLoading(false);
1015
+ setError(null);
1016
+ if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1017
+ const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1018
+ setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1019
+ }
1020
+ return;
1021
+ }
1022
+ if (cached?.promise) {
1023
+ try {
1024
+ setLoading(true);
1025
+ const fetchedPlans = await cached.promise;
1026
+ let processedPlans = filterRef.current ? fetchedPlans.filter(filterRef.current) : fetchedPlans;
1027
+ if (sortByRef.current) {
1028
+ processedPlans = [...processedPlans].sort(sortByRef.current);
1029
+ }
1030
+ setPlans(processedPlans);
1031
+ setError(null);
1032
+ if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1033
+ const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1034
+ setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1035
+ }
1036
+ } catch (err) {
1037
+ setError(err instanceof Error ? err : new Error("Failed to load plans"));
1038
+ } finally {
1039
+ setLoading(false);
1040
+ }
1041
+ return;
1042
+ }
1043
+ try {
1044
+ setLoading(true);
1045
+ setError(null);
1046
+ const fetchPromise = fetcherRef.current(productRef);
1047
+ plansCache.set(productRef, {
1048
+ plans: [],
1049
+ timestamp: now,
1050
+ promise: fetchPromise
1051
+ });
1052
+ const fetchedPlans = await fetchPromise;
1053
+ plansCache.set(productRef, {
1054
+ plans: fetchedPlans,
1055
+ timestamp: now,
1056
+ promise: null
1057
+ });
1058
+ let processedPlans = filterRef.current ? fetchedPlans.filter(filterRef.current) : fetchedPlans;
1059
+ if (sortByRef.current) {
1060
+ processedPlans = [...processedPlans].sort(sortByRef.current);
1061
+ }
1062
+ setPlans(processedPlans);
1063
+ if (autoSelectFirstPaidRef.current && processedPlans.length > 0) {
1064
+ const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
1065
+ setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
1066
+ }
1067
+ } catch (err) {
1068
+ plansCache.delete(productRef);
1069
+ setError(err instanceof Error ? err : new Error("Failed to load plans"));
1070
+ } finally {
1071
+ setLoading(false);
1072
+ }
1073
+ },
1074
+ [productRef]
1075
+ );
1076
+ useEffect3(() => {
687
1077
  fetchPlans();
688
1078
  }, [fetchPlans]);
689
1079
  const selectedPlan = useMemo3(() => {
690
1080
  return plans[selectedPlanIndex] || null;
691
1081
  }, [plans, selectedPlanIndex]);
692
- const selectPlan = useCallback4((planRef) => {
693
- const index = plans.findIndex((p) => p.reference === planRef);
694
- if (index >= 0) {
695
- setSelectedPlanIndex(index);
696
- }
697
- }, [plans]);
1082
+ const selectPlan = useCallback4(
1083
+ (planRef) => {
1084
+ const index = plans.findIndex((p) => p.reference === planRef);
1085
+ if (index >= 0) {
1086
+ setSelectedPlanIndex(index);
1087
+ }
1088
+ },
1089
+ [plans]
1090
+ );
698
1091
  return {
699
1092
  plans,
700
1093
  loading,
@@ -703,77 +1096,78 @@ function usePlans(options) {
703
1096
  selectedPlan,
704
1097
  setSelectedPlanIndex,
705
1098
  selectPlan,
706
- refetch: fetchPlans
1099
+ refetch: () => fetchPlans(true)
1100
+ // Force refetch
707
1101
  };
708
1102
  }
709
1103
 
710
- // src/components/PlanSelector.tsx
711
- import { Fragment as Fragment3, jsx as jsx7 } from "react/jsx-runtime";
712
- var PlanSelector = ({
713
- agentRef,
1104
+ // src/components/PricingSelector.tsx
1105
+ import { Fragment as Fragment4, jsx as jsx7 } from "react/jsx-runtime";
1106
+ var PricingSelector = ({
1107
+ productRef,
714
1108
  fetcher,
715
1109
  filter,
716
1110
  sortBy,
717
1111
  autoSelectFirstPaid,
718
1112
  children
719
1113
  }) => {
720
- const { subscriptions } = useSubscription();
1114
+ const { purchases } = usePurchase();
721
1115
  const plansHook = usePlans({
722
- agentRef,
1116
+ productRef,
723
1117
  fetcher,
724
1118
  filter,
725
1119
  sortBy,
726
1120
  autoSelectFirstPaid
727
1121
  });
728
1122
  const { plans } = plansHook;
729
- const isPaidPlan = useCallback5((planName) => {
730
- const plan = plans.find((p) => p.name === planName);
731
- return plan ? (plan.price ?? 0) > 0 && !plan.isFreeTier : true;
732
- }, [plans]);
733
- const activeSubscription = useMemo4(() => {
734
- const activeSubs = subscriptions.filter((sub) => sub.status === "active");
735
- return activeSubs.sort(
1123
+ const isPaidPlan = useCallback5(
1124
+ (planRef) => {
1125
+ const plan = plans.find((p) => p.reference === planRef);
1126
+ return plan ? (plan.price ?? 0) > 0 && !plan.isFreeTier : true;
1127
+ },
1128
+ [plans]
1129
+ );
1130
+ const activePurchase = useMemo4(() => {
1131
+ const activePurchases = purchases.filter((p) => p.status === "active");
1132
+ return activePurchases.sort(
736
1133
  (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
737
1134
  )[0] || null;
738
- }, [subscriptions]);
739
- const isCurrentPlan = useCallback5((planName) => {
740
- return activeSubscription?.planName === planName;
741
- }, [activeSubscription]);
742
- return /* @__PURE__ */ jsx7(Fragment3, { children: children({
1135
+ }, [purchases]);
1136
+ const isCurrentPlan = useCallback5(
1137
+ (planRef) => {
1138
+ return activePurchase?.planSnapshot?.reference === planRef;
1139
+ },
1140
+ [activePurchase]
1141
+ );
1142
+ return /* @__PURE__ */ jsx7(Fragment4, { children: children({
743
1143
  ...plansHook,
744
- subscriptions,
1144
+ purchases,
745
1145
  isPaidPlan,
746
1146
  isCurrentPlan
747
1147
  }) });
748
1148
  };
1149
+ var PlanSelector = PricingSelector;
749
1150
 
750
- // src/hooks/useSubscriptionHelpers.ts
1151
+ // src/hooks/usePurchaseStatus.ts
751
1152
  import { useMemo as useMemo5, useCallback as useCallback6 } from "react";
752
- function useSubscriptionHelpers(plans) {
753
- const { subscriptions } = useSubscription();
754
- const isPaidPlan = useCallback6((planName) => {
755
- const plan = plans.find((p) => p.name === planName);
756
- return plan ? (plan.price ?? 0) > 0 && !plan.isFreeTier : true;
757
- }, [plans]);
758
- const subscriptionData = useMemo5(() => {
759
- const activePaidSubscriptions = subscriptions.filter(
760
- (sub) => sub.status === "active" && isPaidPlan(sub.planName)
761
- );
762
- const activePaidSubscription = activePaidSubscriptions.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())[0] || null;
763
- const cancelledPaidSubscriptions = subscriptions.filter(
764
- (sub) => sub.status === "cancelled" && isPaidPlan(sub.planName)
765
- );
766
- const cancelledSubscription = cancelledPaidSubscriptions.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())[0] || null;
767
- const hasActiveSubscription = subscriptions.some((sub) => sub.status === "active");
768
- const shouldShowCancelledNotice = !!(cancelledSubscription && (!hasActiveSubscription || cancelledSubscription.endDate));
1153
+ function usePurchaseStatus() {
1154
+ const { purchases } = usePurchase();
1155
+ const isPaidPurchase2 = useCallback6((p) => {
1156
+ return (p.amount ?? 0) > 0;
1157
+ }, []);
1158
+ const purchaseData = useMemo5(() => {
1159
+ const cancelledPaidPurchases = purchases.filter((p) => {
1160
+ return p.status === "active" && p.cancelledAt && isPaidPurchase2(p);
1161
+ });
1162
+ const cancelledPurchase = cancelledPaidPurchases.sort(
1163
+ (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
1164
+ )[0] || null;
1165
+ const shouldShowCancelledNotice = !!cancelledPurchase;
769
1166
  return {
770
- activePaidSubscription,
771
- activePlanName: activePaidSubscription?.planName || null,
772
- cancelledSubscription,
773
- hasPaidSubscription: activePaidSubscriptions.length > 0,
1167
+ cancelledPurchase,
774
1168
  shouldShowCancelledNotice
775
1169
  };
776
- }, [subscriptions, isPaidPlan]);
1170
+ }, [purchases, isPaidPurchase2]);
777
1171
  const formatDate = useCallback6((dateString) => {
778
1172
  if (!dateString) return null;
779
1173
  return new Date(dateString).toLocaleDateString("en-US", {
@@ -791,12 +1185,8 @@ function useSubscriptionHelpers(plans) {
791
1185
  return diffDays > 0 ? diffDays : 0;
792
1186
  }, []);
793
1187
  return {
794
- isPaidPlan,
795
- activePaidSubscription: subscriptionData.activePaidSubscription,
796
- cancelledSubscription: subscriptionData.cancelledSubscription,
797
- hasPaidSubscription: subscriptionData.hasPaidSubscription,
798
- shouldShowCancelledNotice: subscriptionData.shouldShowCancelledNotice,
799
- activePlanName: subscriptionData.activePlanName,
1188
+ cancelledPurchase: purchaseData.cancelledPurchase,
1189
+ shouldShowCancelledNotice: purchaseData.shouldShowCancelledNotice,
800
1190
  formatDate,
801
1191
  getDaysUntilExpiration
802
1192
  };
@@ -805,19 +1195,23 @@ export {
805
1195
  PaymentForm,
806
1196
  PlanBadge,
807
1197
  PlanSelector,
1198
+ PricingSelector,
1199
+ ProductBadge,
1200
+ PurchaseGate,
808
1201
  SolvaPayProvider,
809
1202
  Spinner,
810
1203
  StripePaymentFormWrapper,
811
- SubscriptionGate,
812
- filterSubscriptions,
813
- getActiveSubscriptions,
814
- getCancelledSubscriptionsWithEndDate,
815
- getMostRecentSubscription,
816
- getPrimarySubscription,
817
- hasActivePaidSubscription,
1204
+ defaultAuthAdapter,
1205
+ filterPurchases,
1206
+ getActivePurchases,
1207
+ getCancelledPurchasesWithEndDate,
1208
+ getMostRecentPurchase,
1209
+ getPrimaryPurchase,
1210
+ isPaidPurchase,
818
1211
  useCheckout,
1212
+ useCustomer,
819
1213
  usePlans,
820
- useSolvaPay,
821
- useSubscription,
822
- useSubscriptionHelpers
1214
+ usePurchase,
1215
+ usePurchaseStatus,
1216
+ useSolvaPay
823
1217
  };