@edge-markets/connect-link 1.0.3 → 1.3.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.mjs CHANGED
@@ -155,7 +155,7 @@ var PopupManager = class {
155
155
  * Renders a branded loading state in the popup.
156
156
  *
157
157
  * This shows immediately while we do async work (PKCE, URL building).
158
- * Creates a polished, professional feel like Plaid Link.
158
+ * Creates a polished, professional feel for the authentication flow.
159
159
  */
160
160
  renderLoadingState() {
161
161
  if (!this.popup) return;
@@ -562,9 +562,601 @@ var EdgeLink = class {
562
562
  }
563
563
  };
564
564
 
565
+ // src/hooks.ts
566
+ import { useEffect, useRef, useCallback, useState } from "react";
567
+ function useEdgeLink(config) {
568
+ const linkRef = useRef(null);
569
+ const [ready, setReady] = useState(false);
570
+ const [error, setError] = useState(null);
571
+ const [isOpen, setIsOpen] = useState(false);
572
+ const configRef = useRef(config);
573
+ configRef.current = config;
574
+ useEffect(() => {
575
+ try {
576
+ linkRef.current = new EdgeLink({
577
+ ...configRef.current,
578
+ onSuccess: (result) => {
579
+ setIsOpen(false);
580
+ configRef.current.onSuccess?.(result);
581
+ },
582
+ onExit: (metadata) => {
583
+ setIsOpen(false);
584
+ configRef.current.onExit?.(metadata);
585
+ },
586
+ onEvent: (event) => {
587
+ if (event.eventName === "OPEN") setIsOpen(true);
588
+ if (event.eventName === "CLOSE") setIsOpen(false);
589
+ configRef.current.onEvent?.(event);
590
+ }
591
+ });
592
+ setReady(true);
593
+ setError(null);
594
+ } catch (err) {
595
+ setError(err instanceof Error ? err : new Error("Failed to initialize EdgeLink"));
596
+ setReady(false);
597
+ }
598
+ return () => {
599
+ linkRef.current?.destroy();
600
+ linkRef.current = null;
601
+ setReady(false);
602
+ };
603
+ }, [config.clientId, config.environment]);
604
+ const open = useCallback((options) => {
605
+ if (!linkRef.current) {
606
+ setError(new Error("EdgeLink not initialized"));
607
+ return;
608
+ }
609
+ try {
610
+ linkRef.current.open(options);
611
+ } catch (err) {
612
+ setError(err instanceof Error ? err : new Error("Failed to open EdgeLink"));
613
+ }
614
+ }, []);
615
+ return { open, ready, error, isOpen };
616
+ }
617
+
618
+ // src/edge-transfer-verify.ts
619
+ import {
620
+ EdgePopupBlockedError as EdgePopupBlockedError2
621
+ } from "@edge-markets/connect";
622
+
623
+ // src/iframe-manager.ts
624
+ var DEFAULT_TITLE = "EDGE Connect Transfer Verification";
625
+ var IFRAME_SANDBOX = "allow-scripts allow-same-origin allow-forms";
626
+ var IframeManager = class {
627
+ constructor() {
628
+ this.iframe = null;
629
+ this.loadingEl = null;
630
+ this.container = null;
631
+ this.callbacks = {};
632
+ }
633
+ /**
634
+ * Creates and mounts the iframe into the provided container.
635
+ *
636
+ * Displays a branded loading state while the iframe loads,
637
+ * then swaps to the iframe once it's ready.
638
+ *
639
+ * @param config - Iframe configuration
640
+ * @param callbacks - Optional lifecycle callbacks
641
+ */
642
+ mount(config, callbacks = {}) {
643
+ this.destroy();
644
+ this.container = config.container;
645
+ this.callbacks = callbacks;
646
+ this.renderLoadingState(config.container);
647
+ this.iframe = document.createElement("iframe");
648
+ this.iframe.src = config.url;
649
+ this.iframe.width = config.width || "100%";
650
+ this.iframe.height = config.height || "100%";
651
+ this.iframe.title = config.title || DEFAULT_TITLE;
652
+ this.iframe.setAttribute("sandbox", IFRAME_SANDBOX);
653
+ this.iframe.setAttribute("allow", "clipboard-write");
654
+ this.iframe.style.border = "none";
655
+ this.iframe.style.display = "none";
656
+ this.iframe.style.width = config.width || "100%";
657
+ this.iframe.style.height = config.height || "100%";
658
+ this.iframe.style.minHeight = "400px";
659
+ this.iframe.style.borderRadius = "8px";
660
+ this.iframe.addEventListener("load", () => {
661
+ this.hideLoadingState();
662
+ this.callbacks.onLoad?.();
663
+ });
664
+ this.iframe.addEventListener("error", () => {
665
+ this.hideLoadingState();
666
+ const error = new Error("Failed to load transfer verification iframe");
667
+ this.callbacks.onError?.(error);
668
+ });
669
+ config.container.appendChild(this.iframe);
670
+ }
671
+ /**
672
+ * Gets the iframe's content window for postMessage communication.
673
+ *
674
+ * @returns The iframe's contentWindow, or null if not mounted
675
+ */
676
+ getContentWindow() {
677
+ return this.iframe?.contentWindow ?? null;
678
+ }
679
+ /**
680
+ * Checks if the iframe is currently mounted in the DOM.
681
+ */
682
+ isMounted() {
683
+ return this.iframe !== null && this.iframe.isConnected;
684
+ }
685
+ /**
686
+ * Removes the iframe and loading state from the DOM.
687
+ * Cleans up all references for garbage collection.
688
+ */
689
+ destroy() {
690
+ if (this.loadingEl && this.loadingEl.parentNode) {
691
+ this.loadingEl.parentNode.removeChild(this.loadingEl);
692
+ }
693
+ this.loadingEl = null;
694
+ if (this.iframe && this.iframe.parentNode) {
695
+ this.iframe.parentNode.removeChild(this.iframe);
696
+ }
697
+ this.iframe = null;
698
+ this.container = null;
699
+ this.callbacks = {};
700
+ }
701
+ // ===========================================================================
702
+ // PRIVATE METHODS
703
+ // ===========================================================================
704
+ /**
705
+ * Renders a branded loading state in the container.
706
+ *
707
+ * This displays immediately while the iframe loads the verification URL.
708
+ * Matches the visual style of the PopupManager loading state.
709
+ */
710
+ renderLoadingState(container) {
711
+ this.loadingEl = document.createElement("div");
712
+ this.loadingEl.setAttribute("data-edge-loading", "true");
713
+ this.loadingEl.innerHTML = `
714
+ <div style="
715
+ display: flex;
716
+ align-items: center;
717
+ justify-content: center;
718
+ min-height: 400px;
719
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
720
+ border-radius: 8px;
721
+ color: white;
722
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
723
+ ">
724
+ <div style="text-align: center; padding: 40px;">
725
+ <div style="
726
+ width: 64px;
727
+ height: 64px;
728
+ margin: 0 auto 24px;
729
+ background: linear-gradient(135deg, #00d4aa 0%, #00a080 100%);
730
+ border-radius: 16px;
731
+ display: flex;
732
+ align-items: center;
733
+ justify-content: center;
734
+ font-size: 28px;
735
+ font-weight: bold;
736
+ box-shadow: 0 8px 32px rgba(0, 212, 170, 0.3);
737
+ ">E</div>
738
+ <div style="
739
+ width: 40px;
740
+ height: 40px;
741
+ border: 3px solid rgba(255, 255, 255, 0.2);
742
+ border-top-color: #00d4aa;
743
+ border-radius: 50%;
744
+ animation: edge-spin 1s linear infinite;
745
+ margin: 0 auto 24px;
746
+ "></div>
747
+ <h1 style="
748
+ font-size: 22px;
749
+ font-weight: 600;
750
+ margin-bottom: 8px;
751
+ letter-spacing: -0.5px;
752
+ ">Transfer Verification</h1>
753
+ <p style="
754
+ font-size: 15px;
755
+ color: rgba(255, 255, 255, 0.7);
756
+ line-height: 1.5;
757
+ ">Loading secure verification...</p>
758
+ <div style="
759
+ display: flex;
760
+ align-items: center;
761
+ justify-content: center;
762
+ gap: 6px;
763
+ margin-top: 32px;
764
+ font-size: 13px;
765
+ color: rgba(255, 255, 255, 0.5);
766
+ ">
767
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
768
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
769
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
770
+ </svg>
771
+ <span>256-bit encryption</span>
772
+ </div>
773
+ </div>
774
+ </div>
775
+ <style>
776
+ @keyframes edge-spin {
777
+ to { transform: rotate(360deg); }
778
+ }
779
+ </style>
780
+ `;
781
+ container.appendChild(this.loadingEl);
782
+ }
783
+ /**
784
+ * Hides the loading state and shows the iframe.
785
+ */
786
+ hideLoadingState() {
787
+ if (this.loadingEl && this.loadingEl.parentNode) {
788
+ this.loadingEl.parentNode.removeChild(this.loadingEl);
789
+ this.loadingEl = null;
790
+ }
791
+ if (this.iframe) {
792
+ this.iframe.style.display = "block";
793
+ }
794
+ }
795
+ };
796
+
797
+ // src/edge-transfer-verify.ts
798
+ function generateNonce() {
799
+ const array = new Uint8Array(32);
800
+ crypto.getRandomValues(array);
801
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
802
+ }
803
+ function isValidTransferVerifyMessage(data) {
804
+ if (!data || typeof data !== "object") return false;
805
+ const msg = data;
806
+ const validTypes = [
807
+ "edge:transfer-verify:success",
808
+ "edge:transfer-verify:error",
809
+ "edge:transfer-verify:cancel",
810
+ "edge:transfer-verify:expired",
811
+ "edge:transfer-verify:loaded"
812
+ ];
813
+ if (typeof msg.type !== "string" || !validTypes.includes(msg.type)) {
814
+ return false;
815
+ }
816
+ if (typeof msg.sessionId !== "string" || msg.sessionId.length === 0) return false;
817
+ if (typeof msg.transferId !== "string" || msg.transferId.length === 0) return false;
818
+ if (typeof msg.nonce !== "string" || msg.nonce.length === 0) return false;
819
+ if (typeof msg.timestamp !== "string" || msg.timestamp.length === 0) return false;
820
+ if (msg.error !== void 0 && typeof msg.error !== "string") return false;
821
+ return true;
822
+ }
823
+ var EdgeTransferVerify = class {
824
+ /**
825
+ * Creates a new EdgeTransferVerify instance.
826
+ *
827
+ * @param config - Configuration options
828
+ * @throws Error if required config is missing or invalid
829
+ */
830
+ constructor(config) {
831
+ this.popupManager = null;
832
+ this.iframeManager = null;
833
+ this.messageHandler = null;
834
+ this.isDestroyed = false;
835
+ this.isOpened = false;
836
+ if (!config.verificationUrl) {
837
+ throw new Error("EdgeTransferVerify: verificationUrl is required");
838
+ }
839
+ if (!config.sessionId) {
840
+ throw new Error("EdgeTransferVerify: sessionId is required");
841
+ }
842
+ const mode = config.mode || "iframe";
843
+ if (mode === "iframe" && !config.container) {
844
+ throw new Error("EdgeTransferVerify: container is required for iframe mode");
845
+ }
846
+ this.config = config;
847
+ this.mode = mode;
848
+ this.nonce = config.nonce || generateNonce();
849
+ this.expectedOrigin = new URL(config.verificationUrl).origin;
850
+ this.setupMessageListener();
851
+ }
852
+ // ===========================================================================
853
+ // PUBLIC API
854
+ // ===========================================================================
855
+ /**
856
+ * Opens the transfer verification UI.
857
+ *
858
+ * In iframe mode, this mounts the iframe into the configured container.
859
+ * In popup mode, this opens a popup window.
860
+ *
861
+ * **Popup mode MUST be called directly from a user click handler!**
862
+ * Iframe mode can be called from any context.
863
+ *
864
+ * @throws EdgePopupBlockedError if popup mode and popup is blocked
865
+ * @throws Error if the instance has been destroyed or is already open
866
+ *
867
+ * @example
868
+ * ```typescript
869
+ * // Iframe mode — can call anywhere
870
+ * verify.open()
871
+ *
872
+ * // Popup mode — must call from click handler
873
+ * button.onclick = () => verify.open()
874
+ * ```
875
+ */
876
+ open() {
877
+ if (this.isDestroyed) {
878
+ throw new Error("EdgeTransferVerify: Cannot open - instance has been destroyed");
879
+ }
880
+ if (this.isOpened) {
881
+ if (this.mode === "popup" && this.popupManager?.isOpen()) {
882
+ this.popupManager.focus();
883
+ return;
884
+ }
885
+ if (this.mode === "iframe" && this.iframeManager?.isMounted()) {
886
+ return;
887
+ }
888
+ }
889
+ const url = this.buildVerificationUrl();
890
+ if (this.mode === "popup") {
891
+ this.openPopup(url);
892
+ } else {
893
+ this.openIframe(url);
894
+ }
895
+ this.isOpened = true;
896
+ }
897
+ /**
898
+ * Closes the verification UI.
899
+ *
900
+ * In popup mode, closes the popup window.
901
+ * In iframe mode, removes the iframe from the container.
902
+ * Does not trigger any callbacks.
903
+ */
904
+ close() {
905
+ if (this.mode === "popup") {
906
+ this.popupManager?.close();
907
+ this.popupManager = null;
908
+ } else {
909
+ this.iframeManager?.destroy();
910
+ this.iframeManager = null;
911
+ }
912
+ this.isOpened = false;
913
+ }
914
+ /**
915
+ * Destroys the EdgeTransferVerify instance.
916
+ *
917
+ * Closes the UI, removes the postMessage listener, and cleans up all resources.
918
+ * After destroy(), the instance cannot be reused.
919
+ *
920
+ * @example
921
+ * ```typescript
922
+ * // React cleanup
923
+ * useEffect(() => {
924
+ * const verify = new EdgeTransferVerify({ ... })
925
+ * verify.open()
926
+ * return () => verify.destroy()
927
+ * }, [])
928
+ * ```
929
+ */
930
+ destroy() {
931
+ this.isDestroyed = true;
932
+ this.close();
933
+ this.removeMessageListener();
934
+ }
935
+ /**
936
+ * Checks if the verification UI is currently displayed.
937
+ */
938
+ isOpen() {
939
+ if (this.mode === "popup") {
940
+ return this.popupManager?.isOpen() ?? false;
941
+ }
942
+ return this.iframeManager?.isMounted() ?? false;
943
+ }
944
+ /**
945
+ * Returns the nonce being used for this verification session.
946
+ * Useful for debugging and logging.
947
+ */
948
+ getNonce() {
949
+ return this.nonce;
950
+ }
951
+ // ===========================================================================
952
+ // PRIVATE — LAUNCH METHODS
953
+ // ===========================================================================
954
+ /**
955
+ * Builds the verification URL with nonce and parent origin parameters.
956
+ *
957
+ * Appends:
958
+ * - `nonce` — for replay protection / message correlation
959
+ * - `origin` — so the verification page knows where to postMessage back to
960
+ */
961
+ buildVerificationUrl() {
962
+ const url = new URL(this.config.verificationUrl);
963
+ url.searchParams.set("nonce", this.nonce);
964
+ url.searchParams.set("origin", window.location.origin);
965
+ return url.toString();
966
+ }
967
+ /**
968
+ * Opens the verification UI in a popup window.
969
+ *
970
+ * Uses PopupManager from the existing EdgeLink infrastructure.
971
+ * The popup opens immediately (preserving user gesture) and navigates
972
+ * to the verification URL.
973
+ *
974
+ * @param url - The verification URL to load
975
+ * @throws EdgePopupBlockedError if the popup is blocked
976
+ */
977
+ openPopup(url) {
978
+ this.popupManager = new PopupManager();
979
+ const win = this.popupManager.open({
980
+ onUserClose: () => this.handleUserClose()
981
+ });
982
+ if (!win) {
983
+ this.popupManager = null;
984
+ this.config.onError?.({
985
+ type: "edge:transfer-verify:error",
986
+ sessionId: this.config.sessionId,
987
+ transferId: "",
988
+ nonce: this.nonce,
989
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
990
+ error: "Popup was blocked by the browser. Please allow popups for this site."
991
+ });
992
+ throw new EdgePopupBlockedError2();
993
+ }
994
+ this.popupManager.navigateTo(url);
995
+ }
996
+ /**
997
+ * Opens the verification UI in an iframe.
998
+ *
999
+ * Uses IframeManager to create and mount the iframe into the configured container.
1000
+ *
1001
+ * @param url - The verification URL to load
1002
+ */
1003
+ openIframe(url) {
1004
+ this.iframeManager = new IframeManager();
1005
+ this.iframeManager.mount(
1006
+ {
1007
+ url,
1008
+ container: this.config.container,
1009
+ title: "EDGE Connect Transfer Verification"
1010
+ },
1011
+ {
1012
+ onError: (error) => {
1013
+ this.config.onError?.({
1014
+ type: "edge:transfer-verify:error",
1015
+ sessionId: this.config.sessionId,
1016
+ transferId: "",
1017
+ nonce: this.nonce,
1018
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1019
+ error: error.message
1020
+ });
1021
+ }
1022
+ }
1023
+ );
1024
+ }
1025
+ // ===========================================================================
1026
+ // PRIVATE — MESSAGE HANDLING
1027
+ // ===========================================================================
1028
+ /**
1029
+ * Sets up the postMessage listener for receiving events from the verification page.
1030
+ *
1031
+ * SECURITY: Every incoming message is validated for:
1032
+ * 1. Origin matches the expected EDGE verification origin
1033
+ * 2. Message schema matches TransferVerifyMessage
1034
+ * 3. Nonce matches the nonce we sent
1035
+ * 4. SessionId matches the session we opened
1036
+ */
1037
+ setupMessageListener() {
1038
+ this.messageHandler = (event) => {
1039
+ if (event.origin !== this.expectedOrigin) {
1040
+ return;
1041
+ }
1042
+ const data = event.data;
1043
+ if (!isValidTransferVerifyMessage(data)) {
1044
+ return;
1045
+ }
1046
+ if (data.nonce !== this.nonce) {
1047
+ return;
1048
+ }
1049
+ if (data.sessionId !== this.config.sessionId) {
1050
+ return;
1051
+ }
1052
+ const verifiedEvent = {
1053
+ type: data.type,
1054
+ sessionId: data.sessionId,
1055
+ transferId: data.transferId,
1056
+ nonce: data.nonce,
1057
+ timestamp: data.timestamp,
1058
+ error: data.error
1059
+ };
1060
+ this.config.onEvent?.(verifiedEvent);
1061
+ switch (data.type) {
1062
+ case "edge:transfer-verify:success":
1063
+ this.handleSuccess(verifiedEvent);
1064
+ break;
1065
+ case "edge:transfer-verify:error":
1066
+ this.handleError(verifiedEvent);
1067
+ break;
1068
+ case "edge:transfer-verify:cancel":
1069
+ this.handleCancel(verifiedEvent);
1070
+ break;
1071
+ case "edge:transfer-verify:expired":
1072
+ this.handleExpired(verifiedEvent);
1073
+ break;
1074
+ case "edge:transfer-verify:loaded":
1075
+ this.handleLoaded(verifiedEvent);
1076
+ break;
1077
+ }
1078
+ };
1079
+ window.addEventListener("message", this.messageHandler);
1080
+ }
1081
+ /**
1082
+ * Removes the postMessage listener.
1083
+ */
1084
+ removeMessageListener() {
1085
+ if (this.messageHandler) {
1086
+ window.removeEventListener("message", this.messageHandler);
1087
+ this.messageHandler = null;
1088
+ }
1089
+ }
1090
+ // ===========================================================================
1091
+ // PRIVATE — EVENT HANDLERS
1092
+ // ===========================================================================
1093
+ /**
1094
+ * Handles a successful verification event.
1095
+ *
1096
+ * Closes the UI and notifies the consumer.
1097
+ */
1098
+ handleSuccess(event) {
1099
+ this.close();
1100
+ this.config.onSuccess?.(event);
1101
+ }
1102
+ /**
1103
+ * Handles a verification error event.
1104
+ *
1105
+ * Does NOT close the UI — the verification page may allow retry.
1106
+ * The consumer can call close() or destroy() if they want to dismiss.
1107
+ */
1108
+ handleError(event) {
1109
+ this.config.onError?.(event);
1110
+ }
1111
+ /**
1112
+ * Handles a cancellation event from the user.
1113
+ *
1114
+ * Closes the UI and notifies the consumer.
1115
+ */
1116
+ handleCancel(event) {
1117
+ this.close();
1118
+ this.config.onCancel?.(event);
1119
+ }
1120
+ /**
1121
+ * Handles a session/OTP expiration event.
1122
+ *
1123
+ * Closes the UI and notifies the consumer.
1124
+ */
1125
+ handleExpired(event) {
1126
+ this.close();
1127
+ this.config.onExpired?.(event);
1128
+ }
1129
+ /**
1130
+ * Handles the loaded event from the verification page.
1131
+ *
1132
+ * The verification UI is ready for user interaction.
1133
+ */
1134
+ handleLoaded(event) {
1135
+ this.config.onLoaded?.(event);
1136
+ }
1137
+ /**
1138
+ * Handles user closing the popup manually (popup mode only).
1139
+ *
1140
+ * Treated as a cancellation — we fire onCancel with a synthetic event.
1141
+ */
1142
+ handleUserClose() {
1143
+ this.isOpened = false;
1144
+ this.popupManager = null;
1145
+ const syntheticEvent = {
1146
+ type: "edge:transfer-verify:cancel",
1147
+ sessionId: this.config.sessionId,
1148
+ transferId: "",
1149
+ nonce: this.nonce,
1150
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1151
+ };
1152
+ this.config.onEvent?.(syntheticEvent);
1153
+ this.config.onCancel?.(syntheticEvent);
1154
+ }
1155
+ };
1156
+
565
1157
  // src/index.ts
566
1158
  import {
567
- EdgePopupBlockedError as EdgePopupBlockedError2,
1159
+ EdgePopupBlockedError as EdgePopupBlockedError3,
568
1160
  EdgeStateMismatchError as EdgeStateMismatchError2,
569
1161
  EdgeError,
570
1162
  isEdgeError
@@ -578,10 +1170,13 @@ export {
578
1170
  EDGE_SCOPES,
579
1171
  EdgeError,
580
1172
  EdgeLink,
581
- EdgePopupBlockedError2 as EdgePopupBlockedError,
1173
+ EdgePopupBlockedError3 as EdgePopupBlockedError,
582
1174
  EdgeStateMismatchError2 as EdgeStateMismatchError,
1175
+ EdgeTransferVerify,
1176
+ IframeManager,
583
1177
  assertCryptoAvailable,
584
1178
  generatePKCE,
585
1179
  generateState,
586
- isEdgeError
1180
+ isEdgeError,
1181
+ useEdgeLink
587
1182
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edge-markets/connect-link",
3
- "version": "1.0.3",
3
+ "version": "1.3.0",
4
4
  "description": "Browser SDK for EDGE Connect popup authentication",
5
5
  "author": "EdgeBoost",
6
6
  "license": "MIT",
@@ -20,17 +20,19 @@
20
20
  }
21
21
  }
22
22
  },
23
- "scripts": {
24
- "build": "tsup src/index.ts --format cjs,esm --dts --clean",
25
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
26
- "typecheck": "tsc --noEmit",
27
- "test": "vitest --passWithNoTests",
28
- "test:run": "vitest run --passWithNoTests"
29
- },
30
23
  "dependencies": {
31
- "@edge-markets/connect": "^1.0.0"
24
+ "@edge-markets/connect": "1.3.0"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=17.0.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "react": {
31
+ "optional": true
32
+ }
32
33
  },
33
34
  "devDependencies": {
35
+ "@types/react": "^18.2.0",
34
36
  "tsup": "^8.0.0",
35
37
  "typescript": "^5.3.0",
36
38
  "vitest": "^1.0.0"
@@ -46,7 +48,8 @@
46
48
  "oauth",
47
49
  "popup",
48
50
  "browser",
49
- "fintech"
51
+ "fintech",
52
+ "react"
50
53
  ],
51
54
  "repository": {
52
55
  "type": "git",
@@ -56,7 +59,12 @@
56
59
  "engines": {
57
60
  "node": ">=18"
58
61
  },
59
- "browser": "./dist/index.mjs"
60
- }
61
-
62
-
62
+ "browser": "./dist/index.mjs",
63
+ "scripts": {
64
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
65
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
66
+ "typecheck": "tsc --noEmit",
67
+ "test": "vitest --passWithNoTests",
68
+ "test:run": "vitest run --passWithNoTests"
69
+ }
70
+ }