@abstraxn/signer-react 3.3.6 → 3.3.7

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,11 @@ 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.7] - 2026-09-15
9
+
10
+ ### Fixed
11
+ - **WalletConnect loader stuck after MetaMask approve (create proposal / deposit)** – Returning from the wallet left a zombie relay socket (`connected === true` on a dead WebSocket). The SDK skipped `restartTransport()`, so the tx hash never reached the dapp even though MetaMask confirmed on-chain. After a backgrounded deeplink the relay is now force-restarted, receipt polling retries when the tab is visible again, and React Query refetches on reconnect/focus so status can update without a manual refresh.
12
+
8
13
  ## [3.3.6] - 2026-09-14
9
14
 
10
15
  ### Fixed
@@ -68,8 +68,11 @@ export function useQueryClientSafe() {
68
68
  return new QueryClient({
69
69
  defaultOptions: {
70
70
  queries: {
71
- refetchOnWindowFocus: false,
72
- retry: false,
71
+ // Recover waitForTransactionReceipt / balance queries after returning
72
+ // from a mobile wallet deeplink (tab was frozen / network dropped).
73
+ refetchOnWindowFocus: true,
74
+ refetchOnReconnect: true,
75
+ retry: 2,
73
76
  },
74
77
  },
75
78
  });
package/dist/src/hooks.js CHANGED
@@ -12,6 +12,7 @@ import { useWalletClient as useWagmiWalletClient, useAccount, useConfig, useChai
12
12
  import { getWalletClient, switchChain } from '@wagmi/core';
13
13
  import { getConnectorMeta } from './connectors';
14
14
  import { getChainById } from './chains';
15
+ import { waitUntilDocumentVisible } from './walletConnectMobile';
15
16
  /**
16
17
  * Hook to check if wallet is connected
17
18
  */
@@ -2064,15 +2065,40 @@ export function useWaitForTxnReceipt(provider) {
2064
2065
  throw new Error('Transaction hash is required');
2065
2066
  }
2066
2067
  try {
2067
- // waitForTransactionReceipt is a method on PublicClient
2068
- const receipt = await provider.waitForTransactionReceipt({
2069
- hash,
2070
- confirmations,
2071
- timeout,
2072
- });
2073
- return receipt;
2068
+ // After MetaMask deeplink the tab was frozen; wait until we are visible
2069
+ // again and retry receipt polls so the UI is not stuck on a loader.
2070
+ await waitUntilDocumentVisible();
2071
+ const overallTimeout = timeout ?? 180_000;
2072
+ const startedAt = Date.now();
2073
+ let lastError;
2074
+ while (Date.now() - startedAt < overallTimeout) {
2075
+ const remaining = overallTimeout - (Date.now() - startedAt);
2076
+ try {
2077
+ const receipt = await provider.waitForTransactionReceipt({
2078
+ hash,
2079
+ confirmations,
2080
+ timeout: Math.min(20_000, remaining),
2081
+ pollingInterval: 1_500,
2082
+ });
2083
+ return receipt;
2084
+ }
2085
+ catch (error) {
2086
+ lastError = error;
2087
+ await waitUntilDocumentVisible();
2088
+ await new Promise((resolve) => setTimeout(resolve, 1_000));
2089
+ }
2090
+ }
2091
+ if (lastError instanceof Error) {
2092
+ throw new Error(`Failed to wait for transaction receipt: ${lastError.message}`);
2093
+ }
2094
+ throw lastError instanceof Error
2095
+ ? lastError
2096
+ : new Error('Failed to wait for transaction receipt');
2074
2097
  }
2075
2098
  catch (error) {
2099
+ if (error instanceof Error && error.message.startsWith('Failed to wait for transaction receipt')) {
2100
+ throw error;
2101
+ }
2076
2102
  if (error instanceof Error) {
2077
2103
  throw new Error(`Failed to wait for transaction receipt: ${error.message}`);
2078
2104
  }
@@ -64,8 +64,16 @@ export declare function syncWalletConnectDappRedirect(provider: any): void;
64
64
  /**
65
65
  * Re-open the WalletConnect relay after the mobile browser comes back
66
66
  * from the wallet app so pending approve/reject responses can be delivered.
67
+ *
68
+ * `force` must be used after the tab was backgrounded: iOS/Android often leave
69
+ * `relayer.connected === true` on a dead socket, so a ping/early-return skips
70
+ * the restart and the in-flight tx hash never arrives (loader hangs until refresh).
67
71
  */
68
- export declare function resumeWalletConnectRelayer(provider: any): Promise<void>;
72
+ export declare function resumeWalletConnectRelayer(provider: any, options?: {
73
+ force?: boolean;
74
+ }): Promise<void>;
75
+ /** Resolves when the dapp tab is in the foreground again (mobile wallet return). */
76
+ export declare function waitUntilDocumentVisible(): Promise<void>;
69
77
  /**
70
78
  * Undo the inline `display: none !important` we apply after connect so the
71
79
  * WalletConnect modal can show "Continue in wallet" and process the response.
@@ -16,6 +16,8 @@
16
16
  */
17
17
  const PATCHED_FLAG = "__abstraxnWcMobilePatched";
18
18
  const PENDING_FLAG = "__abstraxnWcPendingApprovals";
19
+ const NEEDS_RESUME_FLAG = "__abstraxnWcNeedsResume";
20
+ const RESUME_LOCK = "__abstraxnWcResumeLock";
19
21
  const RETURN_HREF_KEY = "abstraxn_wc_return_href";
20
22
  const RETURN_AT_KEY = "abstraxn_wc_return_at";
21
23
  const RETURN_TTL_MS = 3 * 60 * 1000;
@@ -249,60 +251,102 @@ function isRelayerConnected(relayer) {
249
251
  relayer?.provider?.connected === true ||
250
252
  relayer?.provider?.connection?.connected === true);
251
253
  }
254
+ async function restartWalletConnectTransport(relayer) {
255
+ if (typeof relayer.restartTransport === "function") {
256
+ await relayer.restartTransport();
257
+ return;
258
+ }
259
+ if (typeof relayer.transportOpen === "function") {
260
+ try {
261
+ await relayer.transportClose?.();
262
+ }
263
+ catch {
264
+ // Ignore close errors and still try to open.
265
+ }
266
+ await relayer.transportOpen();
267
+ }
268
+ }
252
269
  /**
253
270
  * Re-open the WalletConnect relay after the mobile browser comes back
254
271
  * from the wallet app so pending approve/reject responses can be delivered.
272
+ *
273
+ * `force` must be used after the tab was backgrounded: iOS/Android often leave
274
+ * `relayer.connected === true` on a dead socket, so a ping/early-return skips
275
+ * the restart and the in-flight tx hash never arrives (loader hangs until refresh).
255
276
  */
256
- export async function resumeWalletConnectRelayer(provider) {
277
+ export async function resumeWalletConnectRelayer(provider, options) {
257
278
  if (!provider)
258
279
  return;
259
280
  const relayer = getWalletConnectRelayer(provider);
260
281
  if (!relayer)
261
282
  return;
262
- try {
263
- if (isRelayerConnected(relayer)) {
264
- try {
283
+ const existing = provider[RESUME_LOCK];
284
+ if (existing && !options?.force) {
285
+ try {
286
+ await existing;
287
+ }
288
+ catch {
289
+ // Previous resume failed.
290
+ }
291
+ return;
292
+ }
293
+ const run = (async () => {
294
+ try {
295
+ if (!options?.force && isRelayerConnected(relayer)) {
265
296
  const ping = relayer.provider?.connection?.ping;
266
297
  if (typeof ping === "function") {
267
298
  await Promise.race([
268
299
  ping.call(relayer.provider.connection),
269
300
  new Promise((_, reject) => setTimeout(() => reject(new Error("ping timeout")), 1500)),
270
301
  ]);
302
+ return;
271
303
  }
272
- return;
273
- }
274
- catch {
275
- // Ping failed — transport looks stale, restart below.
276
- }
277
- }
278
- if (typeof relayer.restartTransport === "function") {
279
- await relayer.restartTransport();
280
- }
281
- else if (typeof relayer.transportOpen === "function") {
282
- try {
283
- await relayer.transportClose?.();
304
+ // No ping API: do not assume a zombie "connected" socket is healthy.
284
305
  }
285
- catch {
286
- // Ignore close errors and still try to open.
306
+ await restartWalletConnectTransport(relayer);
307
+ const session = provider?.session ?? provider?.signer?.session;
308
+ const topic = session?.topic;
309
+ const client = provider?.signer?.client || provider?.client;
310
+ if (topic && typeof client?.ping === "function") {
311
+ try {
312
+ await client.ping({ topic });
313
+ }
314
+ catch {
315
+ // Session ping is best-effort; the request may still complete.
316
+ }
287
317
  }
288
- await relayer.transportOpen();
289
318
  }
290
- const session = provider?.session ?? provider?.signer?.session;
291
- const topic = session?.topic;
292
- const client = provider?.signer?.client || provider?.client;
293
- if (topic && typeof client?.ping === "function") {
294
- try {
295
- await client.ping({ topic });
296
- }
297
- catch {
298
- // Session ping is best-effort; the request may still complete.
299
- }
319
+ catch {
320
+ // Best-effort: a failed resume should not break the original request.
300
321
  }
322
+ })();
323
+ provider[RESUME_LOCK] = run;
324
+ try {
325
+ await run;
301
326
  }
302
- catch {
303
- // Best-effort: a failed resume should not break the original request.
327
+ finally {
328
+ if (provider[RESUME_LOCK] === run)
329
+ provider[RESUME_LOCK] = null;
304
330
  }
305
331
  }
332
+ /** Resolves when the dapp tab is in the foreground again (mobile wallet return). */
333
+ export function waitUntilDocumentVisible() {
334
+ if (typeof document === "undefined")
335
+ return Promise.resolve();
336
+ if (document.visibilityState === "visible")
337
+ return Promise.resolve();
338
+ return new Promise((resolve) => {
339
+ const onVis = () => {
340
+ if (document.visibilityState === "visible") {
341
+ document.removeEventListener("visibilitychange", onVis);
342
+ window.removeEventListener("pageshow", onVis);
343
+ resolve();
344
+ }
345
+ };
346
+ document.addEventListener("visibilitychange", onVis);
347
+ window.addEventListener("pageshow", onVis);
348
+ });
349
+ }
306
350
  /**
307
351
  * Undo the inline `display: none !important` we apply after connect so the
308
352
  * WalletConnect modal can show "Continue in wallet" and process the response.
@@ -347,18 +391,26 @@ export function patchWalletConnectProviderForMobile(provider) {
347
391
  provider[PENDING_FLAG] = 0;
348
392
  provider.request = async (...args) => {
349
393
  const method = getRequestMethod(args);
350
- if (!isUserApprovalRpcMethod(method)) {
394
+ const isApproval = isUserApprovalRpcMethod(method);
395
+ const wasBackgrounded = !!provider[NEEDS_RESUME_FLAG];
396
+ if (!isApproval && !wasBackgrounded) {
351
397
  return originalRequest(...args);
352
398
  }
353
- provider[PENDING_FLAG] = (provider[PENDING_FLAG] || 0) + 1;
354
- restoreWalletConnectModal();
355
- syncWalletConnectDappRedirect(provider);
399
+ if (isApproval) {
400
+ provider[PENDING_FLAG] = (provider[PENDING_FLAG] || 0) + 1;
401
+ restoreWalletConnectModal();
402
+ syncWalletConnectDappRedirect(provider);
403
+ }
356
404
  try {
357
- await resumeWalletConnectRelayer(provider);
405
+ await resumeWalletConnectRelayer(provider, { force: wasBackgrounded });
406
+ if (wasBackgrounded)
407
+ provider[NEEDS_RESUME_FLAG] = false;
358
408
  return await originalRequest(...args);
359
409
  }
360
410
  finally {
361
- provider[PENDING_FLAG] = Math.max(0, (provider[PENDING_FLAG] || 1) - 1);
411
+ if (isApproval) {
412
+ provider[PENDING_FLAG] = Math.max(0, (provider[PENDING_FLAG] || 1) - 1);
413
+ }
362
414
  }
363
415
  };
364
416
  }
@@ -370,18 +422,28 @@ export function subscribeWalletConnectForegroundResume(provider) {
370
422
  if (typeof window === "undefined" || !provider)
371
423
  return () => { };
372
424
  let timer = null;
425
+ let hiddenAt = 0;
373
426
  const resume = () => {
374
427
  if (timer)
375
428
  clearTimeout(timer);
429
+ const hiddenMs = hiddenAt ? Date.now() - hiddenAt : 0;
430
+ const pending = hasPendingWalletConnectApproval(provider);
431
+ const force = pending || hiddenMs > 400 || !!provider[NEEDS_RESUME_FLAG];
376
432
  timer = setTimeout(() => {
377
433
  clearWalletReturnIfSameDocument();
378
- void resumeWalletConnectRelayer(provider);
379
- if (hasPendingWalletConnectApproval(provider)) {
434
+ void resumeWalletConnectRelayer(provider, { force });
435
+ if (pending)
380
436
  restoreWalletConnectModal();
381
- }
382
- }, 250);
437
+ }, pending || force ? 50 : 250);
383
438
  };
384
439
  const onVisibility = () => {
440
+ if (document.visibilityState === "hidden") {
441
+ hiddenAt = Date.now();
442
+ if (hasPendingWalletConnectApproval(provider)) {
443
+ provider[NEEDS_RESUME_FLAG] = true;
444
+ }
445
+ return;
446
+ }
385
447
  if (document.visibilityState === "visible")
386
448
  resume();
387
449
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abstraxn/signer-react",
3
- "version": "3.3.6",
3
+ "version": "3.3.7",
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",