@abstraxn/signer-react 3.3.5 → 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,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.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
+
13
+ ## [3.3.6] - 2026-09-14
14
+
15
+ ### Fixed
16
+ - **WalletConnect mobile approve/reject never returned to the dapp** – After a WalletConnect action deeplinked to MetaMask (or another wallet) on iOS/Android, the browser dropped the relay WebSocket. Approve and reject results never reached the in-flight `signMessage` / `sendTransaction` / `writeContract` promise. The SDK now resumes the WalletConnect relay when the tab becomes visible again and before user-approval requests, and no longer treats a transport drop as a session disconnect.
17
+ - **WalletConnect iOS return opened Safari on the homepage** – Return metadata used `location.origin`, so MetaMask opened Safari at `/` and reloaded the app (lost in-memory UI state). Metadata now uses the current page URL. On iOS Chrome/Firefox/Edge/Brave/Opera, `redirect.native` is the source browser app scheme with no path so the existing tab can be foregrounded. On Safari, `redirect.universal` is omitted so the wallet can return to the same tab instead of navigating to `/`. If a wallet still lands on the homepage, the SDK restores the page the user transacted from.
18
+
19
+ ### Notes
20
+ - Disconnect and reconnect WalletConnect once after upgrading so the wallet stores the new return metadata. Existing sessions keep the old homepage/Safari redirect until re-paired.
21
+
8
22
  ## [3.3.4] - 2026-08-31
9
23
 
10
24
  ### Fixed
@@ -10,6 +10,7 @@ import { EVM_CHAINS, toCoreChain } from "../../chains";
10
10
  import { AbstraxnProviderWithWagmi } from "./AbstraxnProviderWithWagmi";
11
11
  import { AbstraxnProviderWithoutWagmi } from "./AbstraxnProviderWithoutWagmi";
12
12
  import { useQueryClientSafe, QueryClientWrapper, canUseReactQuery } from "./utils";
13
+ import { restoreAfterWalletRedirect } from "../../walletConnectMobile";
13
14
  import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
14
15
  import { PhantomWalletAdapter, SolflareWalletAdapter } from "@solana/wallet-adapter-wallets";
15
16
  export function AbstraxnProvider({ config, children }) {
@@ -26,6 +27,9 @@ export function AbstraxnProvider({ config, children }) {
26
27
  const [isHydrated, setIsHydrated] = useState(() => {
27
28
  return typeof window !== 'undefined';
28
29
  });
30
+ useEffect(() => {
31
+ restoreAfterWalletRedirect();
32
+ }, []);
29
33
  useEffect(() => {
30
34
  if (externalSsr && typeof window !== 'undefined' && !isHydrated) {
31
35
  // Wait for wallet injection scripts to run (Phantom, etc.)
@@ -12,7 +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
+ import { hasActiveWalletConnectSession, hasPendingWalletConnectApproval, isWalletConnectConnector, patchWalletConnectProviderForMobile, rememberCurrentPageForWalletReturn, resumeWalletConnectRelayer, subscribeWalletConnectForegroundResume, syncWalletConnectDappRedirect, } from "../../walletConnectMobile";
16
16
  /** Syncs wagmi disconnect to external wallet state; only rendered when wagmi is available (under WagmiProvider). */
17
17
  function WagmiConnectionEffectSync({ onDisconnect, }) {
18
18
  useConnectionEffect({ onDisconnect });
@@ -2878,6 +2878,21 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
2878
2878
  });
2879
2879
  }
2880
2880
  try {
2881
+ if (isWalletConnectConnector) {
2882
+ rememberCurrentPageForWalletReturn();
2883
+ try {
2884
+ if (typeof connector.getProvider === "function") {
2885
+ const provider = await connector.getProvider();
2886
+ if (provider) {
2887
+ syncWalletConnectDappRedirect(provider);
2888
+ patchWalletConnectProviderForMobile(provider);
2889
+ }
2890
+ }
2891
+ }
2892
+ catch {
2893
+ // Provider may not exist until after connect.
2894
+ }
2895
+ }
2881
2896
  // Use connectAsync to properly await the connection
2882
2897
  const result = await wagmiConnect.connectAsync({ connector });
2883
2898
  // Use the result directly to set state immediately
@@ -3187,6 +3202,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3187
3202
  const isWc = isWalletConnectConnector(connector);
3188
3203
  if (isWc) {
3189
3204
  wcProviderRef.current = p;
3205
+ syncWalletConnectDappRedirect(p);
3190
3206
  patchWalletConnectProviderForMobile(p);
3191
3207
  unsubscribeForeground = subscribeWalletConnectForegroundResume(p);
3192
3208
  if (!mounted) {
@@ -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
  });
@@ -358,16 +358,16 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
358
358
  withdrawals?: import("viem").Withdrawal[] | undefined | undefined;
359
359
  withdrawalsRoot?: `0x${string}` | undefined;
360
360
  transactions: includeTransactions extends true ? ({
361
+ input: import("viem").Hex;
362
+ s: import("viem").Hex;
361
363
  type: "legacy";
362
364
  value: bigint;
363
365
  yParity?: undefined | undefined;
364
366
  from: Address;
365
367
  gas: bigint;
366
368
  hash: import("viem").Hash;
367
- input: import("viem").Hex;
368
369
  nonce: number;
369
370
  r: import("viem").Hex;
370
- s: import("viem").Hex;
371
371
  to: Address | null;
372
372
  typeHex: import("viem").Hex | null;
373
373
  v: bigint;
@@ -383,16 +383,16 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
383
383
  blockNumber: (blockTag extends "pending" ? true : false) extends infer T_1 ? T_1 extends (blockTag extends "pending" ? true : false) ? T_1 extends true ? null : bigint : never : never;
384
384
  transactionIndex: (blockTag extends "pending" ? true : false) extends infer T_2 ? T_2 extends (blockTag extends "pending" ? true : false) ? T_2 extends true ? null : number : never : never;
385
385
  } | {
386
+ input: import("viem").Hex;
387
+ s: import("viem").Hex;
386
388
  type: "eip2930";
387
389
  value: bigint;
388
390
  yParity: number;
389
391
  from: Address;
390
392
  gas: bigint;
391
393
  hash: import("viem").Hash;
392
- input: import("viem").Hex;
393
394
  nonce: number;
394
395
  r: import("viem").Hex;
395
- s: import("viem").Hex;
396
396
  to: Address | null;
397
397
  typeHex: import("viem").Hex | null;
398
398
  v: bigint;
@@ -408,16 +408,16 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
408
408
  blockNumber: (blockTag extends "pending" ? true : false) extends infer T_4 ? T_4 extends (blockTag extends "pending" ? true : false) ? T_4 extends true ? null : bigint : never : never;
409
409
  transactionIndex: (blockTag extends "pending" ? true : false) extends infer T_5 ? T_5 extends (blockTag extends "pending" ? true : false) ? T_5 extends true ? null : number : never : never;
410
410
  } | {
411
+ input: import("viem").Hex;
412
+ s: import("viem").Hex;
411
413
  type: "eip1559";
412
414
  value: bigint;
413
415
  yParity: number;
414
416
  from: Address;
415
417
  gas: bigint;
416
418
  hash: import("viem").Hash;
417
- input: import("viem").Hex;
418
419
  nonce: number;
419
420
  r: import("viem").Hex;
420
- s: import("viem").Hex;
421
421
  to: Address | null;
422
422
  typeHex: import("viem").Hex | null;
423
423
  v: bigint;
@@ -433,16 +433,16 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
433
433
  blockNumber: (blockTag extends "pending" ? true : false) extends infer T_7 ? T_7 extends (blockTag extends "pending" ? true : false) ? T_7 extends true ? null : bigint : never : never;
434
434
  transactionIndex: (blockTag extends "pending" ? true : false) extends infer T_8 ? T_8 extends (blockTag extends "pending" ? true : false) ? T_8 extends true ? null : number : never : never;
435
435
  } | {
436
+ input: import("viem").Hex;
437
+ s: import("viem").Hex;
436
438
  type: "eip4844";
437
439
  value: bigint;
438
440
  yParity: number;
439
441
  from: Address;
440
442
  gas: bigint;
441
443
  hash: import("viem").Hash;
442
- input: import("viem").Hex;
443
444
  nonce: number;
444
445
  r: import("viem").Hex;
445
- s: import("viem").Hex;
446
446
  to: Address | null;
447
447
  typeHex: import("viem").Hex | null;
448
448
  v: bigint;
@@ -458,16 +458,16 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
458
458
  blockNumber: (blockTag extends "pending" ? true : false) extends infer T_10 ? T_10 extends (blockTag extends "pending" ? true : false) ? T_10 extends true ? null : bigint : never : never;
459
459
  transactionIndex: (blockTag extends "pending" ? true : false) extends infer T_11 ? T_11 extends (blockTag extends "pending" ? true : false) ? T_11 extends true ? null : number : never : never;
460
460
  } | {
461
+ input: import("viem").Hex;
462
+ s: import("viem").Hex;
461
463
  type: "eip7702";
462
464
  value: bigint;
463
465
  yParity: number;
464
466
  from: Address;
465
467
  gas: bigint;
466
468
  hash: import("viem").Hash;
467
- input: import("viem").Hex;
468
469
  nonce: number;
469
470
  r: import("viem").Hex;
470
- s: import("viem").Hex;
471
471
  to: Address | null;
472
472
  typeHex: import("viem").Hex | null;
473
473
  v: bigint;
@@ -508,16 +508,16 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
508
508
  } | undefined) => Promise<import("viem").EstimateMaxPriorityFeePerGasReturnType>;
509
509
  getStorageAt: (args: import("viem").GetStorageAtParameters) => Promise<import("viem").GetStorageAtReturnType>;
510
510
  getTransaction: <blockTag extends import("viem").BlockTag = "latest">(args: import("viem").GetTransactionParameters<blockTag>) => Promise<{
511
+ input: import("viem").Hex;
512
+ s: import("viem").Hex;
511
513
  type: "legacy";
512
514
  value: bigint;
513
515
  yParity?: undefined | undefined;
514
516
  from: Address;
515
517
  gas: bigint;
516
518
  hash: import("viem").Hash;
517
- input: import("viem").Hex;
518
519
  nonce: number;
519
520
  r: import("viem").Hex;
520
- s: import("viem").Hex;
521
521
  to: Address | null;
522
522
  typeHex: import("viem").Hex | null;
523
523
  v: bigint;
@@ -533,16 +533,16 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
533
533
  blockNumber: (blockTag extends "pending" ? true : false) extends infer T_1 ? T_1 extends (blockTag extends "pending" ? true : false) ? T_1 extends true ? null : bigint : never : never;
534
534
  transactionIndex: (blockTag extends "pending" ? true : false) extends infer T_2 ? T_2 extends (blockTag extends "pending" ? true : false) ? T_2 extends true ? null : number : never : never;
535
535
  } | {
536
+ input: import("viem").Hex;
537
+ s: import("viem").Hex;
536
538
  type: "eip2930";
537
539
  value: bigint;
538
540
  yParity: number;
539
541
  from: Address;
540
542
  gas: bigint;
541
543
  hash: import("viem").Hash;
542
- input: import("viem").Hex;
543
544
  nonce: number;
544
545
  r: import("viem").Hex;
545
- s: import("viem").Hex;
546
546
  to: Address | null;
547
547
  typeHex: import("viem").Hex | null;
548
548
  v: bigint;
@@ -558,16 +558,16 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
558
558
  blockNumber: (blockTag extends "pending" ? true : false) extends infer T_4 ? T_4 extends (blockTag extends "pending" ? true : false) ? T_4 extends true ? null : bigint : never : never;
559
559
  transactionIndex: (blockTag extends "pending" ? true : false) extends infer T_5 ? T_5 extends (blockTag extends "pending" ? true : false) ? T_5 extends true ? null : number : never : never;
560
560
  } | {
561
+ input: import("viem").Hex;
562
+ s: import("viem").Hex;
561
563
  type: "eip1559";
562
564
  value: bigint;
563
565
  yParity: number;
564
566
  from: Address;
565
567
  gas: bigint;
566
568
  hash: import("viem").Hash;
567
- input: import("viem").Hex;
568
569
  nonce: number;
569
570
  r: import("viem").Hex;
570
- s: import("viem").Hex;
571
571
  to: Address | null;
572
572
  typeHex: import("viem").Hex | null;
573
573
  v: bigint;
@@ -583,16 +583,16 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
583
583
  blockNumber: (blockTag extends "pending" ? true : false) extends infer T_7 ? T_7 extends (blockTag extends "pending" ? true : false) ? T_7 extends true ? null : bigint : never : never;
584
584
  transactionIndex: (blockTag extends "pending" ? true : false) extends infer T_8 ? T_8 extends (blockTag extends "pending" ? true : false) ? T_8 extends true ? null : number : never : never;
585
585
  } | {
586
+ input: import("viem").Hex;
587
+ s: import("viem").Hex;
586
588
  type: "eip4844";
587
589
  value: bigint;
588
590
  yParity: number;
589
591
  from: Address;
590
592
  gas: bigint;
591
593
  hash: import("viem").Hash;
592
- input: import("viem").Hex;
593
594
  nonce: number;
594
595
  r: import("viem").Hex;
595
- s: import("viem").Hex;
596
596
  to: Address | null;
597
597
  typeHex: import("viem").Hex | null;
598
598
  v: bigint;
@@ -608,16 +608,16 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
608
608
  blockNumber: (blockTag extends "pending" ? true : false) extends infer T_10 ? T_10 extends (blockTag extends "pending" ? true : false) ? T_10 extends true ? null : bigint : never : never;
609
609
  transactionIndex: (blockTag extends "pending" ? true : false) extends infer T_11 ? T_11 extends (blockTag extends "pending" ? true : false) ? T_11 extends true ? null : number : never : never;
610
610
  } | {
611
+ input: import("viem").Hex;
612
+ s: import("viem").Hex;
611
613
  type: "eip7702";
612
614
  value: bigint;
613
615
  yParity: number;
614
616
  from: Address;
615
617
  gas: bigint;
616
618
  hash: import("viem").Hash;
617
- input: import("viem").Hex;
618
619
  nonce: number;
619
620
  r: import("viem").Hex;
620
- s: import("viem").Hex;
621
621
  to: Address | null;
622
622
  typeHex: import("viem").Hex | null;
623
623
  v: bigint;
@@ -8162,9 +8162,9 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8162
8162
  [x: `address[${string}]`]: undefined;
8163
8163
  [x: `bool[${string}]`]: undefined;
8164
8164
  [x: `bytes[${string}]`]: undefined;
8165
+ [x: `bytes3[${string}]`]: undefined;
8165
8166
  [x: `bytes1[${string}]`]: undefined;
8166
8167
  [x: `bytes2[${string}]`]: undefined;
8167
- [x: `bytes3[${string}]`]: undefined;
8168
8168
  [x: `bytes4[${string}]`]: undefined;
8169
8169
  [x: `bytes5[${string}]`]: undefined;
8170
8170
  [x: `bytes6[${string}]`]: undefined;
@@ -8264,9 +8264,9 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8264
8264
  address?: undefined;
8265
8265
  bool?: undefined;
8266
8266
  bytes?: undefined;
8267
+ bytes3?: undefined;
8267
8268
  bytes1?: undefined;
8268
8269
  bytes2?: undefined;
8269
- bytes3?: undefined;
8270
8270
  bytes4?: undefined;
8271
8271
  bytes5?: undefined;
8272
8272
  bytes6?: undefined;
@@ -13137,9 +13137,9 @@ export declare function useExternalWalletClient(): {
13137
13137
  [x: `address[${string}]`]: undefined;
13138
13138
  [x: `bool[${string}]`]: undefined;
13139
13139
  [x: `bytes[${string}]`]: undefined;
13140
+ [x: `bytes3[${string}]`]: undefined;
13140
13141
  [x: `bytes1[${string}]`]: undefined;
13141
13142
  [x: `bytes2[${string}]`]: undefined;
13142
- [x: `bytes3[${string}]`]: undefined;
13143
13143
  [x: `bytes4[${string}]`]: undefined;
13144
13144
  [x: `bytes5[${string}]`]: undefined;
13145
13145
  [x: `bytes6[${string}]`]: undefined;
@@ -13239,9 +13239,9 @@ export declare function useExternalWalletClient(): {
13239
13239
  address?: undefined;
13240
13240
  bool?: undefined;
13241
13241
  bytes?: undefined;
13242
+ bytes3?: undefined;
13242
13243
  bytes1?: undefined;
13243
13244
  bytes2?: undefined;
13244
- bytes3?: undefined;
13245
13245
  bytes4?: undefined;
13246
13246
  bytes5?: undefined;
13247
13247
  bytes6?: undefined;
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
  }
@@ -4,6 +4,7 @@
4
4
  import { createConfig, createStorage, http, cookieStorage, } from "@wagmi/core";
5
5
  import { injected, walletConnect } from "wagmi/connectors";
6
6
  import { SUPPORTED_CHAINS, } from "@abstraxn/signer-core";
7
+ import { createWalletConnectDappMetadata } from "./walletConnectMobile";
7
8
  /**
8
9
  * Convert internal Chain type to viem Chain type
9
10
  * Uses viem's built-in chain definitions when available for better compatibility
@@ -77,21 +78,9 @@ export function createWagmiConfig(chains, walletConnectProjectId, enabledConnect
77
78
  console.warn("WalletConnect project ID is required for WalletConnect connector. Skipping...");
78
79
  }
79
80
  else {
80
- const dappUrl = typeof window !== "undefined"
81
- ? window.location.origin
82
- : "https://abstraxn.com";
83
81
  connectors.push(walletConnect({
84
82
  projectId: walletConnectProjectId,
85
- metadata: {
86
- name: "Abstraxn",
87
- description: "Abstraxn Wallet",
88
- url: dappUrl,
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
- },
94
- },
83
+ metadata: createWalletConnectDappMetadata(),
95
84
  showQrModal: true,
96
85
  qrModalOptions: {
97
86
  themeMode: theme === "dark" ? "dark" : "light",
@@ -5,6 +5,14 @@
5
5
  * WebSocket when the user is sent to MetaMask (or another wallet) via deeplink.
6
6
  * The wallet still approves/rejects, but the in-flight JSON-RPC promise never
7
7
  * settles unless the relay is resumed when the dapp becomes visible again.
8
+ *
9
+ * After approve/reject, MetaMask opens metadata.redirect / metadata.url. Using
10
+ * only `location.origin` sends iOS users to Safari on the homepage (full reload,
11
+ * lost React state). We:
12
+ * - keep metadata.url as the current page (not origin)
13
+ * - on iOS Chrome/Firefox/etc. set redirect.native to the browser app scheme
14
+ * with no path, so the existing tab is foregrounded
15
+ * - on Safari omit redirect so MetaMask can `goBack()` to the same tab
8
16
  */
9
17
  /** Selectors used to close/restore Reown AppKit + WalletConnect modal DOM. */
10
18
  export declare const WALLETCONNECT_MODAL_SELECTORS: string[];
@@ -15,11 +23,57 @@ export declare function isWalletConnectConnector(connector: {
15
23
  export declare function isUserApprovalRpcMethod(method: unknown): boolean;
16
24
  export declare function hasPendingWalletConnectApproval(provider?: any): boolean;
17
25
  export declare function hasActiveWalletConnectSession(provider: any): boolean;
26
+ /**
27
+ * Current dapp page, including path/query/hash. Never origin-only — that is
28
+ * what made MetaMask open the homepage after approve/reject.
29
+ */
30
+ export declare function getDappReturnUrl(): string;
31
+ /**
32
+ * iOS-only app scheme for the browser the user is actually in. No path:
33
+ * opening `googlechrome://` foregrounds Chrome's existing tab instead of
34
+ * navigating Safari to a new URL (which reloads and drops in-memory state).
35
+ */
36
+ export declare function getSourceBrowserNativeRedirect(): string | undefined;
37
+ /**
38
+ * Lazy metadata so url/redirect are read at connect/request time, not at
39
+ * provider mount (which is often the homepage in Next.js layouts).
40
+ */
41
+ export declare function createWalletConnectDappMetadata(): {
42
+ name: string;
43
+ description: string;
44
+ icons: string[];
45
+ url: string;
46
+ redirect?: {
47
+ native?: string;
48
+ universal?: string;
49
+ };
50
+ };
51
+ export declare function rememberCurrentPageForWalletReturn(): void;
52
+ export declare function clearWalletReturnIfSameDocument(): void;
53
+ /**
54
+ * If the wallet opened a fresh Safari tab on the homepage, send the user back
55
+ * to the page they transacted from. This still reloads that page (React state
56
+ * cannot be recovered), but it is better than leaving them on `/`.
57
+ */
58
+ export declare function restoreAfterWalletRedirect(): void;
59
+ /**
60
+ * Push the current page + source-browser scheme onto the live WC provider so
61
+ * MetaMask does not keep a stale homepage redirect from first init.
62
+ */
63
+ export declare function syncWalletConnectDappRedirect(provider: any): void;
18
64
  /**
19
65
  * Re-open the WalletConnect relay after the mobile browser comes back
20
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).
21
71
  */
22
- 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>;
23
77
  /**
24
78
  * Undo the inline `display: none !important` we apply after connect so the
25
79
  * WalletConnect modal can show "Continue in wallet" and process the response.
@@ -5,9 +5,22 @@
5
5
  * WebSocket when the user is sent to MetaMask (or another wallet) via deeplink.
6
6
  * The wallet still approves/rejects, but the in-flight JSON-RPC promise never
7
7
  * settles unless the relay is resumed when the dapp becomes visible again.
8
+ *
9
+ * After approve/reject, MetaMask opens metadata.redirect / metadata.url. Using
10
+ * only `location.origin` sends iOS users to Safari on the homepage (full reload,
11
+ * lost React state). We:
12
+ * - keep metadata.url as the current page (not origin)
13
+ * - on iOS Chrome/Firefox/etc. set redirect.native to the browser app scheme
14
+ * with no path, so the existing tab is foregrounded
15
+ * - on Safari omit redirect so MetaMask can `goBack()` to the same tab
8
16
  */
9
17
  const PATCHED_FLAG = "__abstraxnWcMobilePatched";
10
18
  const PENDING_FLAG = "__abstraxnWcPendingApprovals";
19
+ const NEEDS_RESUME_FLAG = "__abstraxnWcNeedsResume";
20
+ const RESUME_LOCK = "__abstraxnWcResumeLock";
21
+ const RETURN_HREF_KEY = "abstraxn_wc_return_href";
22
+ const RETURN_AT_KEY = "abstraxn_wc_return_at";
23
+ const RETURN_TTL_MS = 3 * 60 * 1000;
11
24
  const USER_APPROVAL_METHODS = new Set([
12
25
  "personal_sign",
13
26
  "eth_sign",
@@ -69,6 +82,163 @@ export function hasActiveWalletConnectSession(provider) {
69
82
  }
70
83
  return true;
71
84
  }
85
+ function isIOS() {
86
+ if (typeof navigator === "undefined")
87
+ return false;
88
+ const ua = navigator.userAgent || "";
89
+ const platform = navigator.platform || "";
90
+ return (/iPad|iPhone|iPod/i.test(ua) ||
91
+ (platform === "MacIntel" && (navigator.maxTouchPoints || 0) > 1));
92
+ }
93
+ /**
94
+ * Current dapp page, including path/query/hash. Never origin-only — that is
95
+ * what made MetaMask open the homepage after approve/reject.
96
+ */
97
+ export function getDappReturnUrl() {
98
+ if (typeof window === "undefined")
99
+ return "https://abstraxn.com";
100
+ const url = new URL(window.location.href);
101
+ url.searchParams.delete("wc_ev");
102
+ url.searchParams.delete("wc_res");
103
+ return `${url.origin}${url.pathname}${url.search}${url.hash}`;
104
+ }
105
+ /**
106
+ * iOS-only app scheme for the browser the user is actually in. No path:
107
+ * opening `googlechrome://` foregrounds Chrome's existing tab instead of
108
+ * navigating Safari to a new URL (which reloads and drops in-memory state).
109
+ */
110
+ export function getSourceBrowserNativeRedirect() {
111
+ if (typeof navigator === "undefined" || !isIOS())
112
+ return undefined;
113
+ const ua = navigator.userAgent || "";
114
+ if (/FxiOS/i.test(ua))
115
+ return "firefox://";
116
+ if (/EdgiOS/i.test(ua))
117
+ return "microsoft-edge://";
118
+ if (/OPiOS/i.test(ua) || /OPT\//i.test(ua))
119
+ return "touch://";
120
+ if (/Brave/i.test(ua))
121
+ return "brave://";
122
+ if (/CriOS/i.test(ua))
123
+ return "googlechrome://";
124
+ return undefined;
125
+ }
126
+ function getWalletConnectRedirect() {
127
+ const native = getSourceBrowserNativeRedirect();
128
+ return native ? { native } : undefined;
129
+ }
130
+ /**
131
+ * Lazy metadata so url/redirect are read at connect/request time, not at
132
+ * provider mount (which is often the homepage in Next.js layouts).
133
+ */
134
+ export function createWalletConnectDappMetadata() {
135
+ return {
136
+ name: "Abstraxn",
137
+ description: "Abstraxn Wallet",
138
+ icons: ["https://avatars.githubusercontent.com/u/37784886"],
139
+ get url() {
140
+ return getDappReturnUrl();
141
+ },
142
+ get redirect() {
143
+ return getWalletConnectRedirect();
144
+ },
145
+ };
146
+ }
147
+ export function rememberCurrentPageForWalletReturn() {
148
+ if (typeof window === "undefined")
149
+ return;
150
+ try {
151
+ window.localStorage.setItem(RETURN_HREF_KEY, getDappReturnUrl());
152
+ window.localStorage.setItem(RETURN_AT_KEY, String(Date.now()));
153
+ }
154
+ catch {
155
+ // Ignore quota / private-mode errors.
156
+ }
157
+ }
158
+ export function clearWalletReturnIfSameDocument() {
159
+ if (typeof window === "undefined")
160
+ return;
161
+ try {
162
+ const saved = window.localStorage.getItem(RETURN_HREF_KEY);
163
+ if (!saved)
164
+ return;
165
+ const savedUrl = new URL(saved, window.location.origin);
166
+ if (savedUrl.origin !== window.location.origin)
167
+ return;
168
+ if (savedUrl.pathname === window.location.pathname &&
169
+ savedUrl.search === window.location.search &&
170
+ savedUrl.hash === window.location.hash) {
171
+ window.localStorage.removeItem(RETURN_HREF_KEY);
172
+ window.localStorage.removeItem(RETURN_AT_KEY);
173
+ }
174
+ }
175
+ catch {
176
+ // Ignore
177
+ }
178
+ }
179
+ /**
180
+ * If the wallet opened a fresh Safari tab on the homepage, send the user back
181
+ * to the page they transacted from. This still reloads that page (React state
182
+ * cannot be recovered), but it is better than leaving them on `/`.
183
+ */
184
+ export function restoreAfterWalletRedirect() {
185
+ if (typeof window === "undefined")
186
+ return;
187
+ try {
188
+ const at = Number(window.localStorage.getItem(RETURN_AT_KEY) || 0);
189
+ const saved = window.localStorage.getItem(RETURN_HREF_KEY);
190
+ if (!saved || !at || Date.now() - at > RETURN_TTL_MS) {
191
+ window.localStorage.removeItem(RETURN_HREF_KEY);
192
+ window.localStorage.removeItem(RETURN_AT_KEY);
193
+ return;
194
+ }
195
+ const savedUrl = new URL(saved, window.location.origin);
196
+ if (savedUrl.origin !== window.location.origin) {
197
+ window.localStorage.removeItem(RETURN_HREF_KEY);
198
+ window.localStorage.removeItem(RETURN_AT_KEY);
199
+ return;
200
+ }
201
+ const savedPath = `${savedUrl.pathname}${savedUrl.search}${savedUrl.hash}`;
202
+ const currentPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
203
+ if (savedPath === currentPath) {
204
+ window.localStorage.removeItem(RETURN_HREF_KEY);
205
+ window.localStorage.removeItem(RETURN_AT_KEY);
206
+ return;
207
+ }
208
+ window.localStorage.removeItem(RETURN_HREF_KEY);
209
+ window.localStorage.removeItem(RETURN_AT_KEY);
210
+ window.location.replace(savedUrl.href);
211
+ }
212
+ catch {
213
+ // Ignore
214
+ }
215
+ }
216
+ function applyDappMetadata(meta, url, redirect) {
217
+ if (!meta || typeof meta !== "object")
218
+ return;
219
+ meta.url = url;
220
+ if (redirect) {
221
+ meta.redirect = { native: redirect.native };
222
+ }
223
+ else if (meta.redirect) {
224
+ delete meta.redirect;
225
+ }
226
+ }
227
+ /**
228
+ * Push the current page + source-browser scheme onto the live WC provider so
229
+ * MetaMask does not keep a stale homepage redirect from first init.
230
+ */
231
+ export function syncWalletConnectDappRedirect(provider) {
232
+ if (!provider || typeof window === "undefined")
233
+ return;
234
+ const url = getDappReturnUrl();
235
+ const redirect = getWalletConnectRedirect();
236
+ rememberCurrentPageForWalletReturn();
237
+ applyDappMetadata(provider.metadata, url, redirect);
238
+ applyDappMetadata(provider.signer?.metadata, url, redirect);
239
+ applyDappMetadata(provider.session?.self?.metadata, url, redirect);
240
+ applyDappMetadata(provider.signer?.session?.self?.metadata, url, redirect);
241
+ }
72
242
  function getWalletConnectRelayer(provider) {
73
243
  return (provider?.signer?.client?.core?.relayer ||
74
244
  provider?.client?.core?.relayer ||
@@ -81,60 +251,102 @@ function isRelayerConnected(relayer) {
81
251
  relayer?.provider?.connected === true ||
82
252
  relayer?.provider?.connection?.connected === true);
83
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
+ }
84
269
  /**
85
270
  * Re-open the WalletConnect relay after the mobile browser comes back
86
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).
87
276
  */
88
- export async function resumeWalletConnectRelayer(provider) {
277
+ export async function resumeWalletConnectRelayer(provider, options) {
89
278
  if (!provider)
90
279
  return;
91
280
  const relayer = getWalletConnectRelayer(provider);
92
281
  if (!relayer)
93
282
  return;
94
- try {
95
- if (isRelayerConnected(relayer)) {
96
- 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)) {
97
296
  const ping = relayer.provider?.connection?.ping;
98
297
  if (typeof ping === "function") {
99
298
  await Promise.race([
100
299
  ping.call(relayer.provider.connection),
101
300
  new Promise((_, reject) => setTimeout(() => reject(new Error("ping timeout")), 1500)),
102
301
  ]);
302
+ return;
103
303
  }
104
- return;
304
+ // No ping API: do not assume a zombie "connected" socket is healthy.
105
305
  }
106
- catch {
107
- // Ping failed — transport looks stale, restart below.
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
+ }
108
317
  }
109
318
  }
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
- }
319
+ catch {
320
+ // Best-effort: a failed resume should not break the original request.
132
321
  }
322
+ })();
323
+ provider[RESUME_LOCK] = run;
324
+ try {
325
+ await run;
133
326
  }
134
- catch {
135
- // Best-effort: a failed resume should not break the original request.
327
+ finally {
328
+ if (provider[RESUME_LOCK] === run)
329
+ provider[RESUME_LOCK] = null;
136
330
  }
137
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
+ }
138
350
  /**
139
351
  * Undo the inline `display: none !important` we apply after connect so the
140
352
  * WalletConnect modal can show "Continue in wallet" and process the response.
@@ -179,17 +391,26 @@ export function patchWalletConnectProviderForMobile(provider) {
179
391
  provider[PENDING_FLAG] = 0;
180
392
  provider.request = async (...args) => {
181
393
  const method = getRequestMethod(args);
182
- if (!isUserApprovalRpcMethod(method)) {
394
+ const isApproval = isUserApprovalRpcMethod(method);
395
+ const wasBackgrounded = !!provider[NEEDS_RESUME_FLAG];
396
+ if (!isApproval && !wasBackgrounded) {
183
397
  return originalRequest(...args);
184
398
  }
185
- provider[PENDING_FLAG] = (provider[PENDING_FLAG] || 0) + 1;
186
- restoreWalletConnectModal();
399
+ if (isApproval) {
400
+ provider[PENDING_FLAG] = (provider[PENDING_FLAG] || 0) + 1;
401
+ restoreWalletConnectModal();
402
+ syncWalletConnectDappRedirect(provider);
403
+ }
187
404
  try {
188
- await resumeWalletConnectRelayer(provider);
405
+ await resumeWalletConnectRelayer(provider, { force: wasBackgrounded });
406
+ if (wasBackgrounded)
407
+ provider[NEEDS_RESUME_FLAG] = false;
189
408
  return await originalRequest(...args);
190
409
  }
191
410
  finally {
192
- 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
+ }
193
414
  }
194
415
  };
195
416
  }
@@ -201,17 +422,28 @@ export function subscribeWalletConnectForegroundResume(provider) {
201
422
  if (typeof window === "undefined" || !provider)
202
423
  return () => { };
203
424
  let timer = null;
425
+ let hiddenAt = 0;
204
426
  const resume = () => {
205
427
  if (timer)
206
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];
207
432
  timer = setTimeout(() => {
208
- void resumeWalletConnectRelayer(provider);
209
- if (hasPendingWalletConnectApproval(provider)) {
433
+ clearWalletReturnIfSameDocument();
434
+ void resumeWalletConnectRelayer(provider, { force });
435
+ if (pending)
210
436
  restoreWalletConnectModal();
211
- }
212
- }, 250);
437
+ }, pending || force ? 50 : 250);
213
438
  };
214
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
+ }
215
447
  if (document.visibilityState === "visible")
216
448
  resume();
217
449
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abstraxn/signer-react",
3
- "version": "3.3.5",
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",