@solvapay/react 1.0.0-preview.1 → 1.0.0-preview.10

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