@reevit/react 0.5.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +147 -67
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +147 -67
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -253,7 +253,7 @@ var ReevitAPIClient = class {
|
|
|
253
253
|
allowed_providers: options?.allowedProviders
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
|
-
const idempotencyKey = generateIdempotencyKey({
|
|
256
|
+
const idempotencyKey = config.idempotencyKey || generateIdempotencyKey({
|
|
257
257
|
amount: config.amount,
|
|
258
258
|
currency: config.currency,
|
|
259
259
|
customer: config.email || config.metadata?.customerId || "",
|
|
@@ -432,6 +432,72 @@ function normalizeBranding(branding) {
|
|
|
432
432
|
setIf("selectedBorderColor", getString(raw.selectedBorderColor ?? raw.selected_border_color));
|
|
433
433
|
return theme;
|
|
434
434
|
}
|
|
435
|
+
var INTENT_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
436
|
+
var intentCache = /* @__PURE__ */ new Map();
|
|
437
|
+
function pruneIntentCache(now = Date.now()) {
|
|
438
|
+
for (const [key, entry] of intentCache) {
|
|
439
|
+
if (entry.expiresAt <= now) {
|
|
440
|
+
intentCache.delete(key);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function getIntentCacheEntry(key) {
|
|
445
|
+
const entry = intentCache.get(key);
|
|
446
|
+
if (!entry) {
|
|
447
|
+
return void 0;
|
|
448
|
+
}
|
|
449
|
+
if (entry.expiresAt <= Date.now()) {
|
|
450
|
+
intentCache.delete(key);
|
|
451
|
+
return void 0;
|
|
452
|
+
}
|
|
453
|
+
return entry;
|
|
454
|
+
}
|
|
455
|
+
function setIntentCacheEntry(key, update) {
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
const existing = getIntentCacheEntry(key);
|
|
458
|
+
const next = {
|
|
459
|
+
...existing,
|
|
460
|
+
...update,
|
|
461
|
+
expiresAt: now + INTENT_CACHE_TTL_MS
|
|
462
|
+
};
|
|
463
|
+
intentCache.set(key, next);
|
|
464
|
+
return next;
|
|
465
|
+
}
|
|
466
|
+
function clearIntentCacheEntry(key) {
|
|
467
|
+
intentCache.delete(key);
|
|
468
|
+
}
|
|
469
|
+
function buildIdempotencyPayload(config, method, options) {
|
|
470
|
+
const payload = {
|
|
471
|
+
amount: config.amount,
|
|
472
|
+
currency: config.currency,
|
|
473
|
+
email: config.email || "",
|
|
474
|
+
phone: config.phone || "",
|
|
475
|
+
customerName: config.customerName || "",
|
|
476
|
+
paymentLinkCode: config.paymentLinkCode || "",
|
|
477
|
+
paymentMethods: config.paymentMethods || [],
|
|
478
|
+
metadata: config.metadata || {},
|
|
479
|
+
customFields: config.customFields || {},
|
|
480
|
+
method: method || "",
|
|
481
|
+
preferredProvider: options?.preferredProvider || "",
|
|
482
|
+
allowedProviders: options?.allowedProviders || [],
|
|
483
|
+
publicKey: config.publicKey || ""
|
|
484
|
+
};
|
|
485
|
+
if (config.reference) {
|
|
486
|
+
payload.reference = config.reference;
|
|
487
|
+
}
|
|
488
|
+
return payload;
|
|
489
|
+
}
|
|
490
|
+
function resolveIntentIdentity(config, method, options) {
|
|
491
|
+
pruneIntentCache();
|
|
492
|
+
const idempotencyKey = config.idempotencyKey || generateIdempotencyKey(buildIdempotencyPayload(config, method, options));
|
|
493
|
+
const existing = getIntentCacheEntry(idempotencyKey);
|
|
494
|
+
const reference = config.reference || existing?.reference || generateReference();
|
|
495
|
+
const cacheEntry = setIntentCacheEntry(idempotencyKey, { reference });
|
|
496
|
+
return { idempotencyKey, reference, cacheEntry };
|
|
497
|
+
}
|
|
498
|
+
function isPaymentError(error) {
|
|
499
|
+
return typeof error === "object" && error !== null && "code" in error && "message" in error;
|
|
500
|
+
}
|
|
435
501
|
function mapToPaymentIntent(response, config) {
|
|
436
502
|
return {
|
|
437
503
|
id: response.id,
|
|
@@ -465,14 +531,22 @@ function useReevit(options) {
|
|
|
465
531
|
selectedMethod: config.initialPaymentIntent?.availableMethods?.length === 1 ? config.initialPaymentIntent.availableMethods[0] : null
|
|
466
532
|
});
|
|
467
533
|
const apiClientRef = useRef(null);
|
|
468
|
-
const
|
|
534
|
+
const stateRef = useRef(state);
|
|
535
|
+
useEffect(() => {
|
|
536
|
+
stateRef.current = state;
|
|
537
|
+
}, [state]);
|
|
538
|
+
const currentIntentKeyRef = useRef(
|
|
539
|
+
config.initialPaymentIntent ? `initial:${config.initialPaymentIntent.id}` : null
|
|
540
|
+
);
|
|
469
541
|
const initRequestIdRef = useRef(0);
|
|
470
542
|
useEffect(() => {
|
|
471
543
|
if (config.initialPaymentIntent) {
|
|
472
544
|
if (!state.paymentIntent || state.paymentIntent.id !== config.initialPaymentIntent.id) {
|
|
473
545
|
dispatch({ type: "INIT_SUCCESS", payload: config.initialPaymentIntent });
|
|
474
|
-
|
|
546
|
+
currentIntentKeyRef.current = `initial:${config.initialPaymentIntent.id}`;
|
|
475
547
|
}
|
|
548
|
+
} else if (currentIntentKeyRef.current?.startsWith("initial:")) {
|
|
549
|
+
currentIntentKeyRef.current = null;
|
|
476
550
|
}
|
|
477
551
|
}, [config.initialPaymentIntent, state.paymentIntent?.id]);
|
|
478
552
|
if (!apiClientRef.current) {
|
|
@@ -486,61 +560,60 @@ function useReevit(options) {
|
|
|
486
560
|
}, [state.status, onStateChange]);
|
|
487
561
|
const initialize = useCallback(
|
|
488
562
|
async (method, options2) => {
|
|
489
|
-
if (
|
|
563
|
+
if (config.initialPaymentIntent) {
|
|
490
564
|
return;
|
|
491
565
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
dispatch({ type: "INIT_START" });
|
|
566
|
+
let requestId = 0;
|
|
567
|
+
let intentKey = null;
|
|
495
568
|
try {
|
|
496
569
|
const apiClient = apiClientRef.current;
|
|
497
570
|
if (!apiClient) {
|
|
498
571
|
throw new Error("API client not initialized");
|
|
499
572
|
}
|
|
500
|
-
const reference = config.reference || generateReference();
|
|
501
573
|
const country = detectCountryFromCurrency(config.currency);
|
|
502
574
|
const defaultMethod = config.paymentMethods && config.paymentMethods.length === 1 ? config.paymentMethods[0] : void 0;
|
|
503
575
|
const paymentMethod = method ?? defaultMethod;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
576
|
+
const identity = resolveIntentIdentity(config, paymentMethod, options2);
|
|
577
|
+
const { idempotencyKey, reference, cacheEntry } = identity;
|
|
578
|
+
intentKey = idempotencyKey;
|
|
579
|
+
if (currentIntentKeyRef.current === idempotencyKey && stateRef.current.paymentIntent) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
currentIntentKeyRef.current = idempotencyKey;
|
|
583
|
+
requestId = ++initRequestIdRef.current;
|
|
584
|
+
if (stateRef.current.status !== "loading") {
|
|
585
|
+
dispatch({ type: "INIT_START" });
|
|
586
|
+
}
|
|
587
|
+
const requestIntent = async () => {
|
|
588
|
+
if (config.paymentLinkCode) {
|
|
589
|
+
const response = await fetch(
|
|
590
|
+
`${apiBaseUrl || DEFAULT_PUBLIC_API_BASE_URL}/v1/pay/${config.paymentLinkCode}/pay`,
|
|
591
|
+
{
|
|
592
|
+
method: "POST",
|
|
593
|
+
headers: {
|
|
594
|
+
"Content-Type": "application/json",
|
|
595
|
+
"Idempotency-Key": idempotencyKey
|
|
596
|
+
},
|
|
597
|
+
body: JSON.stringify({
|
|
598
|
+
amount: config.amount,
|
|
599
|
+
email: config.email || "",
|
|
600
|
+
name: config.customerName || "",
|
|
601
|
+
phone: config.phone || "",
|
|
602
|
+
method: paymentMethod,
|
|
603
|
+
country,
|
|
604
|
+
provider: options2?.preferredProvider || options2?.allowedProviders?.[0],
|
|
605
|
+
custom_fields: config.customFields
|
|
606
|
+
})
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
const responseData = await response.json().catch(() => ({}));
|
|
610
|
+
if (!response.ok) {
|
|
611
|
+
throw buildPaymentLinkError(response, responseData);
|
|
533
612
|
}
|
|
534
|
-
|
|
535
|
-
const responseData = await response.json().catch(() => ({}));
|
|
536
|
-
if (!response.ok) {
|
|
537
|
-
error = buildPaymentLinkError(response, responseData);
|
|
538
|
-
} else {
|
|
539
|
-
data = responseData;
|
|
613
|
+
return responseData;
|
|
540
614
|
}
|
|
541
|
-
} else {
|
|
542
615
|
const result = await apiClient.createPaymentIntent(
|
|
543
|
-
{ ...config, reference },
|
|
616
|
+
{ ...config, reference, idempotencyKey },
|
|
544
617
|
paymentMethod,
|
|
545
618
|
country,
|
|
546
619
|
{
|
|
@@ -548,35 +621,43 @@ function useReevit(options) {
|
|
|
548
621
|
allowedProviders: options2?.allowedProviders
|
|
549
622
|
}
|
|
550
623
|
);
|
|
551
|
-
|
|
552
|
-
|
|
624
|
+
if (result.error) {
|
|
625
|
+
throw result.error;
|
|
626
|
+
}
|
|
627
|
+
if (!result.data) {
|
|
628
|
+
throw {
|
|
629
|
+
code: "INIT_FAILED",
|
|
630
|
+
message: "No data received from API",
|
|
631
|
+
recoverable: true
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
return result.data;
|
|
635
|
+
};
|
|
636
|
+
let data;
|
|
637
|
+
if (cacheEntry?.response) {
|
|
638
|
+
data = cacheEntry.response;
|
|
639
|
+
} else {
|
|
640
|
+
let intentPromise = cacheEntry?.promise;
|
|
641
|
+
if (!intentPromise) {
|
|
642
|
+
intentPromise = requestIntent();
|
|
643
|
+
setIntentCacheEntry(idempotencyKey, { promise: intentPromise });
|
|
644
|
+
}
|
|
645
|
+
data = await intentPromise;
|
|
646
|
+
setIntentCacheEntry(idempotencyKey, { response: data, promise: void 0 });
|
|
553
647
|
}
|
|
554
648
|
if (requestId !== initRequestIdRef.current) {
|
|
555
649
|
return;
|
|
556
650
|
}
|
|
557
|
-
|
|
558
|
-
dispatch({ type: "INIT_ERROR", payload: error });
|
|
559
|
-
onError?.(error);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (!data) {
|
|
563
|
-
const noDataError = {
|
|
564
|
-
code: "INIT_FAILED",
|
|
565
|
-
message: "No data received from API",
|
|
566
|
-
recoverable: true
|
|
567
|
-
};
|
|
568
|
-
dispatch({ type: "INIT_ERROR", payload: noDataError });
|
|
569
|
-
onError?.(noDataError);
|
|
570
|
-
initializingRef.current = false;
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
const paymentIntent = mapToPaymentIntent(data, { ...config, reference });
|
|
651
|
+
const paymentIntent = mapToPaymentIntent(data, { ...config, reference, idempotencyKey });
|
|
574
652
|
dispatch({ type: "INIT_SUCCESS", payload: paymentIntent });
|
|
575
653
|
} catch (err) {
|
|
654
|
+
if (intentKey) {
|
|
655
|
+
clearIntentCacheEntry(intentKey);
|
|
656
|
+
}
|
|
576
657
|
if (requestId !== initRequestIdRef.current) {
|
|
577
658
|
return;
|
|
578
659
|
}
|
|
579
|
-
const error = {
|
|
660
|
+
const error = isPaymentError(err) ? err : {
|
|
580
661
|
code: "INIT_FAILED",
|
|
581
662
|
message: err instanceof Error ? err.message : "Failed to initialize checkout",
|
|
582
663
|
recoverable: true,
|
|
@@ -584,7 +665,6 @@ function useReevit(options) {
|
|
|
584
665
|
};
|
|
585
666
|
dispatch({ type: "INIT_ERROR", payload: error });
|
|
586
667
|
onError?.(error);
|
|
587
|
-
initializingRef.current = false;
|
|
588
668
|
}
|
|
589
669
|
},
|
|
590
670
|
[config, onError, apiBaseUrl]
|
|
@@ -665,7 +745,7 @@ function useReevit(options) {
|
|
|
665
745
|
} catch {
|
|
666
746
|
}
|
|
667
747
|
}
|
|
668
|
-
|
|
748
|
+
currentIntentKeyRef.current = null;
|
|
669
749
|
initRequestIdRef.current += 1;
|
|
670
750
|
dispatch({ type: "RESET" });
|
|
671
751
|
}, [state.paymentIntent, state.status]);
|