@solvapay/react 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +954 -294
- package/dist/index.d.cts +316 -113
- package/dist/index.d.ts +316 -113
- package/dist/index.js +946 -293
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -35,13 +35,26 @@ function isPaidPurchase(purchase) {
|
|
|
35
35
|
return (purchase.amount ?? 0) > 0;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// src/
|
|
39
|
-
import { jsx } from "react/jsx-runtime";
|
|
40
|
-
var SolvaPayContext = createContext(null);
|
|
38
|
+
// src/utils/headers.ts
|
|
41
39
|
var CUSTOMER_REF_KEY = "solvapay_customerRef";
|
|
42
40
|
var CUSTOMER_REF_EXPIRY = "solvapay_customerRef_expiry";
|
|
43
41
|
var CUSTOMER_REF_USER_ID_KEY = "solvapay_customerRef_userId";
|
|
44
|
-
|
|
42
|
+
function getAuthAdapter(config) {
|
|
43
|
+
if (config?.auth?.adapter) {
|
|
44
|
+
return config.auth.adapter;
|
|
45
|
+
}
|
|
46
|
+
if (config?.auth?.getToken || config?.auth?.getUserId) {
|
|
47
|
+
return {
|
|
48
|
+
async getToken() {
|
|
49
|
+
return config?.auth?.getToken?.() || null;
|
|
50
|
+
},
|
|
51
|
+
async getUserId() {
|
|
52
|
+
return config?.auth?.getUserId?.() || null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return defaultAuthAdapter;
|
|
57
|
+
}
|
|
45
58
|
function getCachedCustomerRef(userId) {
|
|
46
59
|
if (typeof window === "undefined") return null;
|
|
47
60
|
const cached = localStorage.getItem(CUSTOMER_REF_KEY);
|
|
@@ -62,6 +75,7 @@ function getCachedCustomerRef(userId) {
|
|
|
62
75
|
}
|
|
63
76
|
return cached;
|
|
64
77
|
}
|
|
78
|
+
var CACHE_DURATION = 24 * 60 * 60 * 1e3;
|
|
65
79
|
function setCachedCustomerRef(customerRef, userId) {
|
|
66
80
|
if (typeof window === "undefined") return;
|
|
67
81
|
if (userId === void 0 || userId === null) {
|
|
@@ -77,41 +91,62 @@ function clearCachedCustomerRef() {
|
|
|
77
91
|
localStorage.removeItem(CUSTOMER_REF_EXPIRY);
|
|
78
92
|
localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
|
|
79
93
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
async function buildRequestHeaders(config) {
|
|
95
|
+
const adapter = getAuthAdapter(config);
|
|
96
|
+
const token = await adapter.getToken();
|
|
97
|
+
const userId = await adapter.getUserId();
|
|
98
|
+
const cachedRef = getCachedCustomerRef(userId);
|
|
99
|
+
const headers = {
|
|
100
|
+
"Content-Type": "application/json"
|
|
101
|
+
};
|
|
102
|
+
if (token) {
|
|
103
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
83
104
|
}
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
async getToken() {
|
|
87
|
-
return config?.auth?.getToken?.() || null;
|
|
88
|
-
},
|
|
89
|
-
async getUserId() {
|
|
90
|
-
return config?.auth?.getUserId?.() || null;
|
|
91
|
-
}
|
|
92
|
-
};
|
|
105
|
+
if (cachedRef) {
|
|
106
|
+
headers["x-solvapay-customer-ref"] = cachedRef;
|
|
93
107
|
}
|
|
94
|
-
|
|
108
|
+
if (config?.headers) {
|
|
109
|
+
const custom = typeof config.headers === "function" ? await config.headers() : config.headers;
|
|
110
|
+
Object.assign(headers, custom);
|
|
111
|
+
}
|
|
112
|
+
return { headers, userId };
|
|
95
113
|
}
|
|
114
|
+
|
|
115
|
+
// src/SolvaPayProvider.tsx
|
|
116
|
+
import { jsx } from "react/jsx-runtime";
|
|
117
|
+
var SolvaPayContext = createContext(null);
|
|
96
118
|
var SolvaPayProvider = ({
|
|
97
119
|
config,
|
|
98
120
|
createPayment: customCreatePayment,
|
|
99
121
|
checkPurchase: customCheckPurchase,
|
|
100
122
|
processPayment: customProcessPayment,
|
|
123
|
+
createTopupPayment: customCreateTopupPayment,
|
|
101
124
|
children
|
|
102
125
|
}) => {
|
|
103
126
|
const [purchaseData, setPurchaseData] = useState({
|
|
104
127
|
purchases: []
|
|
105
128
|
});
|
|
106
129
|
const [loading, setLoading] = useState(false);
|
|
130
|
+
const [isRefetching, setIsRefetching] = useState(false);
|
|
131
|
+
const [purchaseError, setPurchaseError] = useState(null);
|
|
107
132
|
const [internalCustomerRef, setInternalCustomerRef] = useState(void 0);
|
|
108
133
|
const [userId, setUserId] = useState(null);
|
|
109
134
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
135
|
+
const [creditsValue, setCreditsValue] = useState(null);
|
|
136
|
+
const [displayCurrencyValue, setDisplayCurrencyValue] = useState(null);
|
|
137
|
+
const [creditsPerMinorUnitValue, setCreditsPerMinorUnitValue] = useState(null);
|
|
138
|
+
const [displayExchangeRateValue, setDisplayExchangeRateValue] = useState(null);
|
|
139
|
+
const [balanceLoading, setBalanceLoading] = useState(false);
|
|
140
|
+
const balanceInFlightRef = useRef(false);
|
|
141
|
+
const balanceLoadedRef = useRef(false);
|
|
142
|
+
const optimisticUntilRef = useRef(0);
|
|
143
|
+
const optimisticTimerRef = useRef(null);
|
|
144
|
+
const fetchBalanceRef = useRef(null);
|
|
110
145
|
const inFlightRef = useRef(null);
|
|
111
|
-
const lastFetchedRef = useRef(null);
|
|
112
146
|
const checkPurchaseRef = useRef(null);
|
|
113
147
|
const createPaymentRef = useRef(null);
|
|
114
148
|
const processPaymentRef = useRef(null);
|
|
149
|
+
const createTopupPaymentRef = useRef(null);
|
|
115
150
|
const configRef = useRef(config);
|
|
116
151
|
const buildDefaultCheckPurchaseRef = useRef(
|
|
117
152
|
null
|
|
@@ -128,31 +163,15 @@ var SolvaPayProvider = ({
|
|
|
128
163
|
useEffect(() => {
|
|
129
164
|
processPaymentRef.current = customProcessPayment || null;
|
|
130
165
|
}, [customProcessPayment]);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
createTopupPaymentRef.current = customCreateTopupPayment || null;
|
|
168
|
+
}, [customCreateTopupPayment]);
|
|
131
169
|
const buildDefaultCheckPurchase = useCallback(async () => {
|
|
132
170
|
const currentConfig = configRef.current;
|
|
133
|
-
const
|
|
134
|
-
const token = await adapter.getToken();
|
|
135
|
-
const detectedUserId = await adapter.getUserId();
|
|
171
|
+
const { headers } = await buildRequestHeaders(currentConfig);
|
|
136
172
|
const route = currentConfig?.api?.checkPurchase || "/api/check-purchase";
|
|
137
173
|
const fetchFn = currentConfig?.fetch || fetch;
|
|
138
|
-
const
|
|
139
|
-
const headers = {
|
|
140
|
-
"Content-Type": "application/json"
|
|
141
|
-
};
|
|
142
|
-
if (token) {
|
|
143
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
144
|
-
}
|
|
145
|
-
if (cachedRef) {
|
|
146
|
-
headers["x-solvapay-customer-ref"] = cachedRef;
|
|
147
|
-
}
|
|
148
|
-
if (currentConfig?.headers) {
|
|
149
|
-
const customHeaders = typeof currentConfig.headers === "function" ? await currentConfig.headers() : currentConfig.headers;
|
|
150
|
-
Object.assign(headers, customHeaders);
|
|
151
|
-
}
|
|
152
|
-
const res = await fetchFn(route, {
|
|
153
|
-
method: "GET",
|
|
154
|
-
headers
|
|
155
|
-
});
|
|
174
|
+
const res = await fetchFn(route, { method: "GET", headers });
|
|
156
175
|
if (!res.ok) {
|
|
157
176
|
const error = new Error(`Failed to check purchase: ${res.statusText}`);
|
|
158
177
|
currentConfig?.onError?.(error, "checkPurchase");
|
|
@@ -166,26 +185,13 @@ var SolvaPayProvider = ({
|
|
|
166
185
|
const buildDefaultCreatePayment = useCallback(
|
|
167
186
|
async (params) => {
|
|
168
187
|
const currentConfig = configRef.current;
|
|
169
|
-
const
|
|
170
|
-
const token = await adapter.getToken();
|
|
171
|
-
const detectedUserId = await adapter.getUserId();
|
|
188
|
+
const { headers } = await buildRequestHeaders(currentConfig);
|
|
172
189
|
const route = currentConfig?.api?.createPayment || "/api/create-payment-intent";
|
|
173
190
|
const fetchFn = currentConfig?.fetch || fetch;
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
};
|
|
178
|
-
if (token) {
|
|
179
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
180
|
-
}
|
|
181
|
-
if (cachedRef) {
|
|
182
|
-
headers["x-solvapay-customer-ref"] = cachedRef;
|
|
191
|
+
const body = {};
|
|
192
|
+
if (params.planRef) {
|
|
193
|
+
body.planRef = params.planRef;
|
|
183
194
|
}
|
|
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
195
|
if (params.productRef) {
|
|
190
196
|
body.productRef = params.productRef;
|
|
191
197
|
}
|
|
@@ -206,25 +212,9 @@ var SolvaPayProvider = ({
|
|
|
206
212
|
const buildDefaultProcessPayment = useCallback(
|
|
207
213
|
async (params) => {
|
|
208
214
|
const currentConfig = configRef.current;
|
|
209
|
-
const
|
|
210
|
-
const token = await adapter.getToken();
|
|
211
|
-
const detectedUserId = await adapter.getUserId();
|
|
215
|
+
const { headers } = await buildRequestHeaders(currentConfig);
|
|
212
216
|
const route = currentConfig?.api?.processPayment || "/api/process-payment";
|
|
213
217
|
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
218
|
const res = await fetchFn(route, {
|
|
229
219
|
method: "POST",
|
|
230
220
|
headers,
|
|
@@ -239,6 +229,86 @@ var SolvaPayProvider = ({
|
|
|
239
229
|
},
|
|
240
230
|
[]
|
|
241
231
|
);
|
|
232
|
+
const buildDefaultCreateTopupPayment = useCallback(
|
|
233
|
+
async (params) => {
|
|
234
|
+
const currentConfig = configRef.current;
|
|
235
|
+
const { headers } = await buildRequestHeaders(currentConfig);
|
|
236
|
+
const route = currentConfig?.api?.createTopupPayment || "/api/create-topup-payment-intent";
|
|
237
|
+
const fetchFn = currentConfig?.fetch || fetch;
|
|
238
|
+
const res = await fetchFn(route, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers,
|
|
241
|
+
body: JSON.stringify({
|
|
242
|
+
amount: params.amount,
|
|
243
|
+
currency: params.currency
|
|
244
|
+
})
|
|
245
|
+
});
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
const error = new Error(`Failed to create topup payment: ${res.statusText}`);
|
|
248
|
+
currentConfig?.onError?.(error, "createTopupPayment");
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
return res.json();
|
|
252
|
+
},
|
|
253
|
+
[]
|
|
254
|
+
);
|
|
255
|
+
const fetchBalanceImpl = useCallback(async () => {
|
|
256
|
+
if (optimisticUntilRef.current > Date.now()) return;
|
|
257
|
+
if (!isAuthenticated && !internalCustomerRef) {
|
|
258
|
+
setCreditsValue(null);
|
|
259
|
+
setDisplayCurrencyValue(null);
|
|
260
|
+
setCreditsPerMinorUnitValue(null);
|
|
261
|
+
setDisplayExchangeRateValue(null);
|
|
262
|
+
setBalanceLoading(false);
|
|
263
|
+
balanceLoadedRef.current = false;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (balanceInFlightRef.current) return;
|
|
267
|
+
balanceInFlightRef.current = true;
|
|
268
|
+
if (!balanceLoadedRef.current) {
|
|
269
|
+
setBalanceLoading(true);
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const currentConfig = configRef.current;
|
|
273
|
+
const { headers } = await buildRequestHeaders(currentConfig);
|
|
274
|
+
const route = currentConfig?.api?.customerBalance || "/api/customer-balance";
|
|
275
|
+
const fetchFn = currentConfig?.fetch || fetch;
|
|
276
|
+
const res = await fetchFn(route, { method: "GET", headers });
|
|
277
|
+
if (!res.ok) {
|
|
278
|
+
console.error("[SolvaPayProvider] Failed to fetch balance:", res.statusText);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const data = await res.json();
|
|
282
|
+
setCreditsValue(data.credits ?? null);
|
|
283
|
+
setDisplayCurrencyValue(data.displayCurrency ?? null);
|
|
284
|
+
setCreditsPerMinorUnitValue(data.creditsPerMinorUnit ?? null);
|
|
285
|
+
setDisplayExchangeRateValue(data.displayExchangeRate ?? null);
|
|
286
|
+
balanceLoadedRef.current = true;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error("[SolvaPayProvider] Failed to fetch balance:", error);
|
|
289
|
+
} finally {
|
|
290
|
+
setBalanceLoading(false);
|
|
291
|
+
balanceInFlightRef.current = false;
|
|
292
|
+
}
|
|
293
|
+
}, [isAuthenticated, internalCustomerRef]);
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
fetchBalanceRef.current = fetchBalanceImpl;
|
|
296
|
+
}, [fetchBalanceImpl]);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
return () => {
|
|
299
|
+
if (optimisticTimerRef.current) clearTimeout(optimisticTimerRef.current);
|
|
300
|
+
};
|
|
301
|
+
}, []);
|
|
302
|
+
const OPTIMISTIC_GRACE_MS = 8e3;
|
|
303
|
+
const adjustBalanceImpl = useCallback((credits) => {
|
|
304
|
+
setCreditsValue((prev) => (prev ?? 0) + credits);
|
|
305
|
+
optimisticUntilRef.current = Date.now() + OPTIMISTIC_GRACE_MS;
|
|
306
|
+
if (optimisticTimerRef.current) clearTimeout(optimisticTimerRef.current);
|
|
307
|
+
optimisticTimerRef.current = setTimeout(() => {
|
|
308
|
+
optimisticUntilRef.current = 0;
|
|
309
|
+
fetchBalanceRef.current?.();
|
|
310
|
+
}, OPTIMISTIC_GRACE_MS);
|
|
311
|
+
}, []);
|
|
242
312
|
const _checkPurchase = useCallback(async () => {
|
|
243
313
|
if (checkPurchaseRef.current) {
|
|
244
314
|
return checkPurchaseRef.current();
|
|
@@ -266,6 +336,15 @@ var SolvaPayProvider = ({
|
|
|
266
336
|
},
|
|
267
337
|
[buildDefaultProcessPayment]
|
|
268
338
|
);
|
|
339
|
+
const createTopupPayment = useCallback(
|
|
340
|
+
async (params) => {
|
|
341
|
+
if (createTopupPaymentRef.current) {
|
|
342
|
+
return createTopupPaymentRef.current(params);
|
|
343
|
+
}
|
|
344
|
+
return buildDefaultCreateTopupPayment(params);
|
|
345
|
+
},
|
|
346
|
+
[buildDefaultCreateTopupPayment]
|
|
347
|
+
);
|
|
269
348
|
useEffect(() => {
|
|
270
349
|
const detectAuth = async () => {
|
|
271
350
|
const currentConfig = configRef.current;
|
|
@@ -291,27 +370,30 @@ var SolvaPayProvider = ({
|
|
|
291
370
|
}
|
|
292
371
|
};
|
|
293
372
|
detectAuth();
|
|
294
|
-
const interval = setInterval(detectAuth,
|
|
373
|
+
const interval = setInterval(detectAuth, 3e4);
|
|
295
374
|
return () => clearInterval(interval);
|
|
296
375
|
}, [userId]);
|
|
297
376
|
const fetchPurchase = useCallback(
|
|
298
377
|
async (force = false) => {
|
|
299
378
|
if (!isAuthenticated && !internalCustomerRef) {
|
|
300
379
|
setPurchaseData({ purchases: [] });
|
|
380
|
+
setPurchaseError(null);
|
|
301
381
|
setLoading(false);
|
|
382
|
+
setIsRefetching(false);
|
|
302
383
|
inFlightRef.current = null;
|
|
303
|
-
lastFetchedRef.current = null;
|
|
304
384
|
return;
|
|
305
385
|
}
|
|
306
386
|
const cacheKey = internalCustomerRef || userId || "anonymous";
|
|
307
|
-
if (
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
if (inFlightRef.current === cacheKey) {
|
|
387
|
+
if (inFlightRef.current === cacheKey && !force) {
|
|
311
388
|
return;
|
|
312
389
|
}
|
|
313
390
|
inFlightRef.current = cacheKey;
|
|
314
|
-
|
|
391
|
+
const hasExistingData = purchaseData.purchases.length > 0;
|
|
392
|
+
if (hasExistingData) {
|
|
393
|
+
setIsRefetching(true);
|
|
394
|
+
} else {
|
|
395
|
+
setLoading(true);
|
|
396
|
+
}
|
|
315
397
|
try {
|
|
316
398
|
const checkFn = checkPurchaseRef.current || buildDefaultCheckPurchaseRef.current;
|
|
317
399
|
if (!checkFn) {
|
|
@@ -330,43 +412,111 @@ var SolvaPayProvider = ({
|
|
|
330
412
|
purchases: filterPurchases(data.purchases || [])
|
|
331
413
|
};
|
|
332
414
|
setPurchaseData(filteredData);
|
|
333
|
-
|
|
415
|
+
setPurchaseError(null);
|
|
334
416
|
}
|
|
335
|
-
} catch (
|
|
336
|
-
console.error("[SolvaPayProvider] Failed to fetch purchase:",
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.error("[SolvaPayProvider] Failed to fetch purchase:", err);
|
|
337
419
|
if (inFlightRef.current === cacheKey) {
|
|
338
|
-
|
|
339
|
-
purchases: []
|
|
340
|
-
});
|
|
341
|
-
lastFetchedRef.current = cacheKey;
|
|
420
|
+
setPurchaseError(err instanceof Error ? err : new Error(String(err)));
|
|
342
421
|
}
|
|
343
422
|
} finally {
|
|
344
423
|
if (inFlightRef.current === cacheKey) {
|
|
345
424
|
setLoading(false);
|
|
425
|
+
setIsRefetching(false);
|
|
346
426
|
inFlightRef.current = null;
|
|
347
427
|
}
|
|
348
428
|
}
|
|
349
429
|
},
|
|
350
|
-
[isAuthenticated, internalCustomerRef, userId]
|
|
430
|
+
[isAuthenticated, internalCustomerRef, userId, purchaseData.purchases.length]
|
|
351
431
|
);
|
|
352
432
|
const fetchPurchaseRef = useRef(fetchPurchase);
|
|
353
433
|
useEffect(() => {
|
|
354
434
|
fetchPurchaseRef.current = fetchPurchase;
|
|
355
435
|
}, [fetchPurchase]);
|
|
356
436
|
const refetchPurchase = useCallback(async () => {
|
|
357
|
-
lastFetchedRef.current = null;
|
|
358
437
|
inFlightRef.current = null;
|
|
359
|
-
setPurchaseData({ purchases: [] });
|
|
360
438
|
await fetchPurchaseRef.current(true);
|
|
361
439
|
}, []);
|
|
440
|
+
const cancelRenewal = useCallback(
|
|
441
|
+
async (params) => {
|
|
442
|
+
const currentConfig = configRef.current;
|
|
443
|
+
const { headers } = await buildRequestHeaders(currentConfig);
|
|
444
|
+
const route = currentConfig?.api?.cancelRenewal || "/api/cancel-renewal";
|
|
445
|
+
const fetchFn = currentConfig?.fetch || fetch;
|
|
446
|
+
const res = await fetchFn(route, {
|
|
447
|
+
method: "POST",
|
|
448
|
+
headers,
|
|
449
|
+
body: JSON.stringify(params)
|
|
450
|
+
});
|
|
451
|
+
if (!res.ok) {
|
|
452
|
+
const data = await res.json().catch(() => ({}));
|
|
453
|
+
const error = new Error(data.error || `Failed to cancel renewal: ${res.statusText}`);
|
|
454
|
+
currentConfig?.onError?.(error, "cancelRenewal");
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
const result = await res.json();
|
|
458
|
+
await refetchPurchase();
|
|
459
|
+
return result;
|
|
460
|
+
},
|
|
461
|
+
[refetchPurchase]
|
|
462
|
+
);
|
|
463
|
+
const reactivateRenewal = useCallback(
|
|
464
|
+
async (params) => {
|
|
465
|
+
const currentConfig = configRef.current;
|
|
466
|
+
const { headers } = await buildRequestHeaders(currentConfig);
|
|
467
|
+
const route = currentConfig?.api?.reactivateRenewal || "/api/reactivate-renewal";
|
|
468
|
+
const fetchFn = currentConfig?.fetch || fetch;
|
|
469
|
+
const res = await fetchFn(route, {
|
|
470
|
+
method: "POST",
|
|
471
|
+
headers,
|
|
472
|
+
body: JSON.stringify(params)
|
|
473
|
+
});
|
|
474
|
+
if (!res.ok) {
|
|
475
|
+
const data = await res.json().catch(() => ({}));
|
|
476
|
+
const error = new Error(data.error || `Failed to reactivate renewal: ${res.statusText}`);
|
|
477
|
+
currentConfig?.onError?.(error, "reactivateRenewal");
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
const result = await res.json();
|
|
481
|
+
await refetchPurchase();
|
|
482
|
+
return result;
|
|
483
|
+
},
|
|
484
|
+
[refetchPurchase]
|
|
485
|
+
);
|
|
486
|
+
const activatePlan = useCallback(
|
|
487
|
+
async (params) => {
|
|
488
|
+
const currentConfig = configRef.current;
|
|
489
|
+
const { headers } = await buildRequestHeaders(currentConfig);
|
|
490
|
+
const route = currentConfig?.api?.activatePlan || "/api/activate-plan";
|
|
491
|
+
const fetchFn = currentConfig?.fetch || fetch;
|
|
492
|
+
const res = await fetchFn(route, {
|
|
493
|
+
method: "POST",
|
|
494
|
+
headers,
|
|
495
|
+
body: JSON.stringify(params)
|
|
496
|
+
});
|
|
497
|
+
if (!res.ok) {
|
|
498
|
+
const data = await res.json().catch(() => ({}));
|
|
499
|
+
const error = new Error(data.error || `Failed to activate plan: ${res.statusText}`);
|
|
500
|
+
currentConfig?.onError?.(error, "activatePlan");
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
const result = await res.json();
|
|
504
|
+
if (result.status === "activated" || result.status === "already_active") {
|
|
505
|
+
await refetchPurchase();
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
},
|
|
509
|
+
[refetchPurchase]
|
|
510
|
+
);
|
|
362
511
|
useEffect(() => {
|
|
363
|
-
lastFetchedRef.current = null;
|
|
364
512
|
inFlightRef.current = null;
|
|
365
513
|
if (isAuthenticated || internalCustomerRef) {
|
|
366
514
|
fetchPurchase();
|
|
367
515
|
} else {
|
|
368
516
|
setPurchaseData({ purchases: [] });
|
|
517
|
+
setPurchaseError(null);
|
|
369
518
|
setLoading(false);
|
|
519
|
+
setIsRefetching(false);
|
|
370
520
|
}
|
|
371
521
|
}, [isAuthenticated, internalCustomerRef, userId]);
|
|
372
522
|
const updateCustomerRef = useCallback(
|
|
@@ -387,6 +537,8 @@ var SolvaPayProvider = ({
|
|
|
387
537
|
)[0] || null;
|
|
388
538
|
return {
|
|
389
539
|
loading,
|
|
540
|
+
isRefetching,
|
|
541
|
+
error: purchaseError,
|
|
390
542
|
customerRef: purchaseData.customerRef || internalCustomerRef,
|
|
391
543
|
email: purchaseData.email,
|
|
392
544
|
name: purchaseData.name,
|
|
@@ -405,24 +557,47 @@ var SolvaPayProvider = ({
|
|
|
405
557
|
hasPaidPurchase: activePaidPurchases.length > 0,
|
|
406
558
|
activePaidPurchase
|
|
407
559
|
};
|
|
408
|
-
}, [loading, purchaseData, internalCustomerRef]);
|
|
560
|
+
}, [loading, isRefetching, purchaseError, purchaseData, internalCustomerRef]);
|
|
561
|
+
const balance = useMemo(
|
|
562
|
+
() => ({
|
|
563
|
+
loading: balanceLoading,
|
|
564
|
+
credits: creditsValue,
|
|
565
|
+
displayCurrency: displayCurrencyValue,
|
|
566
|
+
creditsPerMinorUnit: creditsPerMinorUnitValue,
|
|
567
|
+
displayExchangeRate: displayExchangeRateValue,
|
|
568
|
+
refetch: fetchBalanceImpl,
|
|
569
|
+
adjustBalance: adjustBalanceImpl
|
|
570
|
+
}),
|
|
571
|
+
[balanceLoading, creditsValue, displayCurrencyValue, creditsPerMinorUnitValue, displayExchangeRateValue, fetchBalanceImpl, adjustBalanceImpl]
|
|
572
|
+
);
|
|
409
573
|
const contextValue = useMemo(
|
|
410
574
|
() => ({
|
|
411
575
|
purchase,
|
|
412
576
|
refetchPurchase,
|
|
413
577
|
createPayment,
|
|
414
578
|
processPayment,
|
|
579
|
+
createTopupPayment,
|
|
580
|
+
cancelRenewal,
|
|
581
|
+
reactivateRenewal,
|
|
582
|
+
activatePlan,
|
|
415
583
|
customerRef: purchaseData.customerRef || internalCustomerRef,
|
|
416
|
-
updateCustomerRef
|
|
584
|
+
updateCustomerRef,
|
|
585
|
+
balance,
|
|
586
|
+
_config: configRef.current
|
|
417
587
|
}),
|
|
418
588
|
[
|
|
419
589
|
purchase,
|
|
420
590
|
refetchPurchase,
|
|
421
591
|
createPayment,
|
|
422
592
|
processPayment,
|
|
593
|
+
createTopupPayment,
|
|
594
|
+
cancelRenewal,
|
|
595
|
+
reactivateRenewal,
|
|
596
|
+
activatePlan,
|
|
423
597
|
purchaseData.customerRef,
|
|
424
598
|
internalCustomerRef,
|
|
425
|
-
updateCustomerRef
|
|
599
|
+
updateCustomerRef,
|
|
600
|
+
balance
|
|
426
601
|
]
|
|
427
602
|
);
|
|
428
603
|
return /* @__PURE__ */ jsx(SolvaPayContext.Provider, { value: contextValue, children });
|
|
@@ -453,29 +628,66 @@ var stripePromiseCache = /* @__PURE__ */ new Map();
|
|
|
453
628
|
function getStripeCacheKey(publishableKey, accountId) {
|
|
454
629
|
return accountId ? `${publishableKey}:${accountId}` : publishableKey;
|
|
455
630
|
}
|
|
631
|
+
async function resolvePlanRef(productRef, fetchFn, headers, listPlansRoute) {
|
|
632
|
+
const url = `${listPlansRoute}?productRef=${encodeURIComponent(productRef)}`;
|
|
633
|
+
const res = await fetchFn(url, { method: "GET", headers });
|
|
634
|
+
if (!res.ok) {
|
|
635
|
+
throw new Error(`Failed to fetch plans for product "${productRef}": ${res.statusText}`);
|
|
636
|
+
}
|
|
637
|
+
const data = await res.json();
|
|
638
|
+
const allPlans = data.plans ?? [];
|
|
639
|
+
const activePlans = allPlans.filter((p) => p.isActive !== false && p.status !== "inactive");
|
|
640
|
+
if (activePlans.length === 0) {
|
|
641
|
+
throw new Error(
|
|
642
|
+
`No active plans found for product "${productRef}". Configure at least one plan in the SolvaPay Console.`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
if (activePlans.length === 1) {
|
|
646
|
+
return activePlans[0].reference;
|
|
647
|
+
}
|
|
648
|
+
const defaultPlan = activePlans.find((p) => p.default === true);
|
|
649
|
+
if (defaultPlan) {
|
|
650
|
+
return defaultPlan.reference;
|
|
651
|
+
}
|
|
652
|
+
throw new Error(
|
|
653
|
+
`Product "${productRef}" has ${activePlans.length} active plans but none is marked as default. Either pass planRef explicitly, use <PricingSelector> for user selection, or mark one plan as default in the SolvaPay Console.`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
456
656
|
function useCheckout(options) {
|
|
457
657
|
const { planRef, productRef } = options;
|
|
458
|
-
const { createPayment, customerRef, updateCustomerRef } = useSolvaPay();
|
|
658
|
+
const { createPayment, customerRef, updateCustomerRef, _config } = useSolvaPay();
|
|
459
659
|
const [loading, setLoading] = useState2(false);
|
|
460
|
-
const [error, setError] = useState2(
|
|
461
|
-
!planRef || typeof planRef !== "string" ? new Error("useCheckout: planRef parameter is required and must be a string") : null
|
|
462
|
-
);
|
|
660
|
+
const [error, setError] = useState2(null);
|
|
463
661
|
const [stripePromise, setStripePromise] = useState2(null);
|
|
464
662
|
const [clientSecret, setClientSecret] = useState2(null);
|
|
663
|
+
const [resolvedPlanRef, setResolvedPlanRef] = useState2(planRef || null);
|
|
465
664
|
const isStartingRef = useRef2(false);
|
|
466
665
|
const startCheckout = useCallback2(async () => {
|
|
467
666
|
if (isStartingRef.current || loading) {
|
|
468
667
|
return;
|
|
469
668
|
}
|
|
470
|
-
if (!planRef
|
|
471
|
-
setError(
|
|
669
|
+
if (!planRef && !productRef) {
|
|
670
|
+
setError(
|
|
671
|
+
new Error(
|
|
672
|
+
"useCheckout: either planRef or productRef is required. Pass planRef directly, or pass productRef to auto-resolve the plan."
|
|
673
|
+
)
|
|
674
|
+
);
|
|
472
675
|
return;
|
|
473
676
|
}
|
|
474
677
|
isStartingRef.current = true;
|
|
475
678
|
setLoading(true);
|
|
476
679
|
setError(null);
|
|
477
680
|
try {
|
|
478
|
-
|
|
681
|
+
let effectivePlanRef = planRef;
|
|
682
|
+
if (!effectivePlanRef && productRef) {
|
|
683
|
+
const listPlansRoute = _config?.api?.listPlans || "/api/list-plans";
|
|
684
|
+
effectivePlanRef = await resolvePlanRef(productRef, fetch, {}, listPlansRoute);
|
|
685
|
+
setResolvedPlanRef(effectivePlanRef);
|
|
686
|
+
}
|
|
687
|
+
if (!effectivePlanRef) {
|
|
688
|
+
throw new Error("Could not determine plan reference for checkout");
|
|
689
|
+
}
|
|
690
|
+
const result = await createPayment({ planRef: effectivePlanRef, productRef });
|
|
479
691
|
if (!result || typeof result !== "object") {
|
|
480
692
|
throw new Error("Invalid payment intent response from server");
|
|
481
693
|
}
|
|
@@ -504,19 +716,21 @@ function useCheckout(options) {
|
|
|
504
716
|
setLoading(false);
|
|
505
717
|
isStartingRef.current = false;
|
|
506
718
|
}
|
|
507
|
-
}, [planRef, productRef, createPayment, updateCustomerRef, loading]);
|
|
719
|
+
}, [planRef, productRef, createPayment, updateCustomerRef, loading, _config]);
|
|
508
720
|
const reset = useCallback2(() => {
|
|
509
721
|
isStartingRef.current = false;
|
|
510
722
|
setLoading(false);
|
|
511
723
|
setError(null);
|
|
512
724
|
setStripePromise(null);
|
|
513
725
|
setClientSecret(null);
|
|
514
|
-
|
|
726
|
+
setResolvedPlanRef(planRef || null);
|
|
727
|
+
}, [planRef]);
|
|
515
728
|
return {
|
|
516
729
|
loading,
|
|
517
730
|
error,
|
|
518
731
|
stripePromise,
|
|
519
732
|
clientSecret,
|
|
733
|
+
resolvedPlanRef,
|
|
520
734
|
startCheckout,
|
|
521
735
|
reset
|
|
522
736
|
};
|
|
@@ -684,14 +898,16 @@ var StripePaymentFormWrapper = ({
|
|
|
684
898
|
const cardElementOptions = {
|
|
685
899
|
style: {
|
|
686
900
|
base: {
|
|
687
|
-
fontSize: "
|
|
688
|
-
|
|
901
|
+
fontSize: "17px",
|
|
902
|
+
lineHeight: "24px",
|
|
903
|
+
color: "#1e293b",
|
|
904
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
689
905
|
"::placeholder": {
|
|
690
|
-
color: "#
|
|
906
|
+
color: "#94a3b8"
|
|
691
907
|
}
|
|
692
908
|
},
|
|
693
909
|
invalid: {
|
|
694
|
-
color: "#
|
|
910
|
+
color: "#dc2626"
|
|
695
911
|
}
|
|
696
912
|
},
|
|
697
913
|
hidePostalCode: true
|
|
@@ -711,22 +927,7 @@ var StripePaymentFormWrapper = ({
|
|
|
711
927
|
}
|
|
712
928
|
}
|
|
713
929
|
}
|
|
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
|
-
),
|
|
930
|
+
) : /* @__PURE__ */ jsx3("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "52px" }, children: /* @__PURE__ */ jsx3(Spinner, { size: "sm" }) }),
|
|
730
931
|
message && !isSuccess && /* @__PURE__ */ jsx3("div", { role: "alert", "aria-live": "assertive", "aria-atomic": "true", children: message }),
|
|
731
932
|
/* @__PURE__ */ jsx3(
|
|
732
933
|
"button",
|
|
@@ -758,88 +959,62 @@ var PaymentForm = ({
|
|
|
758
959
|
className,
|
|
759
960
|
buttonClassName
|
|
760
961
|
}) => {
|
|
761
|
-
const validPlanRef = planRef && typeof planRef === "string" ? planRef : "";
|
|
762
962
|
const {
|
|
763
963
|
loading: checkoutLoading,
|
|
764
964
|
error: checkoutError,
|
|
765
965
|
clientSecret,
|
|
766
966
|
startCheckout,
|
|
767
|
-
stripePromise
|
|
768
|
-
|
|
967
|
+
stripePromise,
|
|
968
|
+
resolvedPlanRef
|
|
969
|
+
} = useCheckout({ planRef, productRef });
|
|
769
970
|
const { refetch } = usePurchase();
|
|
770
971
|
const { processPayment } = useSolvaPay();
|
|
771
972
|
const hasInitializedRef = useRef3(false);
|
|
973
|
+
const hasPlanOrProduct = !!(planRef || productRef);
|
|
772
974
|
useEffect2(() => {
|
|
773
|
-
if (!hasInitializedRef.current &&
|
|
975
|
+
if (!hasInitializedRef.current && hasPlanOrProduct && !checkoutLoading && !checkoutError && !clientSecret) {
|
|
774
976
|
hasInitializedRef.current = true;
|
|
775
977
|
startCheckout().catch(() => {
|
|
776
978
|
hasInitializedRef.current = false;
|
|
777
979
|
});
|
|
778
980
|
}
|
|
779
|
-
if (
|
|
981
|
+
if (hasPlanOrProduct && clientSecret) {
|
|
780
982
|
hasInitializedRef.current = true;
|
|
781
983
|
}
|
|
782
|
-
}, [
|
|
984
|
+
}, [hasPlanOrProduct, checkoutLoading, checkoutError, clientSecret, startCheckout]);
|
|
985
|
+
const effectivePlanRef = planRef || resolvedPlanRef;
|
|
783
986
|
const handleSuccess = useCallback3(
|
|
784
987
|
async (paymentIntent) => {
|
|
785
|
-
let processingTimeout = false;
|
|
786
|
-
let processingResult = null;
|
|
787
988
|
const paymentIntentAny = paymentIntent;
|
|
788
989
|
if (processPayment && productRef) {
|
|
789
990
|
try {
|
|
790
991
|
const result = await processPayment({
|
|
791
992
|
paymentIntentId: paymentIntentAny.id,
|
|
792
993
|
productRef,
|
|
793
|
-
planRef
|
|
994
|
+
planRef: effectivePlanRef || void 0
|
|
794
995
|
});
|
|
795
|
-
processingResult = result;
|
|
796
996
|
const isTimeout = result?.status === "timeout";
|
|
797
|
-
processingTimeout = isTimeout;
|
|
798
997
|
if (isTimeout) {
|
|
799
998
|
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
800
|
-
|
|
801
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
999
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 1e3));
|
|
802
1000
|
await refetch();
|
|
803
1001
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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();
|
|
1002
|
+
const err = new Error("Payment processing timed out \u2014 webhooks may not be configured");
|
|
1003
|
+
onError?.(err);
|
|
1004
|
+
return;
|
|
815
1005
|
}
|
|
1006
|
+
await refetch();
|
|
816
1007
|
} catch (error) {
|
|
817
1008
|
console.error("[PaymentForm] Failed to process payment:", error);
|
|
818
|
-
|
|
819
|
-
|
|
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;
|
|
1009
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1010
|
+
return;
|
|
829
1011
|
}
|
|
830
1012
|
} else {
|
|
831
1013
|
await refetch();
|
|
832
1014
|
}
|
|
833
|
-
|
|
834
|
-
await onSuccess({
|
|
835
|
-
...paymentIntentAny,
|
|
836
|
-
_processingTimeout: processingTimeout,
|
|
837
|
-
_processingResult: processingResult
|
|
838
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
839
|
-
});
|
|
840
|
-
}
|
|
1015
|
+
onSuccess?.(paymentIntentAny);
|
|
841
1016
|
},
|
|
842
|
-
[processPayment, productRef,
|
|
1017
|
+
[processPayment, productRef, effectivePlanRef, refetch, onSuccess, onError]
|
|
843
1018
|
);
|
|
844
1019
|
const handleError = useCallback3(
|
|
845
1020
|
(err) => {
|
|
@@ -850,7 +1025,6 @@ var PaymentForm = ({
|
|
|
850
1025
|
[onError]
|
|
851
1026
|
);
|
|
852
1027
|
const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
|
|
853
|
-
const isValidPlanRef = planRef && typeof planRef === "string";
|
|
854
1028
|
const hasError = !!checkoutError;
|
|
855
1029
|
const hasStripeData = !!(stripePromise && clientSecret);
|
|
856
1030
|
const elementsOptions = useMemo2(() => {
|
|
@@ -864,72 +1038,246 @@ var PaymentForm = ({
|
|
|
864
1038
|
}
|
|
865
1039
|
}, [hasStripeData]);
|
|
866
1040
|
const shouldRenderElements = hasStripeData || hasMountedElements && stripePromise && clientSecret;
|
|
867
|
-
return /* @__PURE__ */ jsx4("div", { className, children: !
|
|
1041
|
+
return /* @__PURE__ */ jsx4("div", { className, children: !hasPlanOrProduct ? /* @__PURE__ */ jsx4("div", { children: "PaymentForm: either planRef or productRef is required" }) : hasError ? /* @__PURE__ */ jsxs3("div", { children: [
|
|
868
1042
|
/* @__PURE__ */ jsx4("div", { children: "Payment initialization failed" }),
|
|
869
1043
|
/* @__PURE__ */ jsx4("div", { children: checkoutError?.message || "Unknown error" })
|
|
870
|
-
] }) : shouldRenderElements && elementsOptions ? (
|
|
871
|
-
|
|
872
|
-
|
|
1044
|
+
] }) : shouldRenderElements && elementsOptions ? /* @__PURE__ */ jsx4(
|
|
1045
|
+
Elements,
|
|
1046
|
+
{
|
|
1047
|
+
stripe: stripePromise,
|
|
1048
|
+
options: elementsOptions,
|
|
1049
|
+
children: /* @__PURE__ */ jsx4(
|
|
1050
|
+
StripePaymentFormWrapper,
|
|
1051
|
+
{
|
|
1052
|
+
onSuccess: handleSuccess,
|
|
1053
|
+
onError: handleError,
|
|
1054
|
+
returnUrl: finalReturnUrl,
|
|
1055
|
+
submitButtonText,
|
|
1056
|
+
buttonClassName,
|
|
1057
|
+
clientSecret
|
|
1058
|
+
}
|
|
1059
|
+
)
|
|
1060
|
+
},
|
|
1061
|
+
clientSecret
|
|
1062
|
+
) : /* @__PURE__ */ jsxs3("div", { children: [
|
|
873
1063
|
/* @__PURE__ */ jsx4(
|
|
874
|
-
|
|
1064
|
+
"div",
|
|
875
1065
|
{
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1066
|
+
style: {
|
|
1067
|
+
minHeight: "40px",
|
|
1068
|
+
display: "flex",
|
|
1069
|
+
alignItems: "center",
|
|
1070
|
+
justifyContent: "center"
|
|
1071
|
+
},
|
|
1072
|
+
children: /* @__PURE__ */ jsx4(Spinner, { size: "md" })
|
|
1073
|
+
}
|
|
1074
|
+
),
|
|
1075
|
+
/* @__PURE__ */ jsx4(
|
|
1076
|
+
"button",
|
|
1077
|
+
{
|
|
1078
|
+
type: "submit",
|
|
1079
|
+
disabled: true,
|
|
1080
|
+
className: buttonClassName,
|
|
1081
|
+
"aria-busy": "false",
|
|
1082
|
+
"aria-disabled": "true",
|
|
1083
|
+
children: submitButtonText
|
|
1084
|
+
}
|
|
891
1085
|
)
|
|
892
|
-
)
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1086
|
+
] }) });
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
// src/TopupForm.tsx
|
|
1090
|
+
import { useEffect as useEffect3, useCallback as useCallback5, useRef as useRef5, useMemo as useMemo3 } from "react";
|
|
1091
|
+
import { Elements as Elements2 } from "@stripe/react-stripe-js";
|
|
1092
|
+
|
|
1093
|
+
// src/hooks/useTopup.ts
|
|
1094
|
+
import { useState as useState5, useCallback as useCallback4, useRef as useRef4 } from "react";
|
|
1095
|
+
import { loadStripe as loadStripe2 } from "@stripe/stripe-js";
|
|
1096
|
+
var stripePromiseCache2 = /* @__PURE__ */ new Map();
|
|
1097
|
+
function getStripeCacheKey2(publishableKey, accountId) {
|
|
1098
|
+
return accountId ? `${publishableKey}:${accountId}` : publishableKey;
|
|
1099
|
+
}
|
|
1100
|
+
function useTopup(options) {
|
|
1101
|
+
const { amount, currency } = options;
|
|
1102
|
+
const { createTopupPayment, customerRef, updateCustomerRef } = useSolvaPay();
|
|
1103
|
+
const [loading, setLoading] = useState5(false);
|
|
1104
|
+
const [error, setError] = useState5(null);
|
|
1105
|
+
const [stripePromise, setStripePromise] = useState5(null);
|
|
1106
|
+
const [clientSecret, setClientSecret] = useState5(null);
|
|
1107
|
+
const isStartingRef = useRef4(false);
|
|
1108
|
+
const startTopup = useCallback4(async () => {
|
|
1109
|
+
if (isStartingRef.current || loading) {
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (!amount || amount <= 0) {
|
|
1113
|
+
setError(new Error("useTopup: amount must be a positive number"));
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
isStartingRef.current = true;
|
|
1117
|
+
setLoading(true);
|
|
1118
|
+
setError(null);
|
|
1119
|
+
try {
|
|
1120
|
+
const result = await createTopupPayment({ amount, currency });
|
|
1121
|
+
if (!result || typeof result !== "object") {
|
|
1122
|
+
throw new Error("Invalid topup payment intent response from server");
|
|
1123
|
+
}
|
|
1124
|
+
if (!result.clientSecret || typeof result.clientSecret !== "string") {
|
|
1125
|
+
throw new Error("Invalid client secret in topup payment intent response");
|
|
1126
|
+
}
|
|
1127
|
+
if (!result.publishableKey || typeof result.publishableKey !== "string") {
|
|
1128
|
+
throw new Error("Invalid publishable key in topup payment intent response");
|
|
1129
|
+
}
|
|
1130
|
+
if (result.customerRef && result.customerRef !== customerRef && updateCustomerRef) {
|
|
1131
|
+
updateCustomerRef(result.customerRef);
|
|
1132
|
+
}
|
|
1133
|
+
const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
|
|
1134
|
+
const cacheKey = getStripeCacheKey2(result.publishableKey, result.accountId);
|
|
1135
|
+
let stripe = stripePromiseCache2.get(cacheKey);
|
|
1136
|
+
if (!stripe) {
|
|
1137
|
+
stripe = loadStripe2(result.publishableKey, stripeOptions);
|
|
1138
|
+
stripePromiseCache2.set(cacheKey, stripe);
|
|
1139
|
+
}
|
|
1140
|
+
setStripePromise(stripe);
|
|
1141
|
+
setClientSecret(result.clientSecret);
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
const error2 = err instanceof Error ? err : new Error("Failed to start topup");
|
|
1144
|
+
setError(error2);
|
|
1145
|
+
} finally {
|
|
1146
|
+
setLoading(false);
|
|
1147
|
+
isStartingRef.current = false;
|
|
1148
|
+
}
|
|
1149
|
+
}, [amount, currency, createTopupPayment, customerRef, updateCustomerRef, loading]);
|
|
1150
|
+
const reset = useCallback4(() => {
|
|
1151
|
+
isStartingRef.current = false;
|
|
1152
|
+
setLoading(false);
|
|
1153
|
+
setError(null);
|
|
1154
|
+
setStripePromise(null);
|
|
1155
|
+
setClientSecret(null);
|
|
1156
|
+
}, []);
|
|
1157
|
+
return {
|
|
1158
|
+
loading,
|
|
1159
|
+
error,
|
|
1160
|
+
stripePromise,
|
|
1161
|
+
clientSecret,
|
|
1162
|
+
startTopup,
|
|
1163
|
+
reset
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/TopupForm.tsx
|
|
1168
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1169
|
+
var TopupForm = ({
|
|
1170
|
+
amount,
|
|
1171
|
+
currency,
|
|
1172
|
+
onSuccess,
|
|
1173
|
+
onError,
|
|
1174
|
+
returnUrl,
|
|
1175
|
+
submitButtonText = "Top Up",
|
|
1176
|
+
className,
|
|
1177
|
+
buttonClassName
|
|
1178
|
+
}) => {
|
|
1179
|
+
const {
|
|
1180
|
+
loading: topupLoading,
|
|
1181
|
+
error: topupError,
|
|
1182
|
+
clientSecret,
|
|
1183
|
+
startTopup,
|
|
1184
|
+
stripePromise
|
|
1185
|
+
} = useTopup({ amount, currency });
|
|
1186
|
+
const hasInitializedRef = useRef5(false);
|
|
1187
|
+
const hasAmount = amount > 0;
|
|
1188
|
+
useEffect3(() => {
|
|
1189
|
+
if (!hasInitializedRef.current && hasAmount && !topupLoading && !topupError && !clientSecret) {
|
|
1190
|
+
hasInitializedRef.current = true;
|
|
1191
|
+
startTopup().catch(() => {
|
|
1192
|
+
hasInitializedRef.current = false;
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
if (hasAmount && clientSecret) {
|
|
1196
|
+
hasInitializedRef.current = true;
|
|
1197
|
+
}
|
|
1198
|
+
}, [hasAmount, topupLoading, topupError, clientSecret, startTopup]);
|
|
1199
|
+
const handleSuccess = useCallback5(
|
|
1200
|
+
async (paymentIntent) => {
|
|
1201
|
+
if (onSuccess) {
|
|
1202
|
+
await onSuccess(paymentIntent);
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
[onSuccess]
|
|
1206
|
+
);
|
|
1207
|
+
const handleError = useCallback5(
|
|
1208
|
+
(err) => {
|
|
1209
|
+
if (onError) {
|
|
1210
|
+
onError(err);
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
[onError]
|
|
1214
|
+
);
|
|
1215
|
+
const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
|
|
1216
|
+
const hasError = !!topupError;
|
|
1217
|
+
const hasStripeData = !!(stripePromise && clientSecret);
|
|
1218
|
+
const elementsOptions = useMemo3(() => {
|
|
1219
|
+
if (!clientSecret) return void 0;
|
|
1220
|
+
return { clientSecret };
|
|
1221
|
+
}, [clientSecret]);
|
|
1222
|
+
return /* @__PURE__ */ jsx5("div", { className, children: !hasAmount ? /* @__PURE__ */ jsx5("div", { children: "TopupForm: amount must be a positive number" }) : hasError ? /* @__PURE__ */ jsxs4("div", { children: [
|
|
1223
|
+
/* @__PURE__ */ jsx5("div", { children: "Top-up initialization failed" }),
|
|
1224
|
+
/* @__PURE__ */ jsx5("div", { children: topupError?.message || "Unknown error" })
|
|
1225
|
+
] }) : hasStripeData && elementsOptions ? /* @__PURE__ */ jsx5(
|
|
1226
|
+
Elements2,
|
|
1227
|
+
{
|
|
1228
|
+
stripe: stripePromise,
|
|
1229
|
+
options: elementsOptions,
|
|
1230
|
+
children: /* @__PURE__ */ jsx5(
|
|
1231
|
+
StripePaymentFormWrapper,
|
|
909
1232
|
{
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1233
|
+
onSuccess: handleSuccess,
|
|
1234
|
+
onError: handleError,
|
|
1235
|
+
returnUrl: finalReturnUrl,
|
|
1236
|
+
submitButtonText,
|
|
1237
|
+
buttonClassName,
|
|
1238
|
+
clientSecret
|
|
916
1239
|
}
|
|
917
1240
|
)
|
|
918
|
-
|
|
919
|
-
|
|
1241
|
+
},
|
|
1242
|
+
clientSecret
|
|
1243
|
+
) : /* @__PURE__ */ jsxs4("div", { children: [
|
|
1244
|
+
/* @__PURE__ */ jsx5(
|
|
1245
|
+
"div",
|
|
1246
|
+
{
|
|
1247
|
+
style: {
|
|
1248
|
+
minHeight: "40px",
|
|
1249
|
+
display: "flex",
|
|
1250
|
+
alignItems: "center",
|
|
1251
|
+
justifyContent: "center"
|
|
1252
|
+
},
|
|
1253
|
+
children: /* @__PURE__ */ jsx5(Spinner, { size: "md" })
|
|
1254
|
+
}
|
|
1255
|
+
),
|
|
1256
|
+
/* @__PURE__ */ jsx5(
|
|
1257
|
+
"button",
|
|
1258
|
+
{
|
|
1259
|
+
type: "submit",
|
|
1260
|
+
disabled: true,
|
|
1261
|
+
className: buttonClassName,
|
|
1262
|
+
"aria-busy": "false",
|
|
1263
|
+
"aria-disabled": "true",
|
|
1264
|
+
children: submitButtonText
|
|
1265
|
+
}
|
|
1266
|
+
)
|
|
1267
|
+
] }) });
|
|
920
1268
|
};
|
|
921
1269
|
|
|
922
1270
|
// src/components/ProductBadge.tsx
|
|
923
|
-
import
|
|
924
|
-
import { Fragment as Fragment2, jsx as
|
|
1271
|
+
import React5 from "react";
|
|
1272
|
+
import { Fragment as Fragment2, jsx as jsx6 } from "react/jsx-runtime";
|
|
925
1273
|
var ProductBadge = ({
|
|
926
1274
|
children,
|
|
927
1275
|
as: Component = "div",
|
|
928
1276
|
className
|
|
929
1277
|
}) => {
|
|
930
1278
|
const { purchases, loading, hasPaidPurchase, activePurchase } = usePurchase();
|
|
931
|
-
const [hasLoadedOnce, setHasLoadedOnce] =
|
|
932
|
-
|
|
1279
|
+
const [hasLoadedOnce, setHasLoadedOnce] = React5.useState(false);
|
|
1280
|
+
React5.useEffect(() => {
|
|
933
1281
|
if (!loading) {
|
|
934
1282
|
setHasLoadedOnce(true);
|
|
935
1283
|
}
|
|
@@ -937,13 +1285,13 @@ var ProductBadge = ({
|
|
|
937
1285
|
const planToDisplay = activePurchase?.productName || null;
|
|
938
1286
|
const shouldShow = planToDisplay !== null && (!loading || hasLoadedOnce);
|
|
939
1287
|
if (children) {
|
|
940
|
-
return /* @__PURE__ */
|
|
1288
|
+
return /* @__PURE__ */ jsx6(Fragment2, { children: children({ purchases, loading, displayPlan: planToDisplay, shouldShow }) });
|
|
941
1289
|
}
|
|
942
1290
|
if (!shouldShow) {
|
|
943
1291
|
return null;
|
|
944
1292
|
}
|
|
945
1293
|
const computedClassName = typeof className === "function" ? className({ purchases }) : className;
|
|
946
|
-
return /* @__PURE__ */
|
|
1294
|
+
return /* @__PURE__ */ jsx6(
|
|
947
1295
|
Component,
|
|
948
1296
|
{
|
|
949
1297
|
className: computedClassName,
|
|
@@ -961,12 +1309,12 @@ var ProductBadge = ({
|
|
|
961
1309
|
var PlanBadge = ProductBadge;
|
|
962
1310
|
|
|
963
1311
|
// src/components/PurchaseGate.tsx
|
|
964
|
-
import { Fragment as Fragment3, jsx as
|
|
1312
|
+
import { Fragment as Fragment3, jsx as jsx7 } from "react/jsx-runtime";
|
|
965
1313
|
var PurchaseGate = ({ requirePlan, requireProduct, children }) => {
|
|
966
1314
|
const { purchases, loading, hasProduct } = usePurchase();
|
|
967
1315
|
const productToCheck = requireProduct || requirePlan;
|
|
968
1316
|
const hasAccess = productToCheck ? hasProduct(productToCheck) : purchases.some((p) => p.status === "active");
|
|
969
|
-
return /* @__PURE__ */
|
|
1317
|
+
return /* @__PURE__ */ jsx7(Fragment3, { children: children({
|
|
970
1318
|
hasAccess,
|
|
971
1319
|
purchases,
|
|
972
1320
|
loading
|
|
@@ -974,35 +1322,103 @@ var PurchaseGate = ({ requirePlan, requireProduct, children }) => {
|
|
|
974
1322
|
};
|
|
975
1323
|
|
|
976
1324
|
// src/components/PricingSelector.tsx
|
|
977
|
-
import { useCallback as
|
|
1325
|
+
import { useCallback as useCallback7, useMemo as useMemo5 } from "react";
|
|
978
1326
|
|
|
979
1327
|
// src/hooks/usePlans.ts
|
|
980
|
-
import { useState as
|
|
1328
|
+
import { useState as useState6, useEffect as useEffect4, useCallback as useCallback6, useMemo as useMemo4, useRef as useRef6 } from "react";
|
|
981
1329
|
var plansCache = /* @__PURE__ */ new Map();
|
|
982
1330
|
var CACHE_DURATION2 = 5 * 60 * 1e3;
|
|
1331
|
+
function processPlans(raw, filter, sortBy) {
|
|
1332
|
+
let result = sortBy ? [...raw].sort(sortBy) : raw;
|
|
1333
|
+
if (filter) result = result.filter(filter);
|
|
1334
|
+
return result;
|
|
1335
|
+
}
|
|
1336
|
+
function computeInitialIndex(plans, initialPlanRef, autoSelectFirstPaid) {
|
|
1337
|
+
if (plans.length === 0) return 0;
|
|
1338
|
+
if (initialPlanRef) {
|
|
1339
|
+
const idx = plans.findIndex((p) => p.reference === initialPlanRef);
|
|
1340
|
+
if (idx >= 0) return idx;
|
|
1341
|
+
}
|
|
1342
|
+
if (autoSelectFirstPaid) {
|
|
1343
|
+
const idx = plans.findIndex((p) => p.requiresPayment !== false);
|
|
1344
|
+
return idx >= 0 ? idx : 0;
|
|
1345
|
+
}
|
|
1346
|
+
return 0;
|
|
1347
|
+
}
|
|
983
1348
|
function usePlans(options) {
|
|
984
|
-
const {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1349
|
+
const {
|
|
1350
|
+
fetcher,
|
|
1351
|
+
productRef,
|
|
1352
|
+
filter,
|
|
1353
|
+
sortBy,
|
|
1354
|
+
autoSelectFirstPaid = false,
|
|
1355
|
+
initialPlanRef,
|
|
1356
|
+
selectionReady = true
|
|
1357
|
+
} = options;
|
|
1358
|
+
const fetcherRef = useRef6(fetcher);
|
|
1359
|
+
const filterRef = useRef6(filter);
|
|
1360
|
+
const sortByRef = useRef6(sortBy);
|
|
1361
|
+
const autoSelectFirstPaidRef = useRef6(autoSelectFirstPaid);
|
|
1362
|
+
const initialPlanRefRef = useRef6(initialPlanRef);
|
|
1363
|
+
const selectionReadyRef = useRef6(selectionReady);
|
|
1364
|
+
const hasAppliedInitialRef = useRef6(false);
|
|
1365
|
+
const userHasSelectedRef = useRef6(false);
|
|
1366
|
+
const [selectedPlanIndex, setSelectedPlanIndexState] = useState6(() => {
|
|
1367
|
+
if (!selectionReady || !productRef) return 0;
|
|
1368
|
+
const cached = plansCache.get(productRef);
|
|
1369
|
+
if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
|
|
1370
|
+
return 0;
|
|
1371
|
+
}
|
|
1372
|
+
const processed = processPlans(cached.plans, filter, sortBy);
|
|
1373
|
+
if (processed.length === 0) return 0;
|
|
1374
|
+
const idx = computeInitialIndex(processed, initialPlanRef, autoSelectFirstPaid);
|
|
1375
|
+
hasAppliedInitialRef.current = true;
|
|
1376
|
+
return idx;
|
|
1377
|
+
});
|
|
1378
|
+
const [plans, setPlans] = useState6(() => {
|
|
1379
|
+
if (!productRef) return [];
|
|
1380
|
+
const cached = plansCache.get(productRef);
|
|
1381
|
+
if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
|
|
1382
|
+
return [];
|
|
1383
|
+
}
|
|
1384
|
+
return processPlans(cached.plans, filter, sortBy);
|
|
1385
|
+
});
|
|
1386
|
+
const [loading, setLoading] = useState6(() => plans.length === 0);
|
|
1387
|
+
const [error, setError] = useState6(null);
|
|
1388
|
+
useEffect4(() => {
|
|
994
1389
|
fetcherRef.current = fetcher;
|
|
995
1390
|
}, [fetcher]);
|
|
996
|
-
|
|
1391
|
+
useEffect4(() => {
|
|
997
1392
|
filterRef.current = filter;
|
|
998
1393
|
}, [filter]);
|
|
999
|
-
|
|
1394
|
+
useEffect4(() => {
|
|
1000
1395
|
sortByRef.current = sortBy;
|
|
1001
1396
|
}, [sortBy]);
|
|
1002
|
-
|
|
1397
|
+
useEffect4(() => {
|
|
1003
1398
|
autoSelectFirstPaidRef.current = autoSelectFirstPaid;
|
|
1004
1399
|
}, [autoSelectFirstPaid]);
|
|
1005
|
-
|
|
1400
|
+
useEffect4(() => {
|
|
1401
|
+
initialPlanRefRef.current = initialPlanRef;
|
|
1402
|
+
}, [initialPlanRef]);
|
|
1403
|
+
useEffect4(() => {
|
|
1404
|
+
selectionReadyRef.current = selectionReady;
|
|
1405
|
+
}, [selectionReady]);
|
|
1406
|
+
const setSelectedPlanIndex = useCallback6((index) => {
|
|
1407
|
+
userHasSelectedRef.current = true;
|
|
1408
|
+
setSelectedPlanIndexState(index);
|
|
1409
|
+
}, []);
|
|
1410
|
+
const applyInitialSelection = useCallback6((processedPlans) => {
|
|
1411
|
+
if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
|
|
1412
|
+
if (!selectionReadyRef.current || processedPlans.length === 0) return;
|
|
1413
|
+
hasAppliedInitialRef.current = true;
|
|
1414
|
+
const idx = computeInitialIndex(
|
|
1415
|
+
processedPlans,
|
|
1416
|
+
initialPlanRefRef.current,
|
|
1417
|
+
autoSelectFirstPaidRef.current
|
|
1418
|
+
);
|
|
1419
|
+
setSelectedPlanIndexState(idx);
|
|
1420
|
+
}, []);
|
|
1421
|
+
const fetchPlans = useCallback6(
|
|
1006
1422
|
async (force = false) => {
|
|
1007
1423
|
if (!productRef) {
|
|
1008
1424
|
setError(new Error("Product reference not configured"));
|
|
@@ -1012,34 +1428,29 @@ function usePlans(options) {
|
|
|
1012
1428
|
const cached = plansCache.get(productRef);
|
|
1013
1429
|
const now = Date.now();
|
|
1014
1430
|
if (!force && cached && now - cached.timestamp < CACHE_DURATION2) {
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1431
|
+
const processedPlans = processPlans(
|
|
1432
|
+
cached.plans,
|
|
1433
|
+
filterRef.current,
|
|
1434
|
+
sortByRef.current
|
|
1435
|
+
);
|
|
1020
1436
|
setPlans(processedPlans);
|
|
1021
1437
|
setLoading(false);
|
|
1022
1438
|
setError(null);
|
|
1023
|
-
|
|
1024
|
-
const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
|
|
1025
|
-
setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
|
|
1026
|
-
}
|
|
1439
|
+
applyInitialSelection(processedPlans);
|
|
1027
1440
|
return;
|
|
1028
1441
|
}
|
|
1029
1442
|
if (cached?.promise) {
|
|
1030
1443
|
try {
|
|
1031
1444
|
setLoading(true);
|
|
1032
1445
|
const fetchedPlans = await cached.promise;
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1446
|
+
const processedPlans = processPlans(
|
|
1447
|
+
fetchedPlans,
|
|
1448
|
+
filterRef.current,
|
|
1449
|
+
sortByRef.current
|
|
1450
|
+
);
|
|
1037
1451
|
setPlans(processedPlans);
|
|
1038
1452
|
setError(null);
|
|
1039
|
-
|
|
1040
|
-
const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
|
|
1041
|
-
setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
|
|
1042
|
-
}
|
|
1453
|
+
applyInitialSelection(processedPlans);
|
|
1043
1454
|
} catch (err) {
|
|
1044
1455
|
setError(err instanceof Error ? err : new Error("Failed to load plans"));
|
|
1045
1456
|
} finally {
|
|
@@ -1051,26 +1462,16 @@ function usePlans(options) {
|
|
|
1051
1462
|
setLoading(true);
|
|
1052
1463
|
setError(null);
|
|
1053
1464
|
const fetchPromise = fetcherRef.current(productRef);
|
|
1054
|
-
plansCache.set(productRef, {
|
|
1055
|
-
plans: [],
|
|
1056
|
-
timestamp: now,
|
|
1057
|
-
promise: fetchPromise
|
|
1058
|
-
});
|
|
1465
|
+
plansCache.set(productRef, { plans: [], timestamp: now, promise: fetchPromise });
|
|
1059
1466
|
const fetchedPlans = await fetchPromise;
|
|
1060
|
-
plansCache.set(productRef, {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
if (sortByRef.current) {
|
|
1067
|
-
processedPlans = [...processedPlans].sort(sortByRef.current);
|
|
1068
|
-
}
|
|
1467
|
+
plansCache.set(productRef, { plans: fetchedPlans, timestamp: now, promise: null });
|
|
1468
|
+
const processedPlans = processPlans(
|
|
1469
|
+
fetchedPlans,
|
|
1470
|
+
filterRef.current,
|
|
1471
|
+
sortByRef.current
|
|
1472
|
+
);
|
|
1069
1473
|
setPlans(processedPlans);
|
|
1070
|
-
|
|
1071
|
-
const firstPaidIndex = processedPlans.findIndex((plan) => plan.price && plan.price > 0);
|
|
1072
|
-
setSelectedPlanIndex(firstPaidIndex >= 0 ? firstPaidIndex : 0);
|
|
1073
|
-
}
|
|
1474
|
+
applyInitialSelection(processedPlans);
|
|
1074
1475
|
} catch (err) {
|
|
1075
1476
|
plansCache.delete(productRef);
|
|
1076
1477
|
setError(err instanceof Error ? err : new Error("Failed to load plans"));
|
|
@@ -1078,22 +1479,25 @@ function usePlans(options) {
|
|
|
1078
1479
|
setLoading(false);
|
|
1079
1480
|
}
|
|
1080
1481
|
},
|
|
1081
|
-
[productRef]
|
|
1482
|
+
[productRef, applyInitialSelection]
|
|
1082
1483
|
);
|
|
1083
|
-
|
|
1484
|
+
useEffect4(() => {
|
|
1084
1485
|
fetchPlans();
|
|
1085
1486
|
}, [fetchPlans]);
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1487
|
+
useEffect4(() => {
|
|
1488
|
+
if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
|
|
1489
|
+
if (!selectionReady || plans.length === 0) return;
|
|
1490
|
+
applyInitialSelection(plans);
|
|
1491
|
+
}, [selectionReady, plans, applyInitialSelection]);
|
|
1492
|
+
const selectedPlan = useMemo4(() => plans[selectedPlanIndex] || null, [plans, selectedPlanIndex]);
|
|
1493
|
+
const selectPlan = useCallback6(
|
|
1090
1494
|
(planRef) => {
|
|
1091
1495
|
const index = plans.findIndex((p) => p.reference === planRef);
|
|
1092
1496
|
if (index >= 0) {
|
|
1093
1497
|
setSelectedPlanIndex(index);
|
|
1094
1498
|
}
|
|
1095
1499
|
},
|
|
1096
|
-
[plans]
|
|
1500
|
+
[plans, setSelectedPlanIndex]
|
|
1097
1501
|
);
|
|
1098
1502
|
return {
|
|
1099
1503
|
plans,
|
|
@@ -1103,13 +1507,13 @@ function usePlans(options) {
|
|
|
1103
1507
|
selectedPlan,
|
|
1104
1508
|
setSelectedPlanIndex,
|
|
1105
1509
|
selectPlan,
|
|
1106
|
-
refetch: () => fetchPlans(true)
|
|
1107
|
-
|
|
1510
|
+
refetch: () => fetchPlans(true),
|
|
1511
|
+
isSelectionReady: hasAppliedInitialRef.current
|
|
1108
1512
|
};
|
|
1109
1513
|
}
|
|
1110
1514
|
|
|
1111
1515
|
// src/components/PricingSelector.tsx
|
|
1112
|
-
import { Fragment as Fragment4, jsx as
|
|
1516
|
+
import { Fragment as Fragment4, jsx as jsx8 } from "react/jsx-runtime";
|
|
1113
1517
|
var PricingSelector = ({
|
|
1114
1518
|
productRef,
|
|
1115
1519
|
fetcher,
|
|
@@ -1127,26 +1531,26 @@ var PricingSelector = ({
|
|
|
1127
1531
|
autoSelectFirstPaid
|
|
1128
1532
|
});
|
|
1129
1533
|
const { plans } = plansHook;
|
|
1130
|
-
const isPaidPlan =
|
|
1534
|
+
const isPaidPlan = useCallback7(
|
|
1131
1535
|
(planRef) => {
|
|
1132
1536
|
const plan = plans.find((p) => p.reference === planRef);
|
|
1133
|
-
return plan ?
|
|
1537
|
+
return plan ? plan.requiresPayment !== false : true;
|
|
1134
1538
|
},
|
|
1135
1539
|
[plans]
|
|
1136
1540
|
);
|
|
1137
|
-
const activePurchase =
|
|
1541
|
+
const activePurchase = useMemo5(() => {
|
|
1138
1542
|
const activePurchases = purchases.filter((p) => p.status === "active");
|
|
1139
1543
|
return activePurchases.sort(
|
|
1140
1544
|
(a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
|
|
1141
1545
|
)[0] || null;
|
|
1142
1546
|
}, [purchases]);
|
|
1143
|
-
const isCurrentPlan =
|
|
1547
|
+
const isCurrentPlan = useCallback7(
|
|
1144
1548
|
(planRef) => {
|
|
1145
1549
|
return activePurchase?.planSnapshot?.reference === planRef;
|
|
1146
1550
|
},
|
|
1147
1551
|
[activePurchase]
|
|
1148
1552
|
);
|
|
1149
|
-
return /* @__PURE__ */
|
|
1553
|
+
return /* @__PURE__ */ jsx8(Fragment4, { children: children({
|
|
1150
1554
|
...plansHook,
|
|
1151
1555
|
purchases,
|
|
1152
1556
|
isPaidPlan,
|
|
@@ -1155,14 +1559,58 @@ var PricingSelector = ({
|
|
|
1155
1559
|
};
|
|
1156
1560
|
var PlanSelector = PricingSelector;
|
|
1157
1561
|
|
|
1562
|
+
// src/hooks/useBalance.ts
|
|
1563
|
+
import { useEffect as useEffect5 } from "react";
|
|
1564
|
+
function useBalance() {
|
|
1565
|
+
const { balance } = useSolvaPay();
|
|
1566
|
+
useEffect5(() => {
|
|
1567
|
+
balance.refetch();
|
|
1568
|
+
}, [balance.refetch]);
|
|
1569
|
+
return balance;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/components/BalanceBadge.tsx
|
|
1573
|
+
import { Fragment as Fragment5, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1574
|
+
function BalanceBadge({ className, numberOnly, children }) {
|
|
1575
|
+
const { credits, displayCurrency, creditsPerMinorUnit, displayExchangeRate, loading } = useBalance();
|
|
1576
|
+
if (children) {
|
|
1577
|
+
return /* @__PURE__ */ jsx9(Fragment5, { children: children({ credits, loading, displayCurrency, creditsPerMinorUnit }) });
|
|
1578
|
+
}
|
|
1579
|
+
if (loading) {
|
|
1580
|
+
return /* @__PURE__ */ jsx9("span", { className, "aria-busy": "true" });
|
|
1581
|
+
}
|
|
1582
|
+
if (credits == null) {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
const formattedCredits = new Intl.NumberFormat().format(credits);
|
|
1586
|
+
if (numberOnly) {
|
|
1587
|
+
return /* @__PURE__ */ jsx9("span", { className, children: formattedCredits });
|
|
1588
|
+
}
|
|
1589
|
+
let currencyEquivalent = "";
|
|
1590
|
+
if (displayCurrency && creditsPerMinorUnit) {
|
|
1591
|
+
const usdCents = credits / creditsPerMinorUnit;
|
|
1592
|
+
const displayMajorUnits = usdCents * (displayExchangeRate ?? 1) / 100;
|
|
1593
|
+
currencyEquivalent = ` (~${new Intl.NumberFormat(void 0, {
|
|
1594
|
+
style: "currency",
|
|
1595
|
+
currency: displayCurrency,
|
|
1596
|
+
minimumFractionDigits: 2
|
|
1597
|
+
}).format(displayMajorUnits)})`;
|
|
1598
|
+
}
|
|
1599
|
+
return /* @__PURE__ */ jsxs5("span", { className, children: [
|
|
1600
|
+
formattedCredits,
|
|
1601
|
+
" credits",
|
|
1602
|
+
currencyEquivalent
|
|
1603
|
+
] });
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1158
1606
|
// src/hooks/usePurchaseStatus.ts
|
|
1159
|
-
import { useMemo as
|
|
1607
|
+
import { useMemo as useMemo6, useCallback as useCallback8 } from "react";
|
|
1160
1608
|
function usePurchaseStatus() {
|
|
1161
1609
|
const { purchases } = usePurchase();
|
|
1162
|
-
const isPaidPurchase2 =
|
|
1610
|
+
const isPaidPurchase2 = useCallback8((p) => {
|
|
1163
1611
|
return (p.amount ?? 0) > 0;
|
|
1164
1612
|
}, []);
|
|
1165
|
-
const purchaseData =
|
|
1613
|
+
const purchaseData = useMemo6(() => {
|
|
1166
1614
|
const cancelledPaidPurchases = purchases.filter((p) => {
|
|
1167
1615
|
return p.status === "active" && p.cancelledAt && isPaidPurchase2(p);
|
|
1168
1616
|
});
|
|
@@ -1175,7 +1623,7 @@ function usePurchaseStatus() {
|
|
|
1175
1623
|
shouldShowCancelledNotice
|
|
1176
1624
|
};
|
|
1177
1625
|
}, [purchases, isPaidPurchase2]);
|
|
1178
|
-
const formatDate =
|
|
1626
|
+
const formatDate = useCallback8((dateString) => {
|
|
1179
1627
|
if (!dateString) return null;
|
|
1180
1628
|
return new Date(dateString).toLocaleDateString("en-US", {
|
|
1181
1629
|
year: "numeric",
|
|
@@ -1183,7 +1631,7 @@ function usePurchaseStatus() {
|
|
|
1183
1631
|
day: "numeric"
|
|
1184
1632
|
});
|
|
1185
1633
|
}, []);
|
|
1186
|
-
const getDaysUntilExpiration =
|
|
1634
|
+
const getDaysUntilExpiration = useCallback8((endDate) => {
|
|
1187
1635
|
if (!endDate) return null;
|
|
1188
1636
|
const now = /* @__PURE__ */ new Date();
|
|
1189
1637
|
const expiration = new Date(endDate);
|
|
@@ -1198,7 +1646,206 @@ function usePurchaseStatus() {
|
|
|
1198
1646
|
getDaysUntilExpiration
|
|
1199
1647
|
};
|
|
1200
1648
|
}
|
|
1649
|
+
|
|
1650
|
+
// src/hooks/usePurchaseActions.ts
|
|
1651
|
+
import { useState as useState7, useCallback as useCallback9 } from "react";
|
|
1652
|
+
function usePurchaseActions() {
|
|
1653
|
+
const ctx = useSolvaPay();
|
|
1654
|
+
const [isCancelling, setIsCancelling] = useState7(false);
|
|
1655
|
+
const [isReactivating, setIsReactivating] = useState7(false);
|
|
1656
|
+
const [isActivating, setIsActivating] = useState7(false);
|
|
1657
|
+
const cancelRenewal = useCallback9(
|
|
1658
|
+
async (params) => {
|
|
1659
|
+
setIsCancelling(true);
|
|
1660
|
+
try {
|
|
1661
|
+
return await ctx.cancelRenewal(params);
|
|
1662
|
+
} finally {
|
|
1663
|
+
setIsCancelling(false);
|
|
1664
|
+
}
|
|
1665
|
+
},
|
|
1666
|
+
[ctx.cancelRenewal]
|
|
1667
|
+
);
|
|
1668
|
+
const reactivateRenewal = useCallback9(
|
|
1669
|
+
async (params) => {
|
|
1670
|
+
setIsReactivating(true);
|
|
1671
|
+
try {
|
|
1672
|
+
return await ctx.reactivateRenewal(params);
|
|
1673
|
+
} finally {
|
|
1674
|
+
setIsReactivating(false);
|
|
1675
|
+
}
|
|
1676
|
+
},
|
|
1677
|
+
[ctx.reactivateRenewal]
|
|
1678
|
+
);
|
|
1679
|
+
const activatePlan = useCallback9(
|
|
1680
|
+
async (params) => {
|
|
1681
|
+
setIsActivating(true);
|
|
1682
|
+
try {
|
|
1683
|
+
return await ctx.activatePlan(params);
|
|
1684
|
+
} finally {
|
|
1685
|
+
setIsActivating(false);
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
[ctx.activatePlan]
|
|
1689
|
+
);
|
|
1690
|
+
return {
|
|
1691
|
+
cancelRenewal,
|
|
1692
|
+
reactivateRenewal,
|
|
1693
|
+
activatePlan,
|
|
1694
|
+
isCancelling,
|
|
1695
|
+
isReactivating,
|
|
1696
|
+
isActivating
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/hooks/useActivation.ts
|
|
1701
|
+
import { useState as useState8, useCallback as useCallback10 } from "react";
|
|
1702
|
+
function useActivation() {
|
|
1703
|
+
const { activatePlan } = useSolvaPay();
|
|
1704
|
+
const [state, setState] = useState8("idle");
|
|
1705
|
+
const [error, setError] = useState8(null);
|
|
1706
|
+
const [result, setResult] = useState8(null);
|
|
1707
|
+
const activate = useCallback10(
|
|
1708
|
+
async (params) => {
|
|
1709
|
+
setState("activating");
|
|
1710
|
+
setError(null);
|
|
1711
|
+
setResult(null);
|
|
1712
|
+
try {
|
|
1713
|
+
const data = await activatePlan(params);
|
|
1714
|
+
setResult(data);
|
|
1715
|
+
switch (data.status) {
|
|
1716
|
+
case "activated":
|
|
1717
|
+
case "already_active":
|
|
1718
|
+
setState("activated");
|
|
1719
|
+
break;
|
|
1720
|
+
case "topup_required":
|
|
1721
|
+
setState("topup_required");
|
|
1722
|
+
break;
|
|
1723
|
+
case "payment_required":
|
|
1724
|
+
setError("This plan requires payment. Please select a different plan.");
|
|
1725
|
+
setState("payment_required");
|
|
1726
|
+
break;
|
|
1727
|
+
case "invalid":
|
|
1728
|
+
setError(data.message || "Invalid plan configuration.");
|
|
1729
|
+
setState("error");
|
|
1730
|
+
break;
|
|
1731
|
+
default:
|
|
1732
|
+
setError("Unexpected response from server.");
|
|
1733
|
+
setState("error");
|
|
1734
|
+
}
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
setError(err instanceof Error ? err.message : "Activation failed");
|
|
1737
|
+
setState("error");
|
|
1738
|
+
}
|
|
1739
|
+
},
|
|
1740
|
+
[activatePlan]
|
|
1741
|
+
);
|
|
1742
|
+
const reset = useCallback10(() => {
|
|
1743
|
+
setState("idle");
|
|
1744
|
+
setError(null);
|
|
1745
|
+
setResult(null);
|
|
1746
|
+
}, []);
|
|
1747
|
+
return { activate, state, error, result, reset };
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/hooks/useTopupAmountSelector.ts
|
|
1751
|
+
import { useState as useState9, useCallback as useCallback11, useMemo as useMemo7 } from "react";
|
|
1752
|
+
function getQuickAmounts(currency) {
|
|
1753
|
+
switch (currency.toUpperCase()) {
|
|
1754
|
+
case "SEK":
|
|
1755
|
+
case "NOK":
|
|
1756
|
+
case "DKK":
|
|
1757
|
+
return [100, 500, 1e3, 5e3];
|
|
1758
|
+
case "JPY":
|
|
1759
|
+
return [1e3, 5e3, 1e4, 5e4];
|
|
1760
|
+
case "KRW":
|
|
1761
|
+
return [1e4, 5e4, 1e5, 5e5];
|
|
1762
|
+
case "ISK":
|
|
1763
|
+
case "HUF":
|
|
1764
|
+
return [1e3, 5e3, 1e4, 5e4];
|
|
1765
|
+
default:
|
|
1766
|
+
return [10, 50, 100, 500];
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
function getCurrencySymbol(currency) {
|
|
1770
|
+
try {
|
|
1771
|
+
const parts = new Intl.NumberFormat("en", { style: "currency", currency }).formatToParts(0);
|
|
1772
|
+
const sym = parts.find((p) => p.type === "currency");
|
|
1773
|
+
return sym?.value || currency;
|
|
1774
|
+
} catch {
|
|
1775
|
+
return currency;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
var DEFAULT_MIN = 1;
|
|
1779
|
+
var DEFAULT_MAX = 1e5;
|
|
1780
|
+
function useTopupAmountSelector(options) {
|
|
1781
|
+
const { currency, minAmount = DEFAULT_MIN, maxAmount = DEFAULT_MAX } = options;
|
|
1782
|
+
const [selectedAmount, setSelectedAmount] = useState9(null);
|
|
1783
|
+
const [customAmount, setCustomAmountRaw] = useState9("");
|
|
1784
|
+
const [error, setError] = useState9(null);
|
|
1785
|
+
const quickAmounts = useMemo7(() => getQuickAmounts(currency), [currency]);
|
|
1786
|
+
const currencySymbol = useMemo7(() => getCurrencySymbol(currency), [currency]);
|
|
1787
|
+
const resolvedAmount = useMemo7(() => {
|
|
1788
|
+
if (customAmount) {
|
|
1789
|
+
const parsed = parseFloat(customAmount);
|
|
1790
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
1791
|
+
}
|
|
1792
|
+
return selectedAmount;
|
|
1793
|
+
}, [selectedAmount, customAmount]);
|
|
1794
|
+
const selectQuickAmount = useCallback11(
|
|
1795
|
+
(amount) => {
|
|
1796
|
+
setSelectedAmount(amount);
|
|
1797
|
+
setCustomAmountRaw("");
|
|
1798
|
+
setError(null);
|
|
1799
|
+
},
|
|
1800
|
+
[]
|
|
1801
|
+
);
|
|
1802
|
+
const setCustomAmount = useCallback11(
|
|
1803
|
+
(value) => {
|
|
1804
|
+
const sanitized = value.replace(/[^0-9.]/g, "");
|
|
1805
|
+
const dotIndex = sanitized.indexOf(".");
|
|
1806
|
+
const cleaned = dotIndex === -1 ? sanitized : sanitized.slice(0, dotIndex + 1) + sanitized.slice(dotIndex + 1).replace(/\./g, "");
|
|
1807
|
+
setCustomAmountRaw(cleaned);
|
|
1808
|
+
setSelectedAmount(null);
|
|
1809
|
+
setError(null);
|
|
1810
|
+
},
|
|
1811
|
+
[]
|
|
1812
|
+
);
|
|
1813
|
+
const validate = useCallback11(() => {
|
|
1814
|
+
if (resolvedAmount == null) {
|
|
1815
|
+
setError("Please select or enter an amount");
|
|
1816
|
+
return false;
|
|
1817
|
+
}
|
|
1818
|
+
if (resolvedAmount < minAmount) {
|
|
1819
|
+
setError(`Minimum amount is ${currencySymbol}${minAmount}`);
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1822
|
+
if (resolvedAmount > maxAmount) {
|
|
1823
|
+
setError(`Maximum amount is ${currencySymbol}${maxAmount.toLocaleString()}`);
|
|
1824
|
+
return false;
|
|
1825
|
+
}
|
|
1826
|
+
setError(null);
|
|
1827
|
+
return true;
|
|
1828
|
+
}, [resolvedAmount, minAmount, maxAmount, currencySymbol]);
|
|
1829
|
+
const reset = useCallback11(() => {
|
|
1830
|
+
setSelectedAmount(null);
|
|
1831
|
+
setCustomAmountRaw("");
|
|
1832
|
+
setError(null);
|
|
1833
|
+
}, []);
|
|
1834
|
+
return {
|
|
1835
|
+
quickAmounts,
|
|
1836
|
+
selectedAmount,
|
|
1837
|
+
customAmount,
|
|
1838
|
+
resolvedAmount,
|
|
1839
|
+
selectQuickAmount,
|
|
1840
|
+
setCustomAmount,
|
|
1841
|
+
error,
|
|
1842
|
+
validate,
|
|
1843
|
+
reset,
|
|
1844
|
+
currencySymbol
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1201
1847
|
export {
|
|
1848
|
+
BalanceBadge,
|
|
1202
1849
|
PaymentForm,
|
|
1203
1850
|
PlanBadge,
|
|
1204
1851
|
PlanSelector,
|
|
@@ -1208,6 +1855,7 @@ export {
|
|
|
1208
1855
|
SolvaPayProvider,
|
|
1209
1856
|
Spinner,
|
|
1210
1857
|
StripePaymentFormWrapper,
|
|
1858
|
+
TopupForm,
|
|
1211
1859
|
defaultAuthAdapter,
|
|
1212
1860
|
filterPurchases,
|
|
1213
1861
|
getActivePurchases,
|
|
@@ -1215,10 +1863,15 @@ export {
|
|
|
1215
1863
|
getMostRecentPurchase,
|
|
1216
1864
|
getPrimaryPurchase,
|
|
1217
1865
|
isPaidPurchase,
|
|
1866
|
+
useActivation,
|
|
1867
|
+
useBalance,
|
|
1218
1868
|
useCheckout,
|
|
1219
1869
|
useCustomer,
|
|
1220
1870
|
usePlans,
|
|
1221
1871
|
usePurchase,
|
|
1872
|
+
usePurchaseActions,
|
|
1222
1873
|
usePurchaseStatus,
|
|
1223
|
-
useSolvaPay
|
|
1874
|
+
useSolvaPay,
|
|
1875
|
+
useTopup,
|
|
1876
|
+
useTopupAmountSelector
|
|
1224
1877
|
};
|