@solvapay/react 1.0.0-preview.6 → 1.0.0-preview.8

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,10 +1,70 @@
1
1
  // src/SolvaPayProvider.tsx
2
- import { createContext, useState, useEffect, useCallback, useMemo } from "react";
2
+ import { createContext, useState, useEffect, useCallback, useMemo, useRef } from "react";
3
+
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;
21
+ }
22
+ function getActiveSubscriptions(subscriptions) {
23
+ return subscriptions.filter((sub) => {
24
+ const subAny = sub;
25
+ return sub.status === "active" && !subAny.cancelledAt;
26
+ });
27
+ }
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();
33
+ });
34
+ }
35
+ function getMostRecentSubscription(subscriptions) {
36
+ if (subscriptions.length === 0) return null;
37
+ return subscriptions.reduce((latest, current) => {
38
+ return new Date(current.startDate) > new Date(latest.startDate) ? current : latest;
39
+ });
40
+ }
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);
50
+ }
51
+ return null;
52
+ }
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;
59
+ }
60
+
61
+ // src/SolvaPayProvider.tsx
3
62
  import { jsx } from "react/jsx-runtime";
4
63
  var SolvaPayContext = createContext(null);
5
64
  var SolvaPayProvider = ({
6
65
  createPayment,
7
66
  checkSubscription,
67
+ processPayment,
8
68
  customerRef,
9
69
  onCustomerRefUpdate,
10
70
  children
@@ -20,43 +80,112 @@ var SolvaPayProvider = ({
20
80
  });
21
81
  const [loading, setLoading] = useState(false);
22
82
  const [internalCustomerRef, setInternalCustomerRef] = useState(customerRef);
83
+ const inFlightRef = useRef(null);
84
+ const lastFetchedRef = useRef(null);
85
+ const checkSubscriptionRef = useRef(checkSubscription);
23
86
  useEffect(() => {
24
- if (customerRef && customerRef !== internalCustomerRef) {
25
- setInternalCustomerRef(customerRef);
26
- }
27
- }, [customerRef, internalCustomerRef]);
28
- const fetchSubscription = useCallback(async () => {
87
+ checkSubscriptionRef.current = checkSubscription;
88
+ }, [checkSubscription]);
89
+ const fetchSubscription = useCallback(async (force = false) => {
29
90
  const currentCustomerRef = internalCustomerRef;
30
91
  if (!currentCustomerRef) {
31
92
  setSubscriptionData({ subscriptions: [] });
32
93
  setLoading(false);
94
+ inFlightRef.current = null;
95
+ lastFetchedRef.current = null;
33
96
  return;
34
97
  }
98
+ if (!force && lastFetchedRef.current === currentCustomerRef && inFlightRef.current !== currentCustomerRef) {
99
+ return;
100
+ }
101
+ if (inFlightRef.current === currentCustomerRef) {
102
+ return;
103
+ }
104
+ inFlightRef.current = currentCustomerRef;
35
105
  setLoading(true);
36
106
  try {
37
- const data = await checkSubscription(currentCustomerRef);
38
- setSubscriptionData(data);
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
+ }
39
116
  } catch (error) {
40
- console.error("\u274C [SolvaPayProvider] Failed to fetch subscription:", error);
41
- setSubscriptionData({
42
- customerRef: currentCustomerRef,
43
- subscriptions: []
44
- });
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;
124
+ }
45
125
  } finally {
46
- setLoading(false);
126
+ if (inFlightRef.current === currentCustomerRef) {
127
+ setLoading(false);
128
+ inFlightRef.current = null;
129
+ }
130
+ }
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);
47
141
  }
48
- }, [internalCustomerRef, checkSubscription]);
142
+ }, [customerRef, internalCustomerRef]);
49
143
  useEffect(() => {
50
- fetchSubscription();
51
- }, [fetchSubscription]);
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;
154
+ setLoading(true);
155
+ try {
156
+ const data = await checkSubscriptionRef.current(customerRef);
157
+ if (inFlightRef.current === customerRef) {
158
+ const filteredData = {
159
+ ...data,
160
+ subscriptions: filterSubscriptions(data.subscriptions || [])
161
+ };
162
+ setSubscriptionData(filteredData);
163
+ lastFetchedRef.current = customerRef;
164
+ }
165
+ } catch (error) {
166
+ console.error("[SolvaPayProvider] Failed to fetch subscription:", error);
167
+ if (inFlightRef.current === customerRef) {
168
+ setSubscriptionData({
169
+ customerRef,
170
+ subscriptions: []
171
+ });
172
+ lastFetchedRef.current = customerRef;
173
+ }
174
+ } finally {
175
+ if (inFlightRef.current === customerRef) {
176
+ setLoading(false);
177
+ inFlightRef.current = null;
178
+ }
179
+ }
180
+ };
181
+ doFetch();
182
+ }, [customerRef]);
52
183
  const updateCustomerRef = useCallback((newCustomerRef) => {
53
184
  const previousRef = internalCustomerRef;
54
185
  setInternalCustomerRef(newCustomerRef);
55
186
  if (onCustomerRefUpdate) {
56
187
  onCustomerRefUpdate(newCustomerRef);
57
188
  }
58
- if (previousRef !== newCustomerRef) {
59
- }
60
189
  }, [onCustomerRefUpdate, internalCustomerRef]);
61
190
  const subscription = useMemo(() => ({
62
191
  loading,
@@ -75,20 +204,21 @@ var SolvaPayProvider = ({
75
204
  }), [loading, subscriptionData]);
76
205
  const contextValue = useMemo(() => ({
77
206
  subscription,
78
- refetchSubscription: fetchSubscription,
207
+ refetchSubscription,
79
208
  createPayment,
209
+ processPayment,
80
210
  customerRef: internalCustomerRef,
81
211
  updateCustomerRef
82
- }), [subscription, fetchSubscription, createPayment, internalCustomerRef, updateCustomerRef]);
212
+ }), [subscription, refetchSubscription, createPayment, processPayment, internalCustomerRef, updateCustomerRef]);
83
213
  return /* @__PURE__ */ jsx(SolvaPayContext.Provider, { value: contextValue, children });
84
214
  };
85
215
 
86
216
  // src/PaymentForm.tsx
87
- import { useState as useState3, useEffect as useEffect2, Suspense, useRef as useRef2 } from "react";
88
- import { useStripe, useElements, PaymentElement, Elements } from "@stripe/react-stripe-js";
217
+ import { useEffect as useEffect2, useCallback as useCallback3, useRef as useRef3, useMemo as useMemo2, useState as useState4 } from "react";
218
+ import { Elements } from "@stripe/react-stripe-js";
89
219
 
90
220
  // src/hooks/useCheckout.ts
91
- import { useState as useState2, useCallback as useCallback2, useRef } from "react";
221
+ import { useState as useState2, useCallback as useCallback2, useRef as useRef2 } from "react";
92
222
  import { loadStripe } from "@stripe/stripe-js";
93
223
 
94
224
  // src/hooks/useSolvaPay.ts
@@ -104,6 +234,10 @@ function useSolvaPay() {
104
234
  }
105
235
 
106
236
  // src/hooks/useCheckout.ts
237
+ var stripePromiseCache = /* @__PURE__ */ new Map();
238
+ function getStripeCacheKey(publishableKey, accountId) {
239
+ return accountId ? `${publishableKey}:${accountId}` : publishableKey;
240
+ }
107
241
  function useCheckout(planRef) {
108
242
  const { createPayment, customerRef, updateCustomerRef } = useSolvaPay();
109
243
  const [loading, setLoading] = useState2(false);
@@ -112,7 +246,7 @@ function useCheckout(planRef) {
112
246
  );
113
247
  const [stripePromise, setStripePromise] = useState2(null);
114
248
  const [clientSecret, setClientSecret] = useState2(null);
115
- const isStartingRef = useRef(false);
249
+ const isStartingRef = useRef2(false);
116
250
  const startCheckout = useCallback2(async () => {
117
251
  if (isStartingRef.current || loading) {
118
252
  return;
@@ -139,22 +273,16 @@ function useCheckout(planRef) {
139
273
  if (!result.publishableKey || typeof result.publishableKey !== "string") {
140
274
  throw new Error("Invalid publishable key in payment intent response");
141
275
  }
142
- if (result.customerRef && result.customerRef !== customerRef) {
143
- console.log(`\u{1F504} [useCheckout] Backend returned different customerRef:`, {
144
- sent: customerRef,
145
- received: result.customerRef
146
- });
147
- if (updateCustomerRef) {
148
- updateCustomerRef(result.customerRef);
149
- console.log("\u2705 [useCheckout] Customer reference updated via callback");
150
- } else {
151
- console.warn("\u26A0\uFE0F [useCheckout] No updateCustomerRef callback available!");
152
- }
153
- } else if (result.customerRef === customerRef) {
154
- console.log("\u2705 [useCheckout] Customer reference unchanged:", customerRef);
276
+ if (result.customerRef && result.customerRef !== customerRef && updateCustomerRef) {
277
+ updateCustomerRef(result.customerRef);
155
278
  }
156
279
  const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
157
- const stripe = loadStripe(result.publishableKey, stripeOptions);
280
+ const cacheKey = getStripeCacheKey(result.publishableKey, result.accountId);
281
+ let stripe = stripePromiseCache.get(cacheKey);
282
+ if (!stripe) {
283
+ stripe = loadStripe(result.publishableKey, stripeOptions);
284
+ stripePromiseCache.set(cacheKey, stripe);
285
+ }
158
286
  setStripePromise(stripe);
159
287
  setClientSecret(result.clientSecret);
160
288
  } catch (err) {
@@ -191,8 +319,8 @@ function useSubscription() {
191
319
  };
192
320
  }
193
321
 
194
- // src/PaymentForm.tsx
195
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
322
+ // src/components/Spinner.tsx
323
+ import { jsx as jsx2 } from "react/jsx-runtime";
196
324
  var Spinner = ({
197
325
  className = "",
198
326
  size = "md"
@@ -211,15 +339,12 @@ var Spinner = ({
211
339
  }
212
340
  );
213
341
  };
214
- var StripePaymentFormWrapper = (props) => {
215
- const stripe = useStripe();
216
- const elements = useElements();
217
- if (!stripe || !elements) {
218
- return /* @__PURE__ */ jsx2("div", { className: "space-y-4", children: /* @__PURE__ */ jsx2("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center", children: /* @__PURE__ */ jsx2(Spinner, { size: "md" }) }) });
219
- }
220
- return /* @__PURE__ */ jsx2(StripePaymentForm, { ...props });
221
- };
222
- var StripePaymentForm = ({
342
+
343
+ // src/components/StripePaymentFormWrapper.tsx
344
+ 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";
347
+ var StripePaymentFormWrapper = ({
223
348
  onSuccess,
224
349
  onError,
225
350
  returnUrl,
@@ -230,6 +355,7 @@ var StripePaymentForm = ({
230
355
  const elements = useElements();
231
356
  const [isProcessing, setIsProcessing] = useState3(false);
232
357
  const [message, setMessage] = useState3(null);
358
+ const [isPaymentElementReady, setIsPaymentElementReady] = useState3(false);
233
359
  const handleSubmit = async (event) => {
234
360
  event.preventDefault();
235
361
  if (!stripe || !elements) {
@@ -277,6 +403,7 @@ var StripePaymentForm = ({
277
403
  }
278
404
  };
279
405
  const isSuccess = message?.includes("successful") ?? false;
406
+ const isReady = !!(stripe && elements);
280
407
  return /* @__PURE__ */ jsxs(
281
408
  "form",
282
409
  {
@@ -284,8 +411,16 @@ var StripePaymentForm = ({
284
411
  className: "space-y-6",
285
412
  noValidate: true,
286
413
  children: [
287
- /* @__PURE__ */ jsx2(PaymentElement, {}),
288
- message && /* @__PURE__ */ jsx2(
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(
289
424
  "div",
290
425
  {
291
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"}`,
@@ -295,206 +430,394 @@ var StripePaymentForm = ({
295
430
  children: message
296
431
  }
297
432
  ),
298
- /* @__PURE__ */ jsx2(
433
+ /* @__PURE__ */ jsx3(
299
434
  "button",
300
435
  {
301
436
  type: "submit",
302
- disabled: !stripe || isProcessing,
437
+ disabled: !isReady || !isPaymentElementReady || isProcessing,
303
438
  className: buttonClassName,
304
- "aria-busy": isProcessing,
305
- "aria-disabled": !stripe || isProcessing,
306
- children: isProcessing ? /* @__PURE__ */ jsx2("span", { className: "flex items-center justify-center gap-2", children: /* @__PURE__ */ jsx2(Spinner, { size: "sm" }) }) : submitButtonText
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
307
442
  }
308
443
  )
309
444
  ]
310
445
  }
311
446
  );
312
447
  };
448
+
449
+ // src/PaymentForm.tsx
450
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
313
451
  var PaymentForm = ({
314
452
  planRef,
453
+ agentRef,
315
454
  onSuccess,
316
455
  onError,
317
456
  returnUrl,
318
457
  submitButtonText = "Pay Now",
319
458
  className,
320
- buttonClassName,
321
- initialButtonText,
322
- cancelButtonText = "Cancel"
459
+ buttonClassName
323
460
  }) => {
324
461
  const validPlanRef = planRef && typeof planRef === "string" ? planRef : "";
325
- const { loading, error, stripePromise, clientSecret, startCheckout, reset } = useCheckout(validPlanRef);
462
+ const checkout = useCheckout(validPlanRef);
326
463
  const { refetch } = useSubscription();
327
- const [showPaymentForm, setShowPaymentForm] = useState3(false);
328
- const [hasStartedCheckout, setHasStartedCheckout] = useState3(false);
329
- const initializationKey = useRef2("");
330
- const planRefError = !planRef || typeof planRef !== "string" ? new Error("PaymentForm: planRef is required") : null;
331
- if (planRefError) {
332
- return /* @__PURE__ */ jsx2("div", { className, children: /* @__PURE__ */ jsx2("div", { className: "mt-4 p-4 bg-red-50 border border-red-400 rounded-lg text-red-700", children: planRefError.message }) });
333
- }
464
+ const { processPayment, customerRef } = useSolvaPay();
465
+ const hasInitializedRef = useRef3(false);
334
466
  useEffect2(() => {
335
- const key = `${planRef}-${initialButtonText}`;
336
- if (initializationKey.current === key) {
337
- return;
467
+ if (!hasInitializedRef.current && validPlanRef && !checkout.loading && !checkout.error && !checkout.clientSecret) {
468
+ hasInitializedRef.current = true;
469
+ checkout.startCheckout().catch(() => {
470
+ hasInitializedRef.current = false;
471
+ });
338
472
  }
339
- setShowPaymentForm(false);
340
- setHasStartedCheckout(false);
341
- reset();
342
- initializationKey.current = key;
343
- if (!initialButtonText && planRef && !loading && !error) {
344
- requestAnimationFrame(() => {
345
- setHasStartedCheckout(true);
346
- startCheckout().then(() => {
347
- setShowPaymentForm(true);
348
- }).catch(() => {
473
+ if (validPlanRef && checkout.clientSecret) {
474
+ hasInitializedRef.current = true;
475
+ }
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
349
485
  });
350
- });
486
+ await refetch();
487
+ } catch (error) {
488
+ console.error("[PaymentForm] Failed to process payment:", error);
489
+ await refetch();
490
+ }
491
+ } else {
492
+ await refetch();
351
493
  }
352
- }, [planRef, initialButtonText, loading, error, reset]);
353
- const handleStartCheckout = async () => {
354
- setHasStartedCheckout(true);
355
- await startCheckout();
356
- setShowPaymentForm(true);
357
- };
358
- const handleSuccess = async (paymentIntent) => {
359
- setShowPaymentForm(false);
360
- reset();
361
- await refetch();
362
494
  if (onSuccess) {
363
495
  onSuccess(paymentIntent);
364
496
  }
365
- };
366
- const handleCancel = () => {
367
- setShowPaymentForm(false);
368
- reset();
369
- };
370
- const handleError = (err) => {
497
+ }, [processPayment, customerRef, agentRef, planRef, refetch, onSuccess]);
498
+ const handleError = useCallback3((err) => {
371
499
  if (onError) {
372
500
  onError(err);
373
501
  }
374
- };
502
+ }, [onError]);
375
503
  const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
376
- const shouldShowForm = (!initialButtonText || showPaymentForm) && stripePromise && clientSecret;
377
- if (shouldShowForm) {
378
- return /* @__PURE__ */ jsxs("div", { className, children: [
379
- /* @__PURE__ */ jsx2(Suspense, { fallback: /* @__PURE__ */ jsx2("div", { className: "space-y-4", children: /* @__PURE__ */ jsx2("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center", children: /* @__PURE__ */ jsx2(Spinner, { size: "md" }) }) }), children: /* @__PURE__ */ jsx2(
380
- Elements,
381
- {
382
- stripe: stripePromise,
383
- options: { clientSecret },
384
- children: /* @__PURE__ */ jsx2(
385
- StripePaymentFormWrapper,
386
- {
387
- onSuccess: handleSuccess,
388
- onError: handleError,
389
- returnUrl: finalReturnUrl,
390
- submitButtonText,
391
- buttonClassName
392
- }
393
- )
394
- },
395
- clientSecret
396
- ) }),
397
- cancelButtonText && /* @__PURE__ */ jsx2(
398
- "button",
399
- {
400
- onClick: handleCancel,
401
- type: "button",
402
- className: "mt-6 w-full px-4 py-2 text-xs text-slate-600 bg-transparent rounded-full hover:text-slate-900 hover:bg-slate-50 font-medium transition-all duration-200",
403
- children: cancelButtonText
404
- }
405
- )
406
- ] });
407
- }
408
- return /* @__PURE__ */ jsxs("div", { className, children: [
409
- loading && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
410
- /* @__PURE__ */ jsx2("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center", children: /* @__PURE__ */ jsx2(Spinner, { size: "md" }) }),
411
- initialButtonText && /* @__PURE__ */ jsx2(
504
+ const isValidPlanRef = planRef && typeof planRef === "string";
505
+ const hasError = !!checkout.error;
506
+ const hasStripeData = !!(checkout.stripePromise && checkout.clientSecret);
507
+ const elementsOptions = useMemo2(() => {
508
+ if (!checkout.clientSecret) return void 0;
509
+ return { clientSecret: checkout.clientSecret };
510
+ }, [checkout.clientSecret]);
511
+ const [hasMountedElements, setHasMountedElements] = useState4(false);
512
+ useEffect2(() => {
513
+ if (hasStripeData) {
514
+ setHasMountedElements(true);
515
+ }
516
+ }, [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 ? (
522
+ // Once we have Stripe data, always render Elements to maintain hook consistency
523
+ // This prevents hook count mismatches when transitioning between states
524
+ /* @__PURE__ */ jsx4(
525
+ Elements,
526
+ {
527
+ stripe: checkout.stripePromise,
528
+ options: elementsOptions,
529
+ children: /* @__PURE__ */ jsx4(
530
+ StripePaymentFormWrapper,
531
+ {
532
+ onSuccess: handleSuccess,
533
+ onError: handleError,
534
+ returnUrl: finalReturnUrl,
535
+ submitButtonText,
536
+ buttonClassName
537
+ }
538
+ )
539
+ },
540
+ checkout.clientSecret
541
+ )
542
+ ) : (
543
+ // 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" }) }),
546
+ /* @__PURE__ */ jsx4(
412
547
  "button",
413
548
  {
414
549
  disabled: true,
415
550
  className: buttonClassName,
416
551
  "aria-busy": "true",
417
- children: /* @__PURE__ */ jsx2("span", { className: "flex items-center justify-center gap-2", children: /* @__PURE__ */ jsx2(Spinner, { size: "sm" }) })
552
+ children: /* @__PURE__ */ jsx4("span", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx4(Spinner, { size: "sm" }) })
418
553
  }
419
554
  )
420
- ] }),
421
- error && /* @__PURE__ */ jsxs(Fragment, { children: [
422
- initialButtonText && /* @__PURE__ */ jsx2(
423
- "button",
424
- {
425
- onClick: () => {
426
- reset();
427
- handleStartCheckout();
428
- },
429
- className: buttonClassName,
430
- children: initialButtonText
431
- }
432
- ),
433
- /* @__PURE__ */ jsx2("div", { className: "mt-4 p-4 bg-red-50 border border-red-400 rounded-lg text-red-700", children: error.message })
434
- ] }),
435
- !loading && !error && initialButtonText && /* @__PURE__ */ jsx2(
436
- "button",
437
- {
438
- onClick: handleStartCheckout,
439
- className: buttonClassName,
440
- children: initialButtonText
441
- }
442
- )
443
- ] });
555
+ ] })
556
+ ) });
444
557
  };
445
558
 
446
559
  // src/components/PlanBadge.tsx
447
- import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
560
+ import { useState as useState5, useEffect as useEffect3, useRef as useRef4 } from "react";
561
+ import { Fragment, jsx as jsx5 } from "react/jsx-runtime";
448
562
  var PlanBadge = ({
449
563
  children,
450
564
  as: Component = "div",
451
565
  className
452
566
  }) => {
453
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;
454
611
  if (children) {
455
- return /* @__PURE__ */ jsx3(Fragment2, { children: children({ subscriptions, loading }) });
612
+ return /* @__PURE__ */ jsx5(Fragment, { children: children({ subscriptions, loading, displayPlan: planToDisplay, shouldShow }) });
613
+ }
614
+ if (!shouldShow) {
615
+ return null;
456
616
  }
457
617
  const computedClassName = typeof className === "function" ? className({ subscriptions }) : className;
458
- const activeSubs = subscriptions.filter((sub) => sub.status === "active");
459
- const latestSub = activeSubs.length > 0 ? activeSubs.reduce((latest, current) => {
460
- return new Date(current.startDate) > new Date(latest.startDate) ? current : latest;
461
- }) : null;
462
- const displayText = loading ? "Loading..." : latestSub ? latestSub.planName : "Free";
463
- return /* @__PURE__ */ jsx3(
618
+ return /* @__PURE__ */ jsx5(
464
619
  Component,
465
620
  {
466
621
  className: computedClassName,
467
622
  "data-loading": loading,
468
- "data-has-subscription": activeSubs.length > 0,
623
+ "data-has-subscription": !!primarySubscription,
469
624
  role: "status",
470
625
  "aria-live": "polite",
471
626
  "aria-busy": loading,
472
- "aria-label": loading ? "Loading subscription status" : `Current plan: ${displayText}`,
473
- children: displayText
627
+ "aria-label": `Current plan: ${planToDisplay}`,
628
+ children: planToDisplay
474
629
  }
475
630
  );
476
631
  };
477
632
 
478
633
  // src/components/SubscriptionGate.tsx
479
- import { Fragment as Fragment3, jsx as jsx4 } from "react/jsx-runtime";
634
+ import { Fragment as Fragment2, jsx as jsx6 } from "react/jsx-runtime";
480
635
  var SubscriptionGate = ({
481
636
  requirePlan,
482
637
  children
483
638
  }) => {
484
639
  const { subscriptions, loading, hasPlan } = useSubscription();
485
640
  const hasAccess = requirePlan ? hasPlan(requirePlan) : subscriptions.some((sub) => sub.status === "active");
486
- return /* @__PURE__ */ jsx4(Fragment3, { children: children({
641
+ return /* @__PURE__ */ jsx6(Fragment2, { children: children({
487
642
  hasAccess,
488
643
  subscriptions,
489
644
  loading
490
645
  }) });
491
646
  };
647
+
648
+ // src/components/PlanSelector.tsx
649
+ import { useCallback as useCallback5, useMemo as useMemo4 } from "react";
650
+
651
+ // src/hooks/usePlans.ts
652
+ import { useState as useState6, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo3 } from "react";
653
+ 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);
679
+ }
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(() => {
687
+ fetchPlans();
688
+ }, [fetchPlans]);
689
+ const selectedPlan = useMemo3(() => {
690
+ return plans[selectedPlanIndex] || null;
691
+ }, [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]);
698
+ return {
699
+ plans,
700
+ loading,
701
+ error,
702
+ selectedPlanIndex,
703
+ selectedPlan,
704
+ setSelectedPlanIndex,
705
+ selectPlan,
706
+ refetch: fetchPlans
707
+ };
708
+ }
709
+
710
+ // src/components/PlanSelector.tsx
711
+ import { Fragment as Fragment3, jsx as jsx7 } from "react/jsx-runtime";
712
+ var PlanSelector = ({
713
+ agentRef,
714
+ fetcher,
715
+ filter,
716
+ sortBy,
717
+ autoSelectFirstPaid,
718
+ children
719
+ }) => {
720
+ const { subscriptions } = useSubscription();
721
+ const plansHook = usePlans({
722
+ agentRef,
723
+ fetcher,
724
+ filter,
725
+ sortBy,
726
+ autoSelectFirstPaid
727
+ });
728
+ 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(
736
+ (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
737
+ )[0] || null;
738
+ }, [subscriptions]);
739
+ const isCurrentPlan = useCallback5((planName) => {
740
+ return activeSubscription?.planName === planName;
741
+ }, [activeSubscription]);
742
+ return /* @__PURE__ */ jsx7(Fragment3, { children: children({
743
+ ...plansHook,
744
+ subscriptions,
745
+ isPaidPlan,
746
+ isCurrentPlan
747
+ }) });
748
+ };
749
+
750
+ // src/hooks/useSubscriptionHelpers.ts
751
+ 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));
769
+ return {
770
+ activePaidSubscription,
771
+ activePlanName: activePaidSubscription?.planName || null,
772
+ cancelledSubscription,
773
+ hasPaidSubscription: activePaidSubscriptions.length > 0,
774
+ shouldShowCancelledNotice
775
+ };
776
+ }, [subscriptions, isPaidPlan]);
777
+ const formatDate = useCallback6((dateString) => {
778
+ if (!dateString) return null;
779
+ return new Date(dateString).toLocaleDateString("en-US", {
780
+ year: "numeric",
781
+ month: "long",
782
+ day: "numeric"
783
+ });
784
+ }, []);
785
+ const getDaysUntilExpiration = useCallback6((endDate) => {
786
+ if (!endDate) return null;
787
+ const now = /* @__PURE__ */ new Date();
788
+ const expiration = new Date(endDate);
789
+ const diffTime = expiration.getTime() - now.getTime();
790
+ const diffDays = Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
791
+ return diffDays > 0 ? diffDays : 0;
792
+ }, []);
793
+ return {
794
+ isPaidPlan,
795
+ activePaidSubscription: subscriptionData.activePaidSubscription,
796
+ cancelledSubscription: subscriptionData.cancelledSubscription,
797
+ hasPaidSubscription: subscriptionData.hasPaidSubscription,
798
+ shouldShowCancelledNotice: subscriptionData.shouldShowCancelledNotice,
799
+ activePlanName: subscriptionData.activePlanName,
800
+ formatDate,
801
+ getDaysUntilExpiration
802
+ };
803
+ }
492
804
  export {
493
805
  PaymentForm,
494
806
  PlanBadge,
807
+ PlanSelector,
495
808
  SolvaPayProvider,
809
+ Spinner,
810
+ StripePaymentFormWrapper,
496
811
  SubscriptionGate,
812
+ filterSubscriptions,
813
+ getActiveSubscriptions,
814
+ getCancelledSubscriptionsWithEndDate,
815
+ getMostRecentSubscription,
816
+ getPrimarySubscription,
817
+ hasActivePaidSubscription,
497
818
  useCheckout,
819
+ usePlans,
498
820
  useSolvaPay,
499
- useSubscription
821
+ useSubscription,
822
+ useSubscriptionHelpers
500
823
  };