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

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
@@ -22,21 +22,92 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  PaymentForm: () => PaymentForm,
24
24
  PlanBadge: () => PlanBadge,
25
+ PlanSelector: () => PlanSelector,
25
26
  SolvaPayProvider: () => SolvaPayProvider,
27
+ Spinner: () => Spinner,
28
+ StripePaymentFormWrapper: () => StripePaymentFormWrapper,
26
29
  SubscriptionGate: () => SubscriptionGate,
30
+ filterSubscriptions: () => filterSubscriptions,
31
+ getActiveSubscriptions: () => getActiveSubscriptions,
32
+ getCancelledSubscriptionsWithEndDate: () => getCancelledSubscriptionsWithEndDate,
33
+ getMostRecentSubscription: () => getMostRecentSubscription,
34
+ getPrimarySubscription: () => getPrimarySubscription,
35
+ hasActivePaidSubscription: () => hasActivePaidSubscription,
27
36
  useCheckout: () => useCheckout,
37
+ usePlans: () => usePlans,
28
38
  useSolvaPay: () => useSolvaPay,
29
- useSubscription: () => useSubscription
39
+ useSubscription: () => useSubscription,
40
+ useSubscriptionHelpers: () => useSubscriptionHelpers
30
41
  });
31
42
  module.exports = __toCommonJS(index_exports);
32
43
 
33
44
  // src/SolvaPayProvider.tsx
34
45
  var import_react = require("react");
46
+
47
+ // src/utils/subscriptions.ts
48
+ function filterSubscriptions(subscriptions) {
49
+ const now = /* @__PURE__ */ new Date();
50
+ const filtered = subscriptions.filter((sub) => {
51
+ const subAny = sub;
52
+ const isCancelled = sub.status === "cancelled" || subAny.cancelledAt;
53
+ if (!isCancelled) {
54
+ return true;
55
+ }
56
+ if (isCancelled && subAny.endDate) {
57
+ const endDate = new Date(subAny.endDate);
58
+ const isFuture = endDate > now;
59
+ return isFuture;
60
+ }
61
+ return false;
62
+ });
63
+ return filtered;
64
+ }
65
+ function getActiveSubscriptions(subscriptions) {
66
+ return subscriptions.filter((sub) => {
67
+ const subAny = sub;
68
+ return sub.status === "active" && !subAny.cancelledAt;
69
+ });
70
+ }
71
+ function getCancelledSubscriptionsWithEndDate(subscriptions) {
72
+ return subscriptions.filter((sub) => {
73
+ const subAny = sub;
74
+ const isCancelled = sub.status === "cancelled" || subAny.cancelledAt;
75
+ return isCancelled && subAny.endDate && new Date(subAny.endDate) > /* @__PURE__ */ new Date();
76
+ });
77
+ }
78
+ function getMostRecentSubscription(subscriptions) {
79
+ if (subscriptions.length === 0) return null;
80
+ return subscriptions.reduce((latest, current) => {
81
+ return new Date(current.startDate) > new Date(latest.startDate) ? current : latest;
82
+ });
83
+ }
84
+ function getPrimarySubscription(subscriptions) {
85
+ const filtered = filterSubscriptions(subscriptions);
86
+ const activeSubs = getActiveSubscriptions(filtered);
87
+ if (activeSubs.length > 0) {
88
+ return getMostRecentSubscription(activeSubs);
89
+ }
90
+ const cancelledWithEndDate = getCancelledSubscriptionsWithEndDate(filtered);
91
+ if (cancelledWithEndDate.length > 0) {
92
+ return getMostRecentSubscription(cancelledWithEndDate);
93
+ }
94
+ return null;
95
+ }
96
+ function hasActivePaidSubscription(subscriptions, isPaidPlan) {
97
+ const filtered = filterSubscriptions(subscriptions);
98
+ const activePaid = getActiveSubscriptions(filtered).some((sub) => isPaidPlan(sub.planName));
99
+ if (activePaid) return true;
100
+ const cancelledPaid = getCancelledSubscriptionsWithEndDate(filtered).some((sub) => isPaidPlan(sub.planName));
101
+ return cancelledPaid;
102
+ }
103
+
104
+ // src/SolvaPayProvider.tsx
35
105
  var import_jsx_runtime = require("react/jsx-runtime");
36
106
  var SolvaPayContext = (0, import_react.createContext)(null);
37
107
  var SolvaPayProvider = ({
38
108
  createPayment,
39
109
  checkSubscription,
110
+ processPayment,
40
111
  customerRef,
41
112
  onCustomerRefUpdate,
42
113
  children
@@ -52,43 +123,112 @@ var SolvaPayProvider = ({
52
123
  });
53
124
  const [loading, setLoading] = (0, import_react.useState)(false);
54
125
  const [internalCustomerRef, setInternalCustomerRef] = (0, import_react.useState)(customerRef);
126
+ const inFlightRef = (0, import_react.useRef)(null);
127
+ const lastFetchedRef = (0, import_react.useRef)(null);
128
+ const checkSubscriptionRef = (0, import_react.useRef)(checkSubscription);
55
129
  (0, import_react.useEffect)(() => {
56
- if (customerRef && customerRef !== internalCustomerRef) {
57
- setInternalCustomerRef(customerRef);
58
- }
59
- }, [customerRef, internalCustomerRef]);
60
- const fetchSubscription = (0, import_react.useCallback)(async () => {
130
+ checkSubscriptionRef.current = checkSubscription;
131
+ }, [checkSubscription]);
132
+ const fetchSubscription = (0, import_react.useCallback)(async (force = false) => {
61
133
  const currentCustomerRef = internalCustomerRef;
62
134
  if (!currentCustomerRef) {
63
135
  setSubscriptionData({ subscriptions: [] });
64
136
  setLoading(false);
137
+ inFlightRef.current = null;
138
+ lastFetchedRef.current = null;
139
+ return;
140
+ }
141
+ if (!force && lastFetchedRef.current === currentCustomerRef && inFlightRef.current !== currentCustomerRef) {
65
142
  return;
66
143
  }
144
+ if (inFlightRef.current === currentCustomerRef) {
145
+ return;
146
+ }
147
+ inFlightRef.current = currentCustomerRef;
67
148
  setLoading(true);
68
149
  try {
69
- const data = await checkSubscription(currentCustomerRef);
70
- setSubscriptionData(data);
150
+ const data = await checkSubscriptionRef.current(currentCustomerRef);
151
+ if (inFlightRef.current === currentCustomerRef) {
152
+ const filteredData = {
153
+ ...data,
154
+ subscriptions: filterSubscriptions(data.subscriptions || [])
155
+ };
156
+ setSubscriptionData(filteredData);
157
+ lastFetchedRef.current = currentCustomerRef;
158
+ }
71
159
  } catch (error) {
72
- console.error("\u274C [SolvaPayProvider] Failed to fetch subscription:", error);
73
- setSubscriptionData({
74
- customerRef: currentCustomerRef,
75
- subscriptions: []
76
- });
160
+ console.error("[SolvaPayProvider] Failed to fetch subscription:", error);
161
+ if (inFlightRef.current === currentCustomerRef) {
162
+ setSubscriptionData({
163
+ customerRef: currentCustomerRef,
164
+ subscriptions: []
165
+ });
166
+ lastFetchedRef.current = currentCustomerRef;
167
+ }
77
168
  } finally {
78
- setLoading(false);
169
+ if (inFlightRef.current === currentCustomerRef) {
170
+ setLoading(false);
171
+ inFlightRef.current = null;
172
+ }
79
173
  }
80
- }, [internalCustomerRef, checkSubscription]);
174
+ }, [internalCustomerRef]);
175
+ const refetchSubscription = (0, import_react.useCallback)(async () => {
176
+ const currentCustomerRef = internalCustomerRef;
177
+ lastFetchedRef.current = null;
178
+ inFlightRef.current = null;
179
+ await fetchSubscription(true);
180
+ }, [fetchSubscription, internalCustomerRef]);
181
+ (0, import_react.useEffect)(() => {
182
+ if (customerRef !== internalCustomerRef) {
183
+ setInternalCustomerRef(customerRef);
184
+ }
185
+ }, [customerRef, internalCustomerRef]);
81
186
  (0, import_react.useEffect)(() => {
82
- fetchSubscription();
83
- }, [fetchSubscription]);
187
+ if (!customerRef) {
188
+ setSubscriptionData({ subscriptions: [] });
189
+ setLoading(false);
190
+ return;
191
+ }
192
+ if (customerRef === lastFetchedRef.current || customerRef === inFlightRef.current) {
193
+ return;
194
+ }
195
+ const doFetch = async () => {
196
+ inFlightRef.current = customerRef;
197
+ setLoading(true);
198
+ try {
199
+ const data = await checkSubscriptionRef.current(customerRef);
200
+ if (inFlightRef.current === customerRef) {
201
+ const filteredData = {
202
+ ...data,
203
+ subscriptions: filterSubscriptions(data.subscriptions || [])
204
+ };
205
+ setSubscriptionData(filteredData);
206
+ lastFetchedRef.current = customerRef;
207
+ }
208
+ } catch (error) {
209
+ console.error("[SolvaPayProvider] Failed to fetch subscription:", error);
210
+ if (inFlightRef.current === customerRef) {
211
+ setSubscriptionData({
212
+ customerRef,
213
+ subscriptions: []
214
+ });
215
+ lastFetchedRef.current = customerRef;
216
+ }
217
+ } finally {
218
+ if (inFlightRef.current === customerRef) {
219
+ setLoading(false);
220
+ inFlightRef.current = null;
221
+ }
222
+ }
223
+ };
224
+ doFetch();
225
+ }, [customerRef]);
84
226
  const updateCustomerRef = (0, import_react.useCallback)((newCustomerRef) => {
85
227
  const previousRef = internalCustomerRef;
86
228
  setInternalCustomerRef(newCustomerRef);
87
229
  if (onCustomerRefUpdate) {
88
230
  onCustomerRefUpdate(newCustomerRef);
89
231
  }
90
- if (previousRef !== newCustomerRef) {
91
- }
92
232
  }, [onCustomerRefUpdate, internalCustomerRef]);
93
233
  const subscription = (0, import_react.useMemo)(() => ({
94
234
  loading,
@@ -107,17 +247,18 @@ var SolvaPayProvider = ({
107
247
  }), [loading, subscriptionData]);
108
248
  const contextValue = (0, import_react.useMemo)(() => ({
109
249
  subscription,
110
- refetchSubscription: fetchSubscription,
250
+ refetchSubscription,
111
251
  createPayment,
252
+ processPayment,
112
253
  customerRef: internalCustomerRef,
113
254
  updateCustomerRef
114
- }), [subscription, fetchSubscription, createPayment, internalCustomerRef, updateCustomerRef]);
255
+ }), [subscription, refetchSubscription, createPayment, processPayment, internalCustomerRef, updateCustomerRef]);
115
256
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SolvaPayContext.Provider, { value: contextValue, children });
116
257
  };
117
258
 
118
259
  // src/PaymentForm.tsx
119
- var import_react4 = require("react");
120
- var import_react_stripe_js = require("@stripe/react-stripe-js");
260
+ var import_react5 = require("react");
261
+ var import_react_stripe_js2 = require("@stripe/react-stripe-js");
121
262
 
122
263
  // src/hooks/useCheckout.ts
123
264
  var import_react3 = require("react");
@@ -136,6 +277,10 @@ function useSolvaPay() {
136
277
  }
137
278
 
138
279
  // src/hooks/useCheckout.ts
280
+ var stripePromiseCache = /* @__PURE__ */ new Map();
281
+ function getStripeCacheKey(publishableKey, accountId) {
282
+ return accountId ? `${publishableKey}:${accountId}` : publishableKey;
283
+ }
139
284
  function useCheckout(planRef) {
140
285
  const { createPayment, customerRef, updateCustomerRef } = useSolvaPay();
141
286
  const [loading, setLoading] = (0, import_react3.useState)(false);
@@ -171,22 +316,16 @@ function useCheckout(planRef) {
171
316
  if (!result.publishableKey || typeof result.publishableKey !== "string") {
172
317
  throw new Error("Invalid publishable key in payment intent response");
173
318
  }
174
- if (result.customerRef && result.customerRef !== customerRef) {
175
- console.log(`\u{1F504} [useCheckout] Backend returned different customerRef:`, {
176
- sent: customerRef,
177
- received: result.customerRef
178
- });
179
- if (updateCustomerRef) {
180
- updateCustomerRef(result.customerRef);
181
- console.log("\u2705 [useCheckout] Customer reference updated via callback");
182
- } else {
183
- console.warn("\u26A0\uFE0F [useCheckout] No updateCustomerRef callback available!");
184
- }
185
- } else if (result.customerRef === customerRef) {
186
- console.log("\u2705 [useCheckout] Customer reference unchanged:", customerRef);
319
+ if (result.customerRef && result.customerRef !== customerRef && updateCustomerRef) {
320
+ updateCustomerRef(result.customerRef);
187
321
  }
188
322
  const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
189
- const stripe = (0, import_stripe_js.loadStripe)(result.publishableKey, stripeOptions);
323
+ const cacheKey = getStripeCacheKey(result.publishableKey, result.accountId);
324
+ let stripe = stripePromiseCache.get(cacheKey);
325
+ if (!stripe) {
326
+ stripe = (0, import_stripe_js.loadStripe)(result.publishableKey, stripeOptions);
327
+ stripePromiseCache.set(cacheKey, stripe);
328
+ }
190
329
  setStripePromise(stripe);
191
330
  setClientSecret(result.clientSecret);
192
331
  } catch (err) {
@@ -223,7 +362,7 @@ function useSubscription() {
223
362
  };
224
363
  }
225
364
 
226
- // src/PaymentForm.tsx
365
+ // src/components/Spinner.tsx
227
366
  var import_jsx_runtime2 = require("react/jsx-runtime");
228
367
  var Spinner = ({
229
368
  className = "",
@@ -243,15 +382,12 @@ var Spinner = ({
243
382
  }
244
383
  );
245
384
  };
246
- var StripePaymentFormWrapper = (props) => {
247
- const stripe = (0, import_react_stripe_js.useStripe)();
248
- const elements = (0, import_react_stripe_js.useElements)();
249
- if (!stripe || !elements) {
250
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "space-y-4", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner, { size: "md" }) }) });
251
- }
252
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StripePaymentForm, { ...props });
253
- };
254
- var StripePaymentForm = ({
385
+
386
+ // src/components/StripePaymentFormWrapper.tsx
387
+ var import_react4 = require("react");
388
+ var import_react_stripe_js = require("@stripe/react-stripe-js");
389
+ var import_jsx_runtime3 = require("react/jsx-runtime");
390
+ var StripePaymentFormWrapper = ({
255
391
  onSuccess,
256
392
  onError,
257
393
  returnUrl,
@@ -262,6 +398,7 @@ var StripePaymentForm = ({
262
398
  const elements = (0, import_react_stripe_js.useElements)();
263
399
  const [isProcessing, setIsProcessing] = (0, import_react4.useState)(false);
264
400
  const [message, setMessage] = (0, import_react4.useState)(null);
401
+ const [isPaymentElementReady, setIsPaymentElementReady] = (0, import_react4.useState)(false);
265
402
  const handleSubmit = async (event) => {
266
403
  event.preventDefault();
267
404
  if (!stripe || !elements) {
@@ -309,15 +446,24 @@ var StripePaymentForm = ({
309
446
  }
310
447
  };
311
448
  const isSuccess = message?.includes("successful") ?? false;
312
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
449
+ const isReady = !!(stripe && elements);
450
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
313
451
  "form",
314
452
  {
315
453
  onSubmit: handleSubmit,
316
454
  className: "space-y-6",
317
455
  noValidate: true,
318
456
  children: [
319
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_stripe_js.PaymentElement, {}),
320
- message && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
457
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative", children: [
458
+ (!isPaymentElementReady || !isReady) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: "md" }) }),
459
+ isReady && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: isPaymentElementReady ? "block" : "opacity-0 pointer-events-none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
460
+ import_react_stripe_js.PaymentElement,
461
+ {
462
+ onReady: () => setIsPaymentElementReady(true)
463
+ }
464
+ ) })
465
+ ] }),
466
+ message && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
321
467
  "div",
322
468
  {
323
469
  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"}`,
@@ -327,207 +473,395 @@ var StripePaymentForm = ({
327
473
  children: message
328
474
  }
329
475
  ),
330
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
476
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
331
477
  "button",
332
478
  {
333
479
  type: "submit",
334
- disabled: !stripe || isProcessing,
480
+ disabled: !isReady || !isPaymentElementReady || isProcessing,
335
481
  className: buttonClassName,
336
- "aria-busy": isProcessing,
337
- "aria-disabled": !stripe || isProcessing,
338
- children: isProcessing ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "flex items-center justify-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner, { size: "sm" }) }) : submitButtonText
482
+ "aria-busy": isProcessing || !isPaymentElementReady || !isReady,
483
+ "aria-disabled": !isReady || !isPaymentElementReady || isProcessing,
484
+ children: isProcessing ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: "sm" }) }) : !isReady || !isPaymentElementReady ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, { size: "sm" }) }) : submitButtonText
339
485
  }
340
486
  )
341
487
  ]
342
488
  }
343
489
  );
344
490
  };
491
+
492
+ // src/PaymentForm.tsx
493
+ var import_jsx_runtime4 = require("react/jsx-runtime");
345
494
  var PaymentForm = ({
346
495
  planRef,
496
+ agentRef,
347
497
  onSuccess,
348
498
  onError,
349
499
  returnUrl,
350
500
  submitButtonText = "Pay Now",
351
501
  className,
352
- buttonClassName,
353
- initialButtonText,
354
- cancelButtonText = "Cancel"
502
+ buttonClassName
355
503
  }) => {
356
504
  const validPlanRef = planRef && typeof planRef === "string" ? planRef : "";
357
- const { loading, error, stripePromise, clientSecret, startCheckout, reset } = useCheckout(validPlanRef);
505
+ const checkout = useCheckout(validPlanRef);
358
506
  const { refetch } = useSubscription();
359
- const [showPaymentForm, setShowPaymentForm] = (0, import_react4.useState)(false);
360
- const [hasStartedCheckout, setHasStartedCheckout] = (0, import_react4.useState)(false);
361
- const initializationKey = (0, import_react4.useRef)("");
362
- const planRefError = !planRef || typeof planRef !== "string" ? new Error("PaymentForm: planRef is required") : null;
363
- if (planRefError) {
364
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-4 p-4 bg-red-50 border border-red-400 rounded-lg text-red-700", children: planRefError.message }) });
365
- }
366
- (0, import_react4.useEffect)(() => {
367
- const key = `${planRef}-${initialButtonText}`;
368
- if (initializationKey.current === key) {
369
- return;
507
+ const { processPayment, customerRef } = useSolvaPay();
508
+ const hasInitializedRef = (0, import_react5.useRef)(false);
509
+ (0, import_react5.useEffect)(() => {
510
+ if (!hasInitializedRef.current && validPlanRef && !checkout.loading && !checkout.error && !checkout.clientSecret) {
511
+ hasInitializedRef.current = true;
512
+ checkout.startCheckout().catch(() => {
513
+ hasInitializedRef.current = false;
514
+ });
370
515
  }
371
- setShowPaymentForm(false);
372
- setHasStartedCheckout(false);
373
- reset();
374
- initializationKey.current = key;
375
- if (!initialButtonText && planRef && !loading && !error) {
376
- requestAnimationFrame(() => {
377
- setHasStartedCheckout(true);
378
- startCheckout().then(() => {
379
- setShowPaymentForm(true);
380
- }).catch(() => {
516
+ if (validPlanRef && checkout.clientSecret) {
517
+ hasInitializedRef.current = true;
518
+ }
519
+ }, [validPlanRef, checkout.loading, checkout.error, checkout.clientSecret, checkout.startCheckout]);
520
+ const handleSuccess = (0, import_react5.useCallback)(async (paymentIntent) => {
521
+ if (processPayment && customerRef && agentRef) {
522
+ try {
523
+ await processPayment({
524
+ paymentIntentId: paymentIntent.id,
525
+ agentRef,
526
+ customerRef,
527
+ planRef
381
528
  });
382
- });
529
+ await refetch();
530
+ } catch (error) {
531
+ console.error("[PaymentForm] Failed to process payment:", error);
532
+ await refetch();
533
+ }
534
+ } else {
535
+ await refetch();
383
536
  }
384
- }, [planRef, initialButtonText, loading, error, reset]);
385
- const handleStartCheckout = async () => {
386
- setHasStartedCheckout(true);
387
- await startCheckout();
388
- setShowPaymentForm(true);
389
- };
390
- const handleSuccess = async (paymentIntent) => {
391
- setShowPaymentForm(false);
392
- reset();
393
- await refetch();
394
537
  if (onSuccess) {
395
538
  onSuccess(paymentIntent);
396
539
  }
397
- };
398
- const handleCancel = () => {
399
- setShowPaymentForm(false);
400
- reset();
401
- };
402
- const handleError = (err) => {
540
+ }, [processPayment, customerRef, agentRef, planRef, refetch, onSuccess]);
541
+ const handleError = (0, import_react5.useCallback)((err) => {
403
542
  if (onError) {
404
543
  onError(err);
405
544
  }
406
- };
545
+ }, [onError]);
407
546
  const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
408
- const shouldShowForm = (!initialButtonText || showPaymentForm) && stripePromise && clientSecret;
409
- if (shouldShowForm) {
410
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className, children: [
411
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Suspense, { fallback: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "space-y-4", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner, { size: "md" }) }) }), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
412
- import_react_stripe_js.Elements,
413
- {
414
- stripe: stripePromise,
415
- options: { clientSecret },
416
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
417
- StripePaymentFormWrapper,
418
- {
419
- onSuccess: handleSuccess,
420
- onError: handleError,
421
- returnUrl: finalReturnUrl,
422
- submitButtonText,
423
- buttonClassName
424
- }
425
- )
426
- },
427
- clientSecret
428
- ) }),
429
- cancelButtonText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
430
- "button",
431
- {
432
- onClick: handleCancel,
433
- type: "button",
434
- 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",
435
- children: cancelButtonText
436
- }
437
- )
438
- ] });
439
- }
440
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className, children: [
441
- loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-4", children: [
442
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner, { size: "md" }) }),
443
- initialButtonText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
547
+ const isValidPlanRef = planRef && typeof planRef === "string";
548
+ const hasError = !!checkout.error;
549
+ const hasStripeData = !!(checkout.stripePromise && checkout.clientSecret);
550
+ const elementsOptions = (0, import_react5.useMemo)(() => {
551
+ if (!checkout.clientSecret) return void 0;
552
+ return { clientSecret: checkout.clientSecret };
553
+ }, [checkout.clientSecret]);
554
+ const [hasMountedElements, setHasMountedElements] = (0, import_react5.useState)(false);
555
+ (0, import_react5.useEffect)(() => {
556
+ if (hasStripeData) {
557
+ setHasMountedElements(true);
558
+ }
559
+ }, [hasStripeData]);
560
+ const shouldRenderElements = hasStripeData || hasMountedElements && checkout.stripePromise && checkout.clientSecret;
561
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className, children: !isValidPlanRef ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "p-4 bg-red-50 border border-red-400 rounded-lg text-red-700", children: [
562
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "font-medium mb-1", children: "Payment initialization failed" }),
563
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "text-sm", children: checkout.error?.message || "Unknown error" })
564
+ ] }) : shouldRenderElements && checkout.stripePromise && elementsOptions ? (
565
+ // Once we have Stripe data, always render Elements to maintain hook consistency
566
+ // This prevents hook count mismatches when transitioning between states
567
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
568
+ import_react_stripe_js2.Elements,
569
+ {
570
+ stripe: checkout.stripePromise,
571
+ options: elementsOptions,
572
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
573
+ StripePaymentFormWrapper,
574
+ {
575
+ onSuccess: handleSuccess,
576
+ onError: handleError,
577
+ returnUrl: finalReturnUrl,
578
+ submitButtonText,
579
+ buttonClassName
580
+ }
581
+ )
582
+ },
583
+ checkout.clientSecret
584
+ )
585
+ ) : (
586
+ // Loading state before Stripe data is available
587
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "space-y-4", children: [
588
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center min-h-[200px]", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Spinner, { size: "md" }) }),
589
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
444
590
  "button",
445
591
  {
446
592
  disabled: true,
447
593
  className: buttonClassName,
448
594
  "aria-busy": "true",
449
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "flex items-center justify-center gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner, { size: "sm" }) })
595
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Spinner, { size: "sm" }) })
450
596
  }
451
597
  )
452
- ] }),
453
- error && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
454
- initialButtonText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
455
- "button",
456
- {
457
- onClick: () => {
458
- reset();
459
- handleStartCheckout();
460
- },
461
- className: buttonClassName,
462
- children: initialButtonText
463
- }
464
- ),
465
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-4 p-4 bg-red-50 border border-red-400 rounded-lg text-red-700", children: error.message })
466
- ] }),
467
- !loading && !error && initialButtonText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
468
- "button",
469
- {
470
- onClick: handleStartCheckout,
471
- className: buttonClassName,
472
- children: initialButtonText
473
- }
474
- )
475
- ] });
598
+ ] })
599
+ ) });
476
600
  };
477
601
 
478
602
  // src/components/PlanBadge.tsx
479
- var import_jsx_runtime3 = require("react/jsx-runtime");
603
+ var import_react6 = require("react");
604
+ var import_jsx_runtime5 = require("react/jsx-runtime");
480
605
  var PlanBadge = ({
481
606
  children,
482
607
  as: Component = "div",
483
608
  className
484
609
  }) => {
485
610
  const { subscriptions, loading } = useSubscription();
611
+ const [displayPlan, setDisplayPlan] = (0, import_react6.useState)(null);
612
+ const [hasLoadedOnce, setHasLoadedOnce] = (0, import_react6.useState)(false);
613
+ const lastPlanRef = (0, import_react6.useRef)(null);
614
+ const lastLoadingRef = (0, import_react6.useRef)(true);
615
+ const previousSubscriptionsRef = (0, import_react6.useRef)(subscriptions);
616
+ const primarySubscription = getPrimarySubscription(subscriptions);
617
+ const currentPlanName = primarySubscription?.planName || null;
618
+ const fallbackPlanName = subscriptions.length > 0 && !currentPlanName ? subscriptions[0]?.planName || null : null;
619
+ const effectivePlanName = currentPlanName || fallbackPlanName;
620
+ (0, import_react6.useEffect)(() => {
621
+ if (!loading && !hasLoadedOnce) {
622
+ setHasLoadedOnce(true);
623
+ }
624
+ lastLoadingRef.current = loading;
625
+ }, [loading, hasLoadedOnce]);
626
+ (0, import_react6.useEffect)(() => {
627
+ const previousSubs = previousSubscriptionsRef.current;
628
+ if (previousSubs !== subscriptions) {
629
+ if (loading) {
630
+ setHasLoadedOnce(false);
631
+ }
632
+ setDisplayPlan(null);
633
+ lastPlanRef.current = null;
634
+ previousSubscriptionsRef.current = subscriptions;
635
+ }
636
+ }, [subscriptions, loading]);
637
+ (0, import_react6.useEffect)(() => {
638
+ const currentPlan = effectivePlanName;
639
+ const previousPlan = lastPlanRef.current;
640
+ if (currentPlan !== previousPlan) {
641
+ if (currentPlan !== null) {
642
+ lastPlanRef.current = currentPlan;
643
+ setDisplayPlan(currentPlan);
644
+ } else {
645
+ lastPlanRef.current = null;
646
+ setDisplayPlan(null);
647
+ }
648
+ } else if (currentPlan !== null && displayPlan === null) {
649
+ setDisplayPlan(currentPlan);
650
+ }
651
+ }, [effectivePlanName, displayPlan]);
652
+ const shouldShow = effectivePlanName !== null && hasLoadedOnce;
653
+ const planToDisplay = displayPlan ?? effectivePlanName;
486
654
  if (children) {
487
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: children({ subscriptions, loading }) });
655
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: children({ subscriptions, loading, displayPlan: planToDisplay, shouldShow }) });
656
+ }
657
+ if (!shouldShow) {
658
+ return null;
488
659
  }
489
660
  const computedClassName = typeof className === "function" ? className({ subscriptions }) : className;
490
- const activeSubs = subscriptions.filter((sub) => sub.status === "active");
491
- const latestSub = activeSubs.length > 0 ? activeSubs.reduce((latest, current) => {
492
- return new Date(current.startDate) > new Date(latest.startDate) ? current : latest;
493
- }) : null;
494
- const displayText = loading ? "Loading..." : latestSub ? latestSub.planName : "Free";
495
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
661
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
496
662
  Component,
497
663
  {
498
664
  className: computedClassName,
499
665
  "data-loading": loading,
500
- "data-has-subscription": activeSubs.length > 0,
666
+ "data-has-subscription": !!primarySubscription,
501
667
  role: "status",
502
668
  "aria-live": "polite",
503
669
  "aria-busy": loading,
504
- "aria-label": loading ? "Loading subscription status" : `Current plan: ${displayText}`,
505
- children: displayText
670
+ "aria-label": `Current plan: ${planToDisplay}`,
671
+ children: planToDisplay
506
672
  }
507
673
  );
508
674
  };
509
675
 
510
676
  // src/components/SubscriptionGate.tsx
511
- var import_jsx_runtime4 = require("react/jsx-runtime");
677
+ var import_jsx_runtime6 = require("react/jsx-runtime");
512
678
  var SubscriptionGate = ({
513
679
  requirePlan,
514
680
  children
515
681
  }) => {
516
682
  const { subscriptions, loading, hasPlan } = useSubscription();
517
683
  const hasAccess = requirePlan ? hasPlan(requirePlan) : subscriptions.some((sub) => sub.status === "active");
518
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: children({
684
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: children({
519
685
  hasAccess,
520
686
  subscriptions,
521
687
  loading
522
688
  }) });
523
689
  };
690
+
691
+ // src/components/PlanSelector.tsx
692
+ var import_react8 = require("react");
693
+
694
+ // src/hooks/usePlans.ts
695
+ var import_react7 = require("react");
696
+ function usePlans(options) {
697
+ const { fetcher, agentRef, filter, sortBy, autoSelectFirstPaid = false } = options;
698
+ const [plans, setPlans] = (0, import_react7.useState)([]);
699
+ const [loading, setLoading] = (0, import_react7.useState)(true);
700
+ const [error, setError] = (0, import_react7.useState)(null);
701
+ const [selectedPlanIndex, setSelectedPlanIndex] = (0, import_react7.useState)(0);
702
+ const fetchPlans = (0, import_react7.useCallback)(async () => {
703
+ if (!agentRef) {
704
+ setError(new Error("Agent reference not configured"));
705
+ setLoading(false);
706
+ return;
707
+ }
708
+ try {
709
+ setLoading(true);
710
+ setError(null);
711
+ const fetchedPlans = await fetcher(agentRef);
712
+ let processedPlans = filter ? fetchedPlans.filter(filter) : fetchedPlans;
713
+ if (sortBy) {
714
+ processedPlans = [...processedPlans].sort(sortBy);
715
+ }
716
+ setPlans(processedPlans);
717
+ if (autoSelectFirstPaid && processedPlans.length > 0) {
718
+ const firstPaidIndex = processedPlans.findIndex(
719
+ (plan) => plan.price && plan.price > 0
720
+ );
721
+ setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
722
+ }
723
+ } catch (err) {
724
+ setError(err instanceof Error ? err : new Error("Failed to load plans"));
725
+ } finally {
726
+ setLoading(false);
727
+ }
728
+ }, [agentRef, fetcher, filter, sortBy, autoSelectFirstPaid]);
729
+ (0, import_react7.useEffect)(() => {
730
+ fetchPlans();
731
+ }, [fetchPlans]);
732
+ const selectedPlan = (0, import_react7.useMemo)(() => {
733
+ return plans[selectedPlanIndex] || null;
734
+ }, [plans, selectedPlanIndex]);
735
+ const selectPlan = (0, import_react7.useCallback)((planRef) => {
736
+ const index = plans.findIndex((p) => p.reference === planRef);
737
+ if (index >= 0) {
738
+ setSelectedPlanIndex(index);
739
+ }
740
+ }, [plans]);
741
+ return {
742
+ plans,
743
+ loading,
744
+ error,
745
+ selectedPlanIndex,
746
+ selectedPlan,
747
+ setSelectedPlanIndex,
748
+ selectPlan,
749
+ refetch: fetchPlans
750
+ };
751
+ }
752
+
753
+ // src/components/PlanSelector.tsx
754
+ var import_jsx_runtime7 = require("react/jsx-runtime");
755
+ var PlanSelector = ({
756
+ agentRef,
757
+ fetcher,
758
+ filter,
759
+ sortBy,
760
+ autoSelectFirstPaid,
761
+ children
762
+ }) => {
763
+ const { subscriptions } = useSubscription();
764
+ const plansHook = usePlans({
765
+ agentRef,
766
+ fetcher,
767
+ filter,
768
+ sortBy,
769
+ autoSelectFirstPaid
770
+ });
771
+ const { plans } = plansHook;
772
+ const isPaidPlan = (0, import_react8.useCallback)((planName) => {
773
+ const plan = plans.find((p) => p.name === planName);
774
+ return plan ? (plan.price ?? 0) > 0 && !plan.isFreeTier : true;
775
+ }, [plans]);
776
+ const activeSubscription = (0, import_react8.useMemo)(() => {
777
+ const activeSubs = subscriptions.filter((sub) => sub.status === "active");
778
+ return activeSubs.sort(
779
+ (a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
780
+ )[0] || null;
781
+ }, [subscriptions]);
782
+ const isCurrentPlan = (0, import_react8.useCallback)((planName) => {
783
+ return activeSubscription?.planName === planName;
784
+ }, [activeSubscription]);
785
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: children({
786
+ ...plansHook,
787
+ subscriptions,
788
+ isPaidPlan,
789
+ isCurrentPlan
790
+ }) });
791
+ };
792
+
793
+ // src/hooks/useSubscriptionHelpers.ts
794
+ var import_react9 = require("react");
795
+ function useSubscriptionHelpers(plans) {
796
+ const { subscriptions } = useSubscription();
797
+ const isPaidPlan = (0, import_react9.useCallback)((planName) => {
798
+ const plan = plans.find((p) => p.name === planName);
799
+ return plan ? (plan.price ?? 0) > 0 && !plan.isFreeTier : true;
800
+ }, [plans]);
801
+ const subscriptionData = (0, import_react9.useMemo)(() => {
802
+ const activePaidSubscriptions = subscriptions.filter(
803
+ (sub) => sub.status === "active" && isPaidPlan(sub.planName)
804
+ );
805
+ const activePaidSubscription = activePaidSubscriptions.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())[0] || null;
806
+ const cancelledPaidSubscriptions = subscriptions.filter(
807
+ (sub) => sub.status === "cancelled" && isPaidPlan(sub.planName)
808
+ );
809
+ const cancelledSubscription = cancelledPaidSubscriptions.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())[0] || null;
810
+ const hasActiveSubscription = subscriptions.some((sub) => sub.status === "active");
811
+ const shouldShowCancelledNotice = !!(cancelledSubscription && (!hasActiveSubscription || cancelledSubscription.endDate));
812
+ return {
813
+ activePaidSubscription,
814
+ activePlanName: activePaidSubscription?.planName || null,
815
+ cancelledSubscription,
816
+ hasPaidSubscription: activePaidSubscriptions.length > 0,
817
+ shouldShowCancelledNotice
818
+ };
819
+ }, [subscriptions, isPaidPlan]);
820
+ const formatDate = (0, import_react9.useCallback)((dateString) => {
821
+ if (!dateString) return null;
822
+ return new Date(dateString).toLocaleDateString("en-US", {
823
+ year: "numeric",
824
+ month: "long",
825
+ day: "numeric"
826
+ });
827
+ }, []);
828
+ const getDaysUntilExpiration = (0, import_react9.useCallback)((endDate) => {
829
+ if (!endDate) return null;
830
+ const now = /* @__PURE__ */ new Date();
831
+ const expiration = new Date(endDate);
832
+ const diffTime = expiration.getTime() - now.getTime();
833
+ const diffDays = Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
834
+ return diffDays > 0 ? diffDays : 0;
835
+ }, []);
836
+ return {
837
+ isPaidPlan,
838
+ activePaidSubscription: subscriptionData.activePaidSubscription,
839
+ cancelledSubscription: subscriptionData.cancelledSubscription,
840
+ hasPaidSubscription: subscriptionData.hasPaidSubscription,
841
+ shouldShowCancelledNotice: subscriptionData.shouldShowCancelledNotice,
842
+ activePlanName: subscriptionData.activePlanName,
843
+ formatDate,
844
+ getDaysUntilExpiration
845
+ };
846
+ }
524
847
  // Annotate the CommonJS export names for ESM import in node:
525
848
  0 && (module.exports = {
526
849
  PaymentForm,
527
850
  PlanBadge,
851
+ PlanSelector,
528
852
  SolvaPayProvider,
853
+ Spinner,
854
+ StripePaymentFormWrapper,
529
855
  SubscriptionGate,
856
+ filterSubscriptions,
857
+ getActiveSubscriptions,
858
+ getCancelledSubscriptionsWithEndDate,
859
+ getMostRecentSubscription,
860
+ getPrimarySubscription,
861
+ hasActivePaidSubscription,
530
862
  useCheckout,
863
+ usePlans,
531
864
  useSolvaPay,
532
- useSubscription
865
+ useSubscription,
866
+ useSubscriptionHelpers
533
867
  });