@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 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
  });