@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.cjs +507 -173
- package/dist/index.d.cts +333 -19
- package/dist/index.d.ts +333 -19
- package/dist/index.js +497 -174
- package/package.json +2 -1
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
|
-
|
|
25
|
-
|
|
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
|
|
38
|
-
|
|
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("
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
142
|
+
}, [customerRef, internalCustomerRef]);
|
|
49
143
|
useEffect(() => {
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
207
|
+
refetchSubscription,
|
|
79
208
|
createPayment,
|
|
209
|
+
processPayment,
|
|
80
210
|
customerRef: internalCustomerRef,
|
|
81
211
|
updateCustomerRef
|
|
82
|
-
}), [subscription,
|
|
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 {
|
|
88
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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/
|
|
195
|
-
import {
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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__ */
|
|
288
|
-
|
|
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__ */
|
|
433
|
+
/* @__PURE__ */ jsx3(
|
|
299
434
|
"button",
|
|
300
435
|
{
|
|
301
436
|
type: "submit",
|
|
302
|
-
disabled: !
|
|
437
|
+
disabled: !isReady || !isPaymentElementReady || isProcessing,
|
|
303
438
|
className: buttonClassName,
|
|
304
|
-
"aria-busy": isProcessing,
|
|
305
|
-
"aria-disabled": !
|
|
306
|
-
children: isProcessing ? /* @__PURE__ */
|
|
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
|
|
462
|
+
const checkout = useCheckout(validPlanRef);
|
|
326
463
|
const { refetch } = useSubscription();
|
|
327
|
-
const
|
|
328
|
-
const
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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__ */
|
|
552
|
+
children: /* @__PURE__ */ jsx4("span", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx4(Spinner, { size: "sm" }) })
|
|
418
553
|
}
|
|
419
554
|
)
|
|
420
|
-
] })
|
|
421
|
-
|
|
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 {
|
|
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__ */
|
|
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
|
-
|
|
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":
|
|
623
|
+
"data-has-subscription": !!primarySubscription,
|
|
469
624
|
role: "status",
|
|
470
625
|
"aria-live": "polite",
|
|
471
626
|
"aria-busy": loading,
|
|
472
|
-
"aria-label":
|
|
473
|
-
children:
|
|
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
|
|
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__ */
|
|
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
|
};
|