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