@usethrottle/checkout-react 1.4.1 → 2.0.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/dist/index.cjs +324 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +219 -3
- package/dist/index.d.ts +219 -3
- package/dist/index.js +325 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/dist/index.cjs
CHANGED
|
@@ -20,10 +20,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.tsx
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
CardNumberField: () => CardNumberField,
|
|
23
24
|
CheckoutEmbed: () => CheckoutEmbed,
|
|
25
|
+
ExpiryField: () => ExpiryField,
|
|
24
26
|
PaymentEmbed: () => PaymentEmbed,
|
|
27
|
+
PostalCodeField: () => PostalCodeField,
|
|
28
|
+
SecureCardProvider: () => SecureCardProvider,
|
|
29
|
+
SecurityCodeField: () => SecurityCodeField,
|
|
25
30
|
ThrottleCheckout: () => CheckoutEmbed,
|
|
26
31
|
useCartSession: () => useCartSession,
|
|
32
|
+
useSecureCard: () => useSecureCard,
|
|
27
33
|
useThrottleCheckout: () => useThrottleCheckout,
|
|
28
34
|
useThrottleEvents: () => useThrottleEvents
|
|
29
35
|
});
|
|
@@ -663,12 +669,330 @@ function useThrottleCheckout(options) {
|
|
|
663
669
|
reset
|
|
664
670
|
};
|
|
665
671
|
}
|
|
672
|
+
|
|
673
|
+
// src/useSecureCard.tsx
|
|
674
|
+
var import_react7 = require("react");
|
|
675
|
+
var import_secure_fields2 = require("@gr4vy/secure-fields");
|
|
676
|
+
|
|
677
|
+
// src/threeDSecure.ts
|
|
678
|
+
var import_secure_fields = require("@gr4vy/secure-fields");
|
|
679
|
+
function resolveChallenge({ instance, element, challengeWindowSize }) {
|
|
680
|
+
return new Promise((resolve) => {
|
|
681
|
+
const handleFinish = () => {
|
|
682
|
+
instance.removeEventListener?.(import_secure_fields.SecureFields.Events.THREE_DS_FINISH, handleFinish);
|
|
683
|
+
resolve();
|
|
684
|
+
};
|
|
685
|
+
instance.addEventListener(import_secure_fields.SecureFields.Events.THREE_DS_FINISH, handleFinish);
|
|
686
|
+
instance.addThreeDSecure(element, challengeWindowSize ? { challengeWindowSize } : void 0);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/useSecureCard.tsx
|
|
691
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
692
|
+
var SecureCardContext = (0, import_react7.createContext)(null);
|
|
693
|
+
var FIELDS_REQUIRED_TO_BE_READY = 3;
|
|
694
|
+
var DEFAULT_CHALLENGE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
695
|
+
function SecureCardProvider({
|
|
696
|
+
session,
|
|
697
|
+
checkoutSessionId,
|
|
698
|
+
children,
|
|
699
|
+
completeFn,
|
|
700
|
+
resolveChallengeFn,
|
|
701
|
+
challengeTimeoutMs = DEFAULT_CHALLENGE_TIMEOUT_MS,
|
|
702
|
+
apiBaseUrl,
|
|
703
|
+
onSuccess
|
|
704
|
+
}) {
|
|
705
|
+
const [status, setStatus] = (0, import_react7.useState)("idle");
|
|
706
|
+
const [error, setError] = (0, import_react7.useState)(null);
|
|
707
|
+
const [mountedCount, setMountedCount] = (0, import_react7.useState)(0);
|
|
708
|
+
const vaultWaiter = (0, import_react7.useRef)(null);
|
|
709
|
+
const threeDSElement = (0, import_react7.useRef)(null);
|
|
710
|
+
const threeDSOptions = (0, import_react7.useRef)(void 0);
|
|
711
|
+
const challengeWaiter = (0, import_react7.useRef)(null);
|
|
712
|
+
const { environment, sessionId } = session;
|
|
713
|
+
const providerInstanceId = session.providerId ?? session.gr4vyId ?? "";
|
|
714
|
+
const [instance, setInstance] = (0, import_react7.useState)(null);
|
|
715
|
+
const [threeDSMount, setThreeDSMount] = (0, import_react7.useState)(null);
|
|
716
|
+
(0, import_react7.useEffect)(() => {
|
|
717
|
+
const created = new import_secure_fields2.SecureFields({ gr4vyId: providerInstanceId, environment, sessionId });
|
|
718
|
+
setInstance(created);
|
|
719
|
+
setMountedCount(0);
|
|
720
|
+
setStatus((current) => current === "idle" || current === "ready" ? "mounting" : current);
|
|
721
|
+
return () => {
|
|
722
|
+
created.controller?.remove();
|
|
723
|
+
setInstance(null);
|
|
724
|
+
};
|
|
725
|
+
}, [providerInstanceId, environment, sessionId]);
|
|
726
|
+
(0, import_react7.useEffect)(() => {
|
|
727
|
+
if (!instance) return;
|
|
728
|
+
const handleSuccess = () => {
|
|
729
|
+
vaultWaiter.current?.resolve();
|
|
730
|
+
};
|
|
731
|
+
const handleFailure = (data) => {
|
|
732
|
+
vaultWaiter.current?.reject({
|
|
733
|
+
code: data?.code ?? "card_vault_failed",
|
|
734
|
+
message: data?.message ?? "The card could not be saved."
|
|
735
|
+
});
|
|
736
|
+
};
|
|
737
|
+
const handleThreeDSStart = () => setStatus("challenging");
|
|
738
|
+
const handleThreeDSFinish = () => {
|
|
739
|
+
if (!challengeWaiter.current) return;
|
|
740
|
+
setStatus((current) => current === "challenging" ? "vaulting" : current);
|
|
741
|
+
};
|
|
742
|
+
instance.addEventListener(import_secure_fields2.SecureFields.Events.CARD_VAULT_SUCCESS, handleSuccess);
|
|
743
|
+
instance.addEventListener(import_secure_fields2.SecureFields.Events.CARD_VAULT_FAILURE, handleFailure);
|
|
744
|
+
instance.addEventListener(import_secure_fields2.SecureFields.Events.THREE_DS_START, handleThreeDSStart);
|
|
745
|
+
instance.addEventListener(import_secure_fields2.SecureFields.Events.THREE_DS_FINISH, handleThreeDSFinish);
|
|
746
|
+
return () => {
|
|
747
|
+
instance.removeEventListener?.(import_secure_fields2.SecureFields.Events.CARD_VAULT_SUCCESS, handleSuccess);
|
|
748
|
+
instance.removeEventListener?.(import_secure_fields2.SecureFields.Events.CARD_VAULT_FAILURE, handleFailure);
|
|
749
|
+
instance.removeEventListener?.(import_secure_fields2.SecureFields.Events.THREE_DS_START, handleThreeDSStart);
|
|
750
|
+
instance.removeEventListener?.(import_secure_fields2.SecureFields.Events.THREE_DS_FINISH, handleThreeDSFinish);
|
|
751
|
+
};
|
|
752
|
+
}, [instance]);
|
|
753
|
+
const registerField = (0, import_react7.useCallback)(
|
|
754
|
+
(kind, element, options = {}) => {
|
|
755
|
+
if (!instance) return;
|
|
756
|
+
if (kind === "number") instance.addCardNumberField(element, options);
|
|
757
|
+
else if (kind === "expiry") instance.addExpiryDateField(element, options);
|
|
758
|
+
else if (kind === "cvv") instance.addSecurityCodeField(element, options);
|
|
759
|
+
else instance.addField(element, { type: "postalCode", ...options });
|
|
760
|
+
setMountedCount((n) => n + 1);
|
|
761
|
+
},
|
|
762
|
+
[instance]
|
|
763
|
+
);
|
|
764
|
+
(0, import_react7.useEffect)(() => {
|
|
765
|
+
if (mountedCount >= FIELDS_REQUIRED_TO_BE_READY) setStatus("ready");
|
|
766
|
+
}, [mountedCount]);
|
|
767
|
+
const registerThreeDSecure = (0, import_react7.useCallback)(
|
|
768
|
+
(element, options) => {
|
|
769
|
+
threeDSElement.current = element;
|
|
770
|
+
threeDSOptions.current = options;
|
|
771
|
+
setThreeDSMount(element);
|
|
772
|
+
},
|
|
773
|
+
[]
|
|
774
|
+
);
|
|
775
|
+
(0, import_react7.useEffect)(() => {
|
|
776
|
+
if (!instance || !threeDSMount) return;
|
|
777
|
+
try {
|
|
778
|
+
instance.addThreeDSecure(
|
|
779
|
+
threeDSMount,
|
|
780
|
+
threeDSOptions.current?.challengeWindowSize ? { challengeWindowSize: threeDSOptions.current.challengeWindowSize } : void 0
|
|
781
|
+
);
|
|
782
|
+
} catch (err) {
|
|
783
|
+
setError({
|
|
784
|
+
code: "three_ds_arm_failed",
|
|
785
|
+
message: err?.message ?? "Could not arm 3-D Secure."
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}, [instance, threeDSMount]);
|
|
789
|
+
const cancelChallenge = (0, import_react7.useCallback)(() => {
|
|
790
|
+
challengeWaiter.current?.reject({
|
|
791
|
+
code: "three_ds_abandoned",
|
|
792
|
+
message: "The 3-D Secure challenge was abandoned."
|
|
793
|
+
});
|
|
794
|
+
}, []);
|
|
795
|
+
const vault = (0, import_react7.useCallback)(() => {
|
|
796
|
+
if (!instance) {
|
|
797
|
+
return Promise.reject({
|
|
798
|
+
code: "not_ready",
|
|
799
|
+
message: "The card fields are not ready yet."
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
if (vaultWaiter.current) {
|
|
803
|
+
return Promise.reject({
|
|
804
|
+
code: "vault_in_progress",
|
|
805
|
+
message: "A card vault is already in progress."
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
setError(null);
|
|
809
|
+
setStatus("vaulting");
|
|
810
|
+
return new Promise((resolve, reject) => {
|
|
811
|
+
vaultWaiter.current = {
|
|
812
|
+
resolve: () => {
|
|
813
|
+
vaultWaiter.current = null;
|
|
814
|
+
resolve();
|
|
815
|
+
},
|
|
816
|
+
reject: (e) => {
|
|
817
|
+
vaultWaiter.current = null;
|
|
818
|
+
setError(e);
|
|
819
|
+
setStatus("error");
|
|
820
|
+
reject(e);
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
instance.submit();
|
|
824
|
+
});
|
|
825
|
+
}, [instance]);
|
|
826
|
+
const complete = (0, import_react7.useCallback)(async () => {
|
|
827
|
+
setStatus("authorizing");
|
|
828
|
+
const doComplete = completeFn ?? ((args) => defaultComplete({ ...args, apiBaseUrl }));
|
|
829
|
+
const doResolveChallenge = resolveChallengeFn ?? resolveChallenge;
|
|
830
|
+
try {
|
|
831
|
+
let res = await doComplete({ sessionId: checkoutSessionId, gr4vySessionId: session.sessionId });
|
|
832
|
+
if (res?.status === "requires_action") {
|
|
833
|
+
if (!threeDSElement.current) {
|
|
834
|
+
throw Object.assign(new Error("No 3-D Secure mount element registered (call registerThreeDSecure)."), {
|
|
835
|
+
code: "three_ds_element_missing"
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
if (!instance) {
|
|
839
|
+
throw Object.assign(new Error("The card fields are not ready yet."), { code: "not_ready" });
|
|
840
|
+
}
|
|
841
|
+
setStatus("challenging");
|
|
842
|
+
await new Promise((resolve, reject) => {
|
|
843
|
+
const timeoutId = setTimeout(() => {
|
|
844
|
+
challengeWaiter.current?.reject({
|
|
845
|
+
code: "three_ds_timeout",
|
|
846
|
+
message: "The 3-D Secure challenge timed out."
|
|
847
|
+
});
|
|
848
|
+
}, challengeTimeoutMs);
|
|
849
|
+
challengeWaiter.current = {
|
|
850
|
+
resolve: () => {
|
|
851
|
+
clearTimeout(timeoutId);
|
|
852
|
+
challengeWaiter.current = null;
|
|
853
|
+
resolve();
|
|
854
|
+
},
|
|
855
|
+
reject: (e) => {
|
|
856
|
+
clearTimeout(timeoutId);
|
|
857
|
+
challengeWaiter.current = null;
|
|
858
|
+
reject(Object.assign(new Error(e.message), { code: e.code }));
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
doResolveChallenge({
|
|
862
|
+
instance,
|
|
863
|
+
element: threeDSElement.current,
|
|
864
|
+
...threeDSOptions.current ?? {}
|
|
865
|
+
}).then(
|
|
866
|
+
() => challengeWaiter.current?.resolve(),
|
|
867
|
+
// `resolveChallenge` calls `instance.addThreeDSecure()` synchronously
|
|
868
|
+
// inside its Promise executor, so an SDK throw rejects here; a
|
|
869
|
+
// merchant-supplied `resolveChallengeFn` can reject too. Without this
|
|
870
|
+
// arm the only exit was `challengeTimeoutMs` — five minutes stuck at
|
|
871
|
+
// `status: 'challenging'`, plus an unhandled rejection. Same
|
|
872
|
+
// code-passthrough convention as `complete()`'s catch: keep a `code`
|
|
873
|
+
// the resolver supplied, otherwise report `three_ds_failed`.
|
|
874
|
+
(err) => challengeWaiter.current?.reject({
|
|
875
|
+
code: err?.code ?? "three_ds_failed",
|
|
876
|
+
message: err?.message ?? "The 3-D Secure challenge could not be completed."
|
|
877
|
+
})
|
|
878
|
+
);
|
|
879
|
+
});
|
|
880
|
+
setStatus("authorizing");
|
|
881
|
+
res = await doComplete({ sessionId: checkoutSessionId, gr4vySessionId: session.sessionId });
|
|
882
|
+
if (res?.status === "requires_action") {
|
|
883
|
+
throw Object.assign(new Error("The payment still requires authentication after the 3-D Secure challenge."), {
|
|
884
|
+
code: "three_ds_unresolved"
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
setStatus("complete");
|
|
889
|
+
onSuccess?.(res);
|
|
890
|
+
return res;
|
|
891
|
+
} catch (err) {
|
|
892
|
+
setError({
|
|
893
|
+
code: err?.code ?? "complete_failed",
|
|
894
|
+
message: err?.message ?? "Payment could not be completed."
|
|
895
|
+
});
|
|
896
|
+
setStatus("error");
|
|
897
|
+
throw err;
|
|
898
|
+
}
|
|
899
|
+
}, [completeFn, resolveChallengeFn, challengeTimeoutMs, apiBaseUrl, instance, checkoutSessionId, session.sessionId, onSuccess]);
|
|
900
|
+
const pay = (0, import_react7.useCallback)(async () => {
|
|
901
|
+
await vault();
|
|
902
|
+
return complete();
|
|
903
|
+
}, [vault, complete]);
|
|
904
|
+
const value = {
|
|
905
|
+
instance,
|
|
906
|
+
registerField,
|
|
907
|
+
registerThreeDSecure,
|
|
908
|
+
cancelChallenge,
|
|
909
|
+
status,
|
|
910
|
+
error,
|
|
911
|
+
// "The fields are mounted and nothing is in flight" — NOT `status ===
|
|
912
|
+
// 'ready'`. Deriving it from the status alone made a decline terminal: the
|
|
913
|
+
// only thing that ever sets `ready` is the mount-count effect, which does
|
|
914
|
+
// not re-run after `setStatus('error')`, so `ready` stayed false and the
|
|
915
|
+
// merchant's pay button stayed disabled. A declined buyer could not try
|
|
916
|
+
// another card without reloading the page — and a decline is the single
|
|
917
|
+
// most common failure at a checkout.
|
|
918
|
+
ready: mountedCount >= FIELDS_REQUIRED_TO_BE_READY && status !== "vaulting" && status !== "authorizing" && status !== "challenging" && status !== "complete",
|
|
919
|
+
pay,
|
|
920
|
+
vault,
|
|
921
|
+
complete
|
|
922
|
+
};
|
|
923
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SecureCardContext.Provider, { value, children });
|
|
924
|
+
}
|
|
925
|
+
async function defaultComplete({
|
|
926
|
+
sessionId,
|
|
927
|
+
gr4vySessionId,
|
|
928
|
+
apiBaseUrl
|
|
929
|
+
}) {
|
|
930
|
+
const res = await fetch(joinApiUrl(apiBaseUrl, `/api/v1/checkout-sessions/${sessionId}/complete`), {
|
|
931
|
+
method: "POST",
|
|
932
|
+
headers: { "Content-Type": "application/json" },
|
|
933
|
+
body: JSON.stringify({ paymentMethod: "embedded", processorCheckoutSessionId: gr4vySessionId })
|
|
934
|
+
});
|
|
935
|
+
const body = await res.json();
|
|
936
|
+
if (!res.ok) {
|
|
937
|
+
throw Object.assign(new Error(body?.error?.message ?? "Payment could not be completed."), {
|
|
938
|
+
code: body?.error?.code ?? "payment_failed"
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return body?.data ?? body;
|
|
942
|
+
}
|
|
943
|
+
function joinApiUrl(base, path) {
|
|
944
|
+
return `${base.replace(/\/+$/, "")}${path}`;
|
|
945
|
+
}
|
|
946
|
+
function useSecureCard() {
|
|
947
|
+
const ctx = (0, import_react7.useContext)(SecureCardContext);
|
|
948
|
+
if (!ctx) throw new Error("useSecureCard must be used inside <SecureCardProvider>");
|
|
949
|
+
return ctx;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/SecureCardFields.tsx
|
|
953
|
+
var import_react8 = require("react");
|
|
954
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
955
|
+
function useField(kind, options) {
|
|
956
|
+
const ctx = (0, import_react8.useContext)(SecureCardContext);
|
|
957
|
+
const hostRef = (0, import_react8.useRef)(null);
|
|
958
|
+
const instance = ctx?.instance ?? null;
|
|
959
|
+
(0, import_react8.useEffect)(() => {
|
|
960
|
+
const host = hostRef.current;
|
|
961
|
+
if (!host || !ctx || !instance) return;
|
|
962
|
+
const target = document.createElement("div");
|
|
963
|
+
target.style.height = "100%";
|
|
964
|
+
host.appendChild(target);
|
|
965
|
+
ctx.registerField(kind, target, options ?? {});
|
|
966
|
+
return () => {
|
|
967
|
+
while (host.firstChild) host.removeChild(host.firstChild);
|
|
968
|
+
};
|
|
969
|
+
}, [instance]);
|
|
970
|
+
return hostRef;
|
|
971
|
+
}
|
|
972
|
+
function CardNumberField({ className, style, options }) {
|
|
973
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: useField("number", options), className, style });
|
|
974
|
+
}
|
|
975
|
+
function ExpiryField({ className, style, options }) {
|
|
976
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: useField("expiry", options), className, style });
|
|
977
|
+
}
|
|
978
|
+
function SecurityCodeField({ className, style, options }) {
|
|
979
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: useField("cvv", options), className, style });
|
|
980
|
+
}
|
|
981
|
+
function PostalCodeField({ className, style, options }) {
|
|
982
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: useField("postalCode", options), className, style });
|
|
983
|
+
}
|
|
666
984
|
// Annotate the CommonJS export names for ESM import in node:
|
|
667
985
|
0 && (module.exports = {
|
|
986
|
+
CardNumberField,
|
|
668
987
|
CheckoutEmbed,
|
|
988
|
+
ExpiryField,
|
|
669
989
|
PaymentEmbed,
|
|
990
|
+
PostalCodeField,
|
|
991
|
+
SecureCardProvider,
|
|
992
|
+
SecurityCodeField,
|
|
670
993
|
ThrottleCheckout,
|
|
671
994
|
useCartSession,
|
|
995
|
+
useSecureCard,
|
|
672
996
|
useThrottleCheckout,
|
|
673
997
|
useThrottleEvents
|
|
674
998
|
});
|