@abstraxn/signer-react 3.3.3 → 3.3.5

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/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.3.4] - 2026-08-31
9
+
10
+ ### Fixed
11
+ - **Passkey export/sign 401** — via `@abstraxn/signer-core@2.1.4`, passkey login
12
+ registers the IndexedDB session key on Turnkey after login (fixes
13
+ `PUBLIC_KEY_NOT_FOUND` on export, sign, and proxy).
14
+
15
+ ### Changed
16
+ - Bumped `@abstraxn/signer-core` to `2.1.4`.
17
+
18
+ ### Notes
19
+ - Deploy **wallet-service** with `POST /auth/passkey/register-session` before upgrading.
20
+ - Users on passkey must **log out and log in again** after upgrade.
21
+
8
22
  ## [3.3.3] - 2026-07-09
9
23
 
10
24
  ### Changed
@@ -12,6 +12,7 @@ import { ExternalWalletButtons, resetExternalWalletViewState, } from "../../Exte
12
12
  import { EVM_CHAINS, SOLANA_CHAINS, getChainById, toCoreChain, } from "../../chains";
13
13
  import { AbstraxnContext } from "./context";
14
14
  import { isLinkAuthCallback } from "./utils";
15
+ import { hasActiveWalletConnectSession, hasPendingWalletConnectApproval, isWalletConnectConnector, patchWalletConnectProviderForMobile, resumeWalletConnectRelayer, subscribeWalletConnectForegroundResume, } from "../../walletConnectMobile";
15
16
  /** Syncs wagmi disconnect to external wallet state; only rendered when wagmi is available (under WagmiProvider). */
16
17
  function WagmiConnectionEffectSync({ onDisconnect, }) {
17
18
  useConnectionEffect({ onDisconnect });
@@ -45,6 +46,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
45
46
  const [emailForOtp, setEmailForOtpState] = useState("");
46
47
  const explicitConnectionRef = useRef(false);
47
48
  const autoDisconnectHandledRef = useRef(false);
49
+ const wcProviderRef = useRef(null);
48
50
  useEffect(() => {
49
51
  isExternalWalletConnectedRef.current = isExternalWalletConnected;
50
52
  }, [isExternalWalletConnected]);
@@ -2555,6 +2557,14 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
2555
2557
  if (chainTypePref === "solana" && solana?.wallet?.connected) {
2556
2558
  return;
2557
2559
  }
2560
+ // Mobile WalletConnect: leaving the browser for MetaMask drops the relay
2561
+ // socket, which can make wagmi look disconnected even though the session
2562
+ // is still valid. Keep UI state and resume the relay instead of resetting.
2563
+ if (!wagmiAccount.isConnected &&
2564
+ hasActiveWalletConnectSession(wcProviderRef.current)) {
2565
+ void resumeWalletConnectRelayer(wcProviderRef.current);
2566
+ return;
2567
+ }
2558
2568
  // Prevent auto-connect (but allow restoration from persistence)
2559
2569
  // Only auto-disconnect if:
2560
2570
  // 1. wagmiAccount is connected
@@ -2664,6 +2674,8 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
2664
2674
  // Close WalletConnect QR modal when connection detected via wagmi (e.g. Next.js hydration)
2665
2675
  if (typeof document !== "undefined") {
2666
2676
  const closeWcModalDom = () => {
2677
+ if (hasPendingWalletConnectApproval(wcProviderRef.current))
2678
+ return;
2667
2679
  ["w3m-modal", "w3m-modal-backdrop", "[data-w3m-modal]", ".w3m-modal", ".w3m-modal-backdrop", "[class*='w3m-modal']", "[class*='walletconnect-qrcode']"].forEach((sel) => {
2668
2680
  try {
2669
2681
  const el = document.querySelector(sel);
@@ -2931,9 +2943,15 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
2931
2943
  connectorName.includes("walletconnect");
2932
2944
  if (isWalletConnectConnector) {
2933
2945
  const closeWalletConnectModal = async () => {
2946
+ if (hasPendingWalletConnectApproval(wcProviderRef.current))
2947
+ return;
2934
2948
  try {
2935
2949
  if (typeof connector.getProvider === "function") {
2936
2950
  const provider = await connector.getProvider();
2951
+ if (provider) {
2952
+ wcProviderRef.current = provider;
2953
+ patchWalletConnectProviderForMobile(provider);
2954
+ }
2937
2955
  if (provider?.modal?.close) {
2938
2956
  provider.modal.close();
2939
2957
  }
@@ -3113,6 +3131,11 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3113
3131
  const handleWagmiDisconnect = useCallback(() => {
3114
3132
  if (!externalWalletsEnabledRef.current)
3115
3133
  return;
3134
+ // Transport drop (mobile deeplink) is not a session disconnect.
3135
+ if (hasActiveWalletConnectSession(wcProviderRef.current)) {
3136
+ void resumeWalletConnectRelayer(wcProviderRef.current);
3137
+ return;
3138
+ }
3116
3139
  setIsExternalWalletConnected(false);
3117
3140
  setExternalWalletAddress(null);
3118
3141
  setExternalWalletChainId(null);
@@ -3152,6 +3175,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3152
3175
  }
3153
3176
  const connector = wagmiAccount.connector;
3154
3177
  let mounted = true;
3178
+ let unsubscribeForeground;
3155
3179
  const setup = async () => {
3156
3180
  try {
3157
3181
  const getProvider = connector.getProvider ?? connector.getEthereumProvider;
@@ -3160,15 +3184,38 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3160
3184
  const p = await getProvider();
3161
3185
  if (!mounted || !p)
3162
3186
  return;
3187
+ const isWc = isWalletConnectConnector(connector);
3188
+ if (isWc) {
3189
+ wcProviderRef.current = p;
3190
+ patchWalletConnectProviderForMobile(p);
3191
+ unsubscribeForeground = subscribeWalletConnectForegroundResume(p);
3192
+ if (!mounted) {
3193
+ unsubscribeForeground();
3194
+ unsubscribeForeground = undefined;
3195
+ wcProviderRef.current = null;
3196
+ return;
3197
+ }
3198
+ }
3199
+ else {
3200
+ wcProviderRef.current = null;
3201
+ }
3163
3202
  const onDisconnectFromProvider = () => {
3164
3203
  if (!isExternalWalletConnectedRef.current)
3165
3204
  return;
3205
+ if (hasActiveWalletConnectSession(p)) {
3206
+ void resumeWalletConnectRelayer(p);
3207
+ return;
3208
+ }
3166
3209
  handleWagmiDisconnect();
3167
3210
  };
3168
3211
  const onAccountsChanged = (accounts) => {
3169
3212
  const list = Array.isArray(accounts) ? accounts : [];
3170
- if (list.length === 0)
3213
+ if (list.length === 0) {
3214
+ // Empty accounts during mobile resume is often a transport blip.
3215
+ if (hasActiveWalletConnectSession(p))
3216
+ return;
3171
3217
  onDisconnectFromProvider();
3218
+ }
3172
3219
  };
3173
3220
  p.on?.("accountsChanged", onAccountsChanged);
3174
3221
  p.on?.("disconnect", onDisconnectFromProvider);
@@ -3188,6 +3235,8 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3188
3235
  });
3189
3236
  return () => {
3190
3237
  mounted = false;
3238
+ unsubscribeForeground?.();
3239
+ wcProviderRef.current = null;
3191
3240
  const cleanup = providerListenerCleanupRef.current;
3192
3241
  providerListenerCleanupRef.current = null;
3193
3242
  if (typeof cleanup === "function")
@@ -77,15 +77,20 @@ export function createWagmiConfig(chains, walletConnectProjectId, enabledConnect
77
77
  console.warn("WalletConnect project ID is required for WalletConnect connector. Skipping...");
78
78
  }
79
79
  else {
80
+ const dappUrl = typeof window !== "undefined"
81
+ ? window.location.origin
82
+ : "https://abstraxn.com";
80
83
  connectors.push(walletConnect({
81
84
  projectId: walletConnectProjectId,
82
85
  metadata: {
83
86
  name: "Abstraxn",
84
87
  description: "Abstraxn Wallet",
85
- url: typeof window !== "undefined"
86
- ? window.location.origin
87
- : "https://abstraxn.com",
88
+ url: dappUrl,
88
89
  icons: ["https://avatars.githubusercontent.com/u/37784886"],
90
+ // Helps mobile wallets (MetaMask) return to this dapp after approve/reject.
91
+ redirect: {
92
+ universal: dappUrl,
93
+ },
89
94
  },
90
95
  showQrModal: true,
91
96
  qrModalOptions: {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Mobile WalletConnect helpers.
3
+ *
4
+ * On iOS/Android the browser suspends JS and drops the WalletConnect relay
5
+ * WebSocket when the user is sent to MetaMask (or another wallet) via deeplink.
6
+ * The wallet still approves/rejects, but the in-flight JSON-RPC promise never
7
+ * settles unless the relay is resumed when the dapp becomes visible again.
8
+ */
9
+ /** Selectors used to close/restore Reown AppKit + WalletConnect modal DOM. */
10
+ export declare const WALLETCONNECT_MODAL_SELECTORS: string[];
11
+ export declare function isWalletConnectConnector(connector: {
12
+ id?: string;
13
+ name?: string;
14
+ } | null | undefined): boolean;
15
+ export declare function isUserApprovalRpcMethod(method: unknown): boolean;
16
+ export declare function hasPendingWalletConnectApproval(provider?: any): boolean;
17
+ export declare function hasActiveWalletConnectSession(provider: any): boolean;
18
+ /**
19
+ * Re-open the WalletConnect relay after the mobile browser comes back
20
+ * from the wallet app so pending approve/reject responses can be delivered.
21
+ */
22
+ export declare function resumeWalletConnectRelayer(provider: any): Promise<void>;
23
+ /**
24
+ * Undo the inline `display: none !important` we apply after connect so the
25
+ * WalletConnect modal can show "Continue in wallet" and process the response.
26
+ */
27
+ export declare function restoreWalletConnectModal(): void;
28
+ /**
29
+ * Patch the EIP-1193 provider so signing/tx requests resume the relay first.
30
+ * Covers wagmi hooks, viem walletClient, and direct provider.request calls.
31
+ */
32
+ export declare function patchWalletConnectProviderForMobile(provider: any): void;
33
+ /**
34
+ * When the dapp tab is shown again (user left MetaMask), restart the relay so
35
+ * the pending WalletConnect response can be received.
36
+ */
37
+ export declare function subscribeWalletConnectForegroundResume(provider: any): () => void;
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Mobile WalletConnect helpers.
3
+ *
4
+ * On iOS/Android the browser suspends JS and drops the WalletConnect relay
5
+ * WebSocket when the user is sent to MetaMask (or another wallet) via deeplink.
6
+ * The wallet still approves/rejects, but the in-flight JSON-RPC promise never
7
+ * settles unless the relay is resumed when the dapp becomes visible again.
8
+ */
9
+ const PATCHED_FLAG = "__abstraxnWcMobilePatched";
10
+ const PENDING_FLAG = "__abstraxnWcPendingApprovals";
11
+ const USER_APPROVAL_METHODS = new Set([
12
+ "personal_sign",
13
+ "eth_sign",
14
+ "eth_signTypedData",
15
+ "eth_signTypedData_v3",
16
+ "eth_signTypedData_v4",
17
+ "eth_signTransaction",
18
+ "eth_sendTransaction",
19
+ "eth_sendRawTransaction",
20
+ "wallet_sendCalls",
21
+ "wallet_sendCallsSync",
22
+ "wallet_grantPermissions",
23
+ "wallet_requestPermissions",
24
+ "wallet_watchAsset",
25
+ "wallet_addEthereumChain",
26
+ "wallet_switchEthereumChain",
27
+ ]);
28
+ /** Selectors used to close/restore Reown AppKit + WalletConnect modal DOM. */
29
+ export const WALLETCONNECT_MODAL_SELECTORS = [
30
+ "w3m-modal",
31
+ "w3m-modal-backdrop",
32
+ "wcm-modal",
33
+ "[data-w3m-modal]",
34
+ ".w3m-modal",
35
+ ".w3m-modal-backdrop",
36
+ "#w3m-modal",
37
+ "[class*='w3m-modal']",
38
+ "[id*='w3m-modal']",
39
+ "[class*='walletconnect-qrcode']",
40
+ "[id*='walletconnect']",
41
+ ];
42
+ export function isWalletConnectConnector(connector) {
43
+ if (!connector)
44
+ return false;
45
+ const id = String(connector.id || "").toLowerCase();
46
+ const name = String(connector.name || "").toLowerCase();
47
+ return (id.includes("walletconnect") ||
48
+ id.includes("wallet_connect") ||
49
+ name.includes("walletconnect") ||
50
+ name.includes("wallet connect"));
51
+ }
52
+ export function isUserApprovalRpcMethod(method) {
53
+ return typeof method === "string" && USER_APPROVAL_METHODS.has(method);
54
+ }
55
+ export function hasPendingWalletConnectApproval(provider) {
56
+ if (provider && typeof provider[PENDING_FLAG] === "number") {
57
+ return provider[PENDING_FLAG] > 0;
58
+ }
59
+ return false;
60
+ }
61
+ export function hasActiveWalletConnectSession(provider) {
62
+ const session = provider?.session ?? provider?.signer?.session;
63
+ if (!session?.topic)
64
+ return false;
65
+ if (typeof session.expiry === "number" &&
66
+ session.expiry > 0 &&
67
+ session.expiry * 1000 < Date.now()) {
68
+ return false;
69
+ }
70
+ return true;
71
+ }
72
+ function getWalletConnectRelayer(provider) {
73
+ return (provider?.signer?.client?.core?.relayer ||
74
+ provider?.client?.core?.relayer ||
75
+ provider?.rpc?.provider?.signer?.client?.core?.relayer ||
76
+ provider?.universalProvider?.client?.core?.relayer ||
77
+ null);
78
+ }
79
+ function isRelayerConnected(relayer) {
80
+ return (relayer?.connected === true ||
81
+ relayer?.provider?.connected === true ||
82
+ relayer?.provider?.connection?.connected === true);
83
+ }
84
+ /**
85
+ * Re-open the WalletConnect relay after the mobile browser comes back
86
+ * from the wallet app so pending approve/reject responses can be delivered.
87
+ */
88
+ export async function resumeWalletConnectRelayer(provider) {
89
+ if (!provider)
90
+ return;
91
+ const relayer = getWalletConnectRelayer(provider);
92
+ if (!relayer)
93
+ return;
94
+ try {
95
+ if (isRelayerConnected(relayer)) {
96
+ try {
97
+ const ping = relayer.provider?.connection?.ping;
98
+ if (typeof ping === "function") {
99
+ await Promise.race([
100
+ ping.call(relayer.provider.connection),
101
+ new Promise((_, reject) => setTimeout(() => reject(new Error("ping timeout")), 1500)),
102
+ ]);
103
+ }
104
+ return;
105
+ }
106
+ catch {
107
+ // Ping failed — transport looks stale, restart below.
108
+ }
109
+ }
110
+ if (typeof relayer.restartTransport === "function") {
111
+ await relayer.restartTransport();
112
+ }
113
+ else if (typeof relayer.transportOpen === "function") {
114
+ try {
115
+ await relayer.transportClose?.();
116
+ }
117
+ catch {
118
+ // Ignore close errors and still try to open.
119
+ }
120
+ await relayer.transportOpen();
121
+ }
122
+ const session = provider?.session ?? provider?.signer?.session;
123
+ const topic = session?.topic;
124
+ const client = provider?.signer?.client || provider?.client;
125
+ if (topic && typeof client?.ping === "function") {
126
+ try {
127
+ await client.ping({ topic });
128
+ }
129
+ catch {
130
+ // Session ping is best-effort; the request may still complete.
131
+ }
132
+ }
133
+ }
134
+ catch {
135
+ // Best-effort: a failed resume should not break the original request.
136
+ }
137
+ }
138
+ /**
139
+ * Undo the inline `display: none !important` we apply after connect so the
140
+ * WalletConnect modal can show "Continue in wallet" and process the response.
141
+ */
142
+ export function restoreWalletConnectModal() {
143
+ if (typeof document === "undefined")
144
+ return;
145
+ WALLETCONNECT_MODAL_SELECTORS.forEach((sel) => {
146
+ try {
147
+ document.querySelectorAll(sel).forEach((el) => {
148
+ const htmlEl = el;
149
+ if (htmlEl?.style) {
150
+ htmlEl.style.removeProperty("display");
151
+ }
152
+ });
153
+ }
154
+ catch {
155
+ // Ignore invalid selectors in older browsers.
156
+ }
157
+ });
158
+ }
159
+ function getRequestMethod(args) {
160
+ const payload = args[0];
161
+ if (typeof payload === "string")
162
+ return payload;
163
+ if (payload && typeof payload === "object" && "method" in payload) {
164
+ return payload.method;
165
+ }
166
+ return undefined;
167
+ }
168
+ /**
169
+ * Patch the EIP-1193 provider so signing/tx requests resume the relay first.
170
+ * Covers wagmi hooks, viem walletClient, and direct provider.request calls.
171
+ */
172
+ export function patchWalletConnectProviderForMobile(provider) {
173
+ if (!provider || typeof provider.request !== "function")
174
+ return;
175
+ if (provider[PATCHED_FLAG])
176
+ return;
177
+ const originalRequest = provider.request.bind(provider);
178
+ provider[PATCHED_FLAG] = true;
179
+ provider[PENDING_FLAG] = 0;
180
+ provider.request = async (...args) => {
181
+ const method = getRequestMethod(args);
182
+ if (!isUserApprovalRpcMethod(method)) {
183
+ return originalRequest(...args);
184
+ }
185
+ provider[PENDING_FLAG] = (provider[PENDING_FLAG] || 0) + 1;
186
+ restoreWalletConnectModal();
187
+ try {
188
+ await resumeWalletConnectRelayer(provider);
189
+ return await originalRequest(...args);
190
+ }
191
+ finally {
192
+ provider[PENDING_FLAG] = Math.max(0, (provider[PENDING_FLAG] || 1) - 1);
193
+ }
194
+ };
195
+ }
196
+ /**
197
+ * When the dapp tab is shown again (user left MetaMask), restart the relay so
198
+ * the pending WalletConnect response can be received.
199
+ */
200
+ export function subscribeWalletConnectForegroundResume(provider) {
201
+ if (typeof window === "undefined" || !provider)
202
+ return () => { };
203
+ let timer = null;
204
+ const resume = () => {
205
+ if (timer)
206
+ clearTimeout(timer);
207
+ timer = setTimeout(() => {
208
+ void resumeWalletConnectRelayer(provider);
209
+ if (hasPendingWalletConnectApproval(provider)) {
210
+ restoreWalletConnectModal();
211
+ }
212
+ }, 250);
213
+ };
214
+ const onVisibility = () => {
215
+ if (document.visibilityState === "visible")
216
+ resume();
217
+ };
218
+ document.addEventListener("visibilitychange", onVisibility);
219
+ window.addEventListener("pageshow", resume);
220
+ window.addEventListener("focus", resume);
221
+ return () => {
222
+ if (timer)
223
+ clearTimeout(timer);
224
+ document.removeEventListener("visibilitychange", onVisibility);
225
+ window.removeEventListener("pageshow", resume);
226
+ window.removeEventListener("focus", resume);
227
+ };
228
+ }
229
+ //# sourceMappingURL=walletConnectMobile.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abstraxn/signer-react",
3
- "version": "3.3.3",
3
+ "version": "3.3.5",
4
4
  "description": "React SDK for Abstraxn Wallet - React components, hooks, and providers for seamless Web3 wallet integration",
5
5
  "main": "./dist/src/index.js",
6
6
  "module": "./dist/src/index.js",
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@abstraxn/signer-core": "2.1.3",
43
+ "@abstraxn/signer-core": "2.1.4",
44
44
  "@abstraxn/solana-relayer": "^1.1.0",
45
45
  "@solana/wallet-adapter-base": "^0.9.27",
46
46
  "@solana/wallet-adapter-react": "^0.15.39",