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