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