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