@abstraxn/signer-react 3.3.9 → 3.3.10

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,17 @@ 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.10] - 2026-09-18
9
+
10
+ ### Fixed
11
+ - **Mobile WalletConnect reject / confirm / on-chain failure** (aligned with WC + EIP-1193):
12
+ - **Reject:** On return from MetaMask, force-resume the relay (time-bounded). Map EIP-1193 `4001` / WC `5000` to `User rejected the request`. If WC stays silent and pending nonce never moved, surface the same reject error so loaders clear.
13
+ - **Confirm:** WC success is a **tx hash** only. Gated chain poll recovers the hash when the relay drops after approve (success-only; does not invent rejects).
14
+ - **On-chain failure:** `useWaitForTxnReceipt` defaults to `throwOnRevert: true` so a mined `status: reverted` throws `Transaction reverted` (receipt check is dapp/RPC, not WalletConnect).
15
+
16
+ ### Added
17
+ - `isUserRejectionError`, `toUserRejectionError`, `forceResumeWalletConnectOnReturn`.
18
+
8
19
  ## [3.3.9] - 2026-09-17
9
20
 
10
21
  ### Fixed
@@ -8165,11 +8165,12 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8165
8165
  [x: `bytes3[${string}]`]: undefined;
8166
8166
  [x: `bytes1[${string}]`]: undefined;
8167
8167
  [x: `bytes2[${string}]`]: undefined;
8168
+ [x: `bytes16[${string}]`]: undefined;
8169
+ [x: `bytes8[${string}]`]: undefined;
8168
8170
  [x: `bytes4[${string}]`]: undefined;
8169
8171
  [x: `bytes5[${string}]`]: undefined;
8170
8172
  [x: `bytes6[${string}]`]: undefined;
8171
8173
  [x: `bytes7[${string}]`]: undefined;
8172
- [x: `bytes8[${string}]`]: undefined;
8173
8174
  [x: `bytes9[${string}]`]: undefined;
8174
8175
  [x: `bytes10[${string}]`]: undefined;
8175
8176
  [x: `bytes11[${string}]`]: undefined;
@@ -8177,7 +8178,6 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8177
8178
  [x: `bytes13[${string}]`]: undefined;
8178
8179
  [x: `bytes14[${string}]`]: undefined;
8179
8180
  [x: `bytes15[${string}]`]: undefined;
8180
- [x: `bytes16[${string}]`]: undefined;
8181
8181
  [x: `bytes17[${string}]`]: undefined;
8182
8182
  [x: `bytes18[${string}]`]: undefined;
8183
8183
  [x: `bytes19[${string}]`]: undefined;
@@ -8195,8 +8195,8 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8195
8195
  [x: `bytes31[${string}]`]: undefined;
8196
8196
  [x: `bytes32[${string}]`]: undefined;
8197
8197
  [x: `int[${string}]`]: undefined;
8198
- [x: `int8[${string}]`]: undefined;
8199
8198
  [x: `int16[${string}]`]: undefined;
8199
+ [x: `int8[${string}]`]: undefined;
8200
8200
  [x: `int24[${string}]`]: undefined;
8201
8201
  [x: `int32[${string}]`]: undefined;
8202
8202
  [x: `int40[${string}]`]: undefined;
@@ -8228,8 +8228,8 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8228
8228
  [x: `int248[${string}]`]: undefined;
8229
8229
  [x: `int256[${string}]`]: undefined;
8230
8230
  [x: `uint[${string}]`]: undefined;
8231
- [x: `uint8[${string}]`]: undefined;
8232
8231
  [x: `uint16[${string}]`]: undefined;
8232
+ [x: `uint8[${string}]`]: undefined;
8233
8233
  [x: `uint24[${string}]`]: undefined;
8234
8234
  [x: `uint32[${string}]`]: undefined;
8235
8235
  [x: `uint40[${string}]`]: undefined;
@@ -8267,11 +8267,12 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8267
8267
  bytes3?: undefined;
8268
8268
  bytes1?: undefined;
8269
8269
  bytes2?: undefined;
8270
+ bytes16?: undefined;
8271
+ bytes8?: undefined;
8270
8272
  bytes4?: undefined;
8271
8273
  bytes5?: undefined;
8272
8274
  bytes6?: undefined;
8273
8275
  bytes7?: undefined;
8274
- bytes8?: undefined;
8275
8276
  bytes9?: undefined;
8276
8277
  bytes10?: undefined;
8277
8278
  bytes11?: undefined;
@@ -8279,7 +8280,6 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8279
8280
  bytes13?: undefined;
8280
8281
  bytes14?: undefined;
8281
8282
  bytes15?: undefined;
8282
- bytes16?: undefined;
8283
8283
  bytes17?: undefined;
8284
8284
  bytes18?: undefined;
8285
8285
  bytes19?: undefined;
@@ -8296,8 +8296,8 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8296
8296
  bytes30?: undefined;
8297
8297
  bytes31?: undefined;
8298
8298
  bytes32?: undefined;
8299
- int8?: undefined;
8300
8299
  int16?: undefined;
8300
+ int8?: undefined;
8301
8301
  int24?: undefined;
8302
8302
  int32?: undefined;
8303
8303
  int40?: undefined;
@@ -8328,8 +8328,8 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8328
8328
  int240?: undefined;
8329
8329
  int248?: undefined;
8330
8330
  int256?: undefined;
8331
- uint8?: undefined;
8332
8331
  uint16?: undefined;
8332
+ uint8?: undefined;
8333
8333
  uint24?: undefined;
8334
8334
  uint32?: undefined;
8335
8335
  uint40?: undefined;
@@ -8811,10 +8811,16 @@ export declare function useSignAndSendTxn(provider: PublicClient): {
8811
8811
  * ```
8812
8812
  */
8813
8813
  export declare function useWaitForTxnReceipt(provider: PublicClient): {
8814
- waitForTxnReceipt: ({ hash, confirmations, timeout, }: {
8814
+ waitForTxnReceipt: ({ hash, confirmations, timeout, throwOnRevert, }: {
8815
8815
  hash: `0x${string}`;
8816
8816
  confirmations?: number;
8817
8817
  timeout?: number;
8818
+ /**
8819
+ * When true (default), a mined receipt with status reverted throws
8820
+ * `Transaction reverted` — on-chain failure is not a WalletConnect concern;
8821
+ * the dapp must check the receipt after receiving the hash.
8822
+ */
8823
+ throwOnRevert?: boolean;
8818
8824
  }) => Promise<TransactionReceipt>;
8819
8825
  };
8820
8826
  /**
@@ -13140,11 +13146,12 @@ export declare function useExternalWalletClient(): {
13140
13146
  [x: `bytes3[${string}]`]: undefined;
13141
13147
  [x: `bytes1[${string}]`]: undefined;
13142
13148
  [x: `bytes2[${string}]`]: undefined;
13149
+ [x: `bytes16[${string}]`]: undefined;
13150
+ [x: `bytes8[${string}]`]: undefined;
13143
13151
  [x: `bytes4[${string}]`]: undefined;
13144
13152
  [x: `bytes5[${string}]`]: undefined;
13145
13153
  [x: `bytes6[${string}]`]: undefined;
13146
13154
  [x: `bytes7[${string}]`]: undefined;
13147
- [x: `bytes8[${string}]`]: undefined;
13148
13155
  [x: `bytes9[${string}]`]: undefined;
13149
13156
  [x: `bytes10[${string}]`]: undefined;
13150
13157
  [x: `bytes11[${string}]`]: undefined;
@@ -13152,7 +13159,6 @@ export declare function useExternalWalletClient(): {
13152
13159
  [x: `bytes13[${string}]`]: undefined;
13153
13160
  [x: `bytes14[${string}]`]: undefined;
13154
13161
  [x: `bytes15[${string}]`]: undefined;
13155
- [x: `bytes16[${string}]`]: undefined;
13156
13162
  [x: `bytes17[${string}]`]: undefined;
13157
13163
  [x: `bytes18[${string}]`]: undefined;
13158
13164
  [x: `bytes19[${string}]`]: undefined;
@@ -13170,8 +13176,8 @@ export declare function useExternalWalletClient(): {
13170
13176
  [x: `bytes31[${string}]`]: undefined;
13171
13177
  [x: `bytes32[${string}]`]: undefined;
13172
13178
  [x: `int[${string}]`]: undefined;
13173
- [x: `int8[${string}]`]: undefined;
13174
13179
  [x: `int16[${string}]`]: undefined;
13180
+ [x: `int8[${string}]`]: undefined;
13175
13181
  [x: `int24[${string}]`]: undefined;
13176
13182
  [x: `int32[${string}]`]: undefined;
13177
13183
  [x: `int40[${string}]`]: undefined;
@@ -13203,8 +13209,8 @@ export declare function useExternalWalletClient(): {
13203
13209
  [x: `int248[${string}]`]: undefined;
13204
13210
  [x: `int256[${string}]`]: undefined;
13205
13211
  [x: `uint[${string}]`]: undefined;
13206
- [x: `uint8[${string}]`]: undefined;
13207
13212
  [x: `uint16[${string}]`]: undefined;
13213
+ [x: `uint8[${string}]`]: undefined;
13208
13214
  [x: `uint24[${string}]`]: undefined;
13209
13215
  [x: `uint32[${string}]`]: undefined;
13210
13216
  [x: `uint40[${string}]`]: undefined;
@@ -13242,11 +13248,12 @@ export declare function useExternalWalletClient(): {
13242
13248
  bytes3?: undefined;
13243
13249
  bytes1?: undefined;
13244
13250
  bytes2?: undefined;
13251
+ bytes16?: undefined;
13252
+ bytes8?: undefined;
13245
13253
  bytes4?: undefined;
13246
13254
  bytes5?: undefined;
13247
13255
  bytes6?: undefined;
13248
13256
  bytes7?: undefined;
13249
- bytes8?: undefined;
13250
13257
  bytes9?: undefined;
13251
13258
  bytes10?: undefined;
13252
13259
  bytes11?: undefined;
@@ -13254,7 +13261,6 @@ export declare function useExternalWalletClient(): {
13254
13261
  bytes13?: undefined;
13255
13262
  bytes14?: undefined;
13256
13263
  bytes15?: undefined;
13257
- bytes16?: undefined;
13258
13264
  bytes17?: undefined;
13259
13265
  bytes18?: undefined;
13260
13266
  bytes19?: undefined;
@@ -13271,8 +13277,8 @@ export declare function useExternalWalletClient(): {
13271
13277
  bytes30?: undefined;
13272
13278
  bytes31?: undefined;
13273
13279
  bytes32?: undefined;
13274
- int8?: undefined;
13275
13280
  int16?: undefined;
13281
+ int8?: undefined;
13276
13282
  int24?: undefined;
13277
13283
  int32?: undefined;
13278
13284
  int40?: undefined;
@@ -13303,8 +13309,8 @@ export declare function useExternalWalletClient(): {
13303
13309
  int240?: undefined;
13304
13310
  int248?: undefined;
13305
13311
  int256?: undefined;
13306
- uint8?: undefined;
13307
13312
  uint16?: undefined;
13313
+ uint8?: undefined;
13308
13314
  uint24?: undefined;
13309
13315
  uint32?: undefined;
13310
13316
  uint40?: undefined;
package/dist/src/hooks.js CHANGED
@@ -12,7 +12,7 @@ import { useWalletClient as useWagmiWalletClient, useAccount, useConfig, useChai
12
12
  import { getWalletClient, switchChain, getPublicClient } from '@wagmi/core';
13
13
  import { getConnectorMeta } from './connectors';
14
14
  import { getChainById } from './chains';
15
- import { waitUntilDocumentVisible, pollForOutgoingTx, clearWalletConnectPendingState, isWalletConnectConnector, isMobileDevice, } from './walletConnectMobile';
15
+ import { waitUntilDocumentVisible, pollForOutgoingTx, clearWalletConnectPendingState, isWalletConnectConnector, isMobileDevice, isUserRejectionError, toUserRejectionError, } from './walletConnectMobile';
16
16
  /**
17
17
  * Hook to check if wallet is connected
18
18
  */
@@ -2057,7 +2057,7 @@ export function useSignAndSendTxn(provider) {
2057
2057
  * ```
2058
2058
  */
2059
2059
  export function useWaitForTxnReceipt(provider) {
2060
- const waitForTxnReceipt = useCallback(async ({ hash, confirmations, timeout, }) => {
2060
+ const waitForTxnReceipt = useCallback(async ({ hash, confirmations, timeout, throwOnRevert = true, }) => {
2061
2061
  if (!provider) {
2062
2062
  throw new Error('Provider (publicClient) is required');
2063
2063
  }
@@ -2080,9 +2080,18 @@ export function useWaitForTxnReceipt(provider) {
2080
2080
  timeout: Math.min(20_000, remaining),
2081
2081
  pollingInterval: 1_500,
2082
2082
  });
2083
+ if (throwOnRevert &&
2084
+ (receipt.status === 'reverted' ||
2085
+ receipt.status === 0 ||
2086
+ receipt.status === '0x0')) {
2087
+ throw new Error('Transaction reverted');
2088
+ }
2083
2089
  return receipt;
2084
2090
  }
2085
2091
  catch (error) {
2092
+ if (error instanceof Error && error.message === 'Transaction reverted') {
2093
+ throw error;
2094
+ }
2086
2095
  lastError = error;
2087
2096
  await waitUntilDocumentVisible();
2088
2097
  await new Promise((resolve) => setTimeout(resolve, 1_000));
@@ -2096,6 +2105,9 @@ export function useWaitForTxnReceipt(provider) {
2096
2105
  : new Error('Failed to wait for transaction receipt');
2097
2106
  }
2098
2107
  catch (error) {
2108
+ if (error instanceof Error && error.message === 'Transaction reverted') {
2109
+ throw error;
2110
+ }
2099
2111
  if (error instanceof Error && error.message.startsWith('Failed to wait for transaction receipt')) {
2100
2112
  throw error;
2101
2113
  }
@@ -2523,7 +2535,12 @@ export function useWriteContract() {
2523
2535
  // Matching without selector is still useful (from + to).
2524
2536
  }
2525
2537
  const cancelPoll = { cancelled: false };
2526
- const sendPromise = sendWrite();
2538
+ const sendPromise = sendWrite().then((hash) => hash, (err) => {
2539
+ if (isUserRejectionError(err))
2540
+ throw toUserRejectionError(err);
2541
+ throw err;
2542
+ });
2543
+ // Success-only chain recovery when WC hash never arrives.
2527
2544
  const pollPromise = pollForOutgoingTx({
2528
2545
  publicClient,
2529
2546
  from: fromAddress,
@@ -2532,12 +2549,11 @@ export function useWriteContract() {
2532
2549
  value,
2533
2550
  startBlock,
2534
2551
  signal: cancelPoll,
2535
- initialDelayMs: 10_000,
2536
- timeoutMs: 90_000,
2552
+ initialDelayMs: 1_000,
2553
+ timeoutMs: 60_000,
2537
2554
  });
2538
2555
  try {
2539
2556
  const hash = await Promise.race([sendPromise, pollPromise]);
2540
- // Poll may win while WC is still hanging — ignore a late reject.
2541
2557
  void sendPromise.then(() => undefined, () => undefined);
2542
2558
  return hash;
2543
2559
  }
@@ -16,4 +16,4 @@ export type { OnboardingUIComponentProps, OnboardingUIComponentRef } from './Onb
16
16
  export { ALL_CHAINS, EVM_CHAINS, SOLANA_CHAINS, getChainById, getChainsByType, getDefaultChains, toCoreChain, type ChainData, } from './chains';
17
17
  export { CONNECTORS, getConnectorMeta, isOAuthConnector, isWalletConnector, isAuthConnector, type ConnectorType, type ConnectorMeta, } from './connectors';
18
18
  export { createWagmiConfig } from './wagmiConfig';
19
- export { pollForOutgoingTx, clearWalletConnectPendingState, waitUntilDocumentVisible, isWalletConnectConnector, isMobileDevice, } from './walletConnectMobile';
19
+ export { pollForOutgoingTx, clearWalletConnectPendingState, waitUntilDocumentVisible, isWalletConnectConnector, isMobileDevice, forceResumeWalletConnectOnReturn, isUserRejectionError, toUserRejectionError, } from './walletConnectMobile';
package/dist/src/index.js CHANGED
@@ -11,5 +11,5 @@ export { OnboardingUIComponent as OnboardingUI } from './OnboardingUI';
11
11
  export { ALL_CHAINS, EVM_CHAINS, SOLANA_CHAINS, getChainById, getChainsByType, getDefaultChains, toCoreChain, } from './chains';
12
12
  export { CONNECTORS, getConnectorMeta, isOAuthConnector, isWalletConnector, isAuthConnector, } from './connectors';
13
13
  export { createWagmiConfig } from './wagmiConfig';
14
- export { pollForOutgoingTx, clearWalletConnectPendingState, waitUntilDocumentVisible, isWalletConnectConnector, isMobileDevice, } from './walletConnectMobile';
14
+ export { pollForOutgoingTx, clearWalletConnectPendingState, waitUntilDocumentVisible, isWalletConnectConnector, isMobileDevice, forceResumeWalletConnectOnReturn, isUserRejectionError, toUserRejectionError, } from './walletConnectMobile';
15
15
  //# sourceMappingURL=index.js.map
@@ -107,9 +107,12 @@ export declare function waitUntilDocumentVisible(): Promise<void>;
107
107
  * (poll won while eth_sendTransaction is still hanging).
108
108
  */
109
109
  export declare function clearWalletConnectPendingState(provider: any): void;
110
+ /** EIP-1193 4001 / WC 5000 (and common message shapes). */
111
+ export declare function isUserRejectionError(error: unknown): boolean;
112
+ export declare function toUserRejectionError(error?: unknown): Error;
110
113
  /**
111
- * When WalletConnect never returns eth_sendTransaction after MetaMask approve,
112
- * scan recent blocks for a matching outgoing tx (from/to + optional calldata prefix).
114
+ * Success-only recovery: find an on-chain hash when WC never returns one.
115
+ * Does not infer reject (that is handled by relay resume + reject fallback).
113
116
  */
114
117
  export declare function pollForOutgoingTx(params: {
115
118
  publicClient: {
@@ -123,7 +126,6 @@ export declare function pollForOutgoingTx(params: {
123
126
  };
124
127
  from: string;
125
128
  to: string;
126
- /** Match tx.input prefix (e.g. 0x + 4-byte selector). */
127
129
  dataPrefix?: string;
128
130
  value?: bigint;
129
131
  startBlock: bigint;
@@ -133,13 +135,15 @@ export declare function pollForOutgoingTx(params: {
133
135
  timeoutMs?: number;
134
136
  initialDelayMs?: number;
135
137
  }): Promise<`0x${string}`>;
138
+ /** Time-bounded force-resume (must not hang on transportOpen). */
139
+ export declare function forceResumeWalletConnectOnReturn(provider: any, timeoutMs?: number): Promise<void>;
136
140
  /**
137
141
  * Patch the EIP-1193 provider so signing/tx requests resume the relay first.
138
142
  * Covers wagmi hooks, viem walletClient, and direct provider.request calls.
139
143
  */
140
144
  export declare function patchWalletConnectProviderForMobile(provider: any): void;
141
145
  /**
142
- * When the dapp tab is shown again (user left MetaMask), restart the relay so
143
- * the pending WalletConnect response can be received.
146
+ * When the dapp tab is shown again, restart the relay so pending
147
+ * approve/reject responses can be delivered (WalletConnect mobile linking).
144
148
  */
145
149
  export declare function subscribeWalletConnectForegroundResume(provider: any): () => void;
@@ -569,19 +569,57 @@ export function clearWalletConnectPendingState(provider) {
569
569
  // ignore
570
570
  }
571
571
  }
572
+ /** EIP-1193 4001 / WC 5000 (and common message shapes). */
573
+ export function isUserRejectionError(error) {
574
+ const walk = (err, depth = 0) => {
575
+ if (err == null || depth > 8)
576
+ return false;
577
+ if (typeof err === "string") {
578
+ return /user rejected|rejected by user|user denied|denied transaction|request rejected|cancelled by user/i.test(err);
579
+ }
580
+ if (typeof err !== "object")
581
+ return false;
582
+ const e = err;
583
+ if (e.code === 4001 || e.code === 5000 || e.code === "4001" || e.code === "ACTION_REJECTED") {
584
+ return true;
585
+ }
586
+ if (typeof e.name === "string" && /UserRejected/i.test(e.name))
587
+ return true;
588
+ const msg = `${e.message ?? ""} ${e.shortMessage ?? ""}`;
589
+ if (/user rejected|rejected by user|user denied|denied transaction|request rejected|user cancelled|user canceled/i.test(msg)) {
590
+ return true;
591
+ }
592
+ return walk(e.cause, depth + 1);
593
+ };
594
+ return walk(error);
595
+ }
596
+ export function toUserRejectionError(error) {
597
+ if (error instanceof Error && isUserRejectionError(error)) {
598
+ const err = new Error("User rejected the request");
599
+ err.code = 4001;
600
+ err.cause = error;
601
+ return err;
602
+ }
603
+ const err = new Error("User rejected the request");
604
+ err.code = 4001;
605
+ if (error !== undefined)
606
+ err.cause = error;
607
+ return err;
608
+ }
572
609
  /**
573
- * When WalletConnect never returns eth_sendTransaction after MetaMask approve,
574
- * scan recent blocks for a matching outgoing tx (from/to + optional calldata prefix).
610
+ * Success-only recovery: find an on-chain hash when WC never returns one.
611
+ * Does not infer reject (that is handled by relay resume + reject fallback).
575
612
  */
576
613
  export async function pollForOutgoingTx(params) {
577
- const { publicClient, from, to, dataPrefix, value, startBlock, signal, timeoutMs = 90_000, initialDelayMs = 10_000, } = params;
614
+ const { publicClient, from, to, dataPrefix, value, startBlock, signal, timeoutMs = 60_000, initialDelayMs = 1_000, } = params;
615
+ await waitUntilDocumentVisible();
616
+ if (signal.cancelled)
617
+ throw new Error("cancelled");
578
618
  if (initialDelayMs > 0) {
579
619
  await new Promise((r) => setTimeout(r, initialDelayMs));
580
620
  }
581
- if (signal.cancelled) {
621
+ if (signal.cancelled)
582
622
  throw new Error("cancelled");
583
- }
584
- await waitUntilDocumentVisible();
585
623
  const fromLc = from.toLowerCase();
586
624
  const toLc = to.toLowerCase();
587
625
  const prefixLc = dataPrefix ? dataPrefix.toLowerCase() : undefined;
@@ -589,11 +627,7 @@ export async function pollForOutgoingTx(params) {
589
627
  while (!signal.cancelled && Date.now() - started < timeoutMs) {
590
628
  try {
591
629
  const latest = await publicClient.getBlockNumber();
592
- const fromBlock = startBlock > 0n
593
- ? startBlock
594
- : latest > 12n
595
- ? latest - 12n
596
- : 0n;
630
+ const fromBlock = startBlock > 0n ? startBlock : latest > 12n ? latest - 12n : 0n;
597
631
  for (let b = fromBlock; b <= latest; b++) {
598
632
  if (signal.cancelled)
599
633
  break;
@@ -609,7 +643,8 @@ export async function pollForOutgoingTx(params) {
609
643
  continue;
610
644
  if (String(tx.to || "").toLowerCase() !== toLc)
611
645
  continue;
612
- if (value !== undefined && BigInt(tx.value ?? 0) !== value) {
646
+ if (value !== undefined &&
647
+ BigInt(tx.value ?? 0) !== value) {
613
648
  continue;
614
649
  }
615
650
  if (prefixLc) {
@@ -625,11 +660,83 @@ export async function pollForOutgoingTx(params) {
625
660
  }
626
661
  }
627
662
  catch {
628
- // RPC blip — keep polling
663
+ // RPC blip
629
664
  }
630
665
  await new Promise((r) => setTimeout(r, 2_000));
631
666
  }
632
- throw new Error("No WalletConnect response and no matching on-chain transaction found. If MetaMask showed confirmed, refresh and check the explorer.");
667
+ // Stay pending until cancelled — reject UX is owned by the approval race.
668
+ if (signal.cancelled)
669
+ throw new Error("cancelled");
670
+ await new Promise(() => { });
671
+ throw new Error("cancelled");
672
+ }
673
+ /** Time-bounded force-resume (must not hang on transportOpen). */
674
+ export async function forceResumeWalletConnectOnReturn(provider, timeoutMs = 1_500) {
675
+ if (!provider)
676
+ return;
677
+ try {
678
+ provider[NEEDS_RESUME_FLAG] = true;
679
+ await Promise.race([
680
+ resumeWalletConnectRelayer(provider, { force: true }),
681
+ new Promise((resolve) => setTimeout(resolve, timeoutMs)),
682
+ ]);
683
+ }
684
+ catch {
685
+ // best-effort
686
+ }
687
+ }
688
+ async function readPendingNonce(provider) {
689
+ try {
690
+ const accounts = provider?.accounts ||
691
+ provider?.session?.namespaces?.eip155?.accounts?.map((a) => a.includes(":") ? a.split(":").pop() : a) ||
692
+ [];
693
+ const address = accounts[0];
694
+ if (!address || typeof provider.request !== "function")
695
+ return null;
696
+ // Use unbound original if patched — readPendingNonce is called from
697
+ // inside the patch with the live provider; eth_getTransactionCount is
698
+ // not an approval method so the patch forwards it.
699
+ const hex = await provider.request({
700
+ method: "eth_getTransactionCount",
701
+ params: [address, "pending"],
702
+ });
703
+ return typeof hex === "string" ? parseInt(hex, 16) : Number(hex);
704
+ }
705
+ catch {
706
+ return null;
707
+ }
708
+ }
709
+ /**
710
+ * Prefer WC delivering real approve/reject after return (docs: respondSessionRequest).
711
+ * If still pending with no nonce bump → EIP-1193-style user rejection.
712
+ * If nonce moved → do not fake reject (let success hash arrive via WC or poll).
713
+ */
714
+ async function raceApprovalWithResumeAndRejectFallback(resultPromise, provider, startNonce) {
715
+ const normalizedResult = resultPromise.then((v) => v, (err) => {
716
+ if (isUserRejectionError(err))
717
+ throw toUserRejectionError(err);
718
+ throw err;
719
+ });
720
+ const rejectFallback = (async () => {
721
+ await waitUntilDocumentVisible();
722
+ await forceResumeWalletConnectOnReturn(provider, 1_500);
723
+ await new Promise((r) => setTimeout(r, 400));
724
+ await forceResumeWalletConnectOnReturn(provider, 1_500);
725
+ // Window for MetaMask's real JSON-RPC reject/hash after resume.
726
+ await new Promise((r) => setTimeout(r, 5_000));
727
+ if (!hasPendingWalletConnectApproval(provider)) {
728
+ return await new Promise(() => { });
729
+ }
730
+ const currentNonce = await readPendingNonce(provider);
731
+ if (startNonce != null &&
732
+ currentNonce != null &&
733
+ currentNonce > startNonce) {
734
+ // Tx submitted — confirmation path owns this.
735
+ return await new Promise(() => { });
736
+ }
737
+ throw toUserRejectionError();
738
+ })();
739
+ return await Promise.race([normalizedResult, rejectFallback]);
633
740
  }
634
741
  function getRequestMethod(args) {
635
742
  const payload = args[0];
@@ -661,7 +768,10 @@ export function patchWalletConnectProviderForMobile(provider) {
661
768
  }
662
769
  if (!isApproval && wasBackgrounded) {
663
770
  try {
664
- await resumeWalletConnectRelayer(provider, { force: true });
771
+ await Promise.race([
772
+ resumeWalletConnectRelayer(provider, { force: true }),
773
+ new Promise((resolve) => setTimeout(resolve, 1_500)),
774
+ ]);
665
775
  }
666
776
  finally {
667
777
  provider[NEEDS_RESUME_FLAG] = false;
@@ -669,10 +779,10 @@ export function patchWalletConnectProviderForMobile(provider) {
669
779
  return originalRequest(...args);
670
780
  }
671
781
  let cancelDeeplinks;
782
+ let startNonce = null;
672
783
  if (isApproval) {
673
784
  provider[PENDING_FLAG] = (provider[PENDING_FLAG] || 0) + 1;
674
- // Already connected: never re-open "All Wallets". Only restore modal
675
- // during connect (no session yet).
785
+ startNonce = await readPendingNonce(provider);
676
786
  if (hasActiveWalletConnectSession(provider)) {
677
787
  hideWalletConnectModal(provider);
678
788
  }
@@ -680,29 +790,24 @@ export function patchWalletConnectProviderForMobile(provider) {
680
790
  restoreWalletConnectModal();
681
791
  }
682
792
  syncWalletConnectDappRedirect(provider);
683
- // Mark before deeplink so a fast backgrounding still triggers force-resume.
684
793
  provider[NEEDS_RESUME_FLAG] = true;
685
794
  }
686
795
  try {
687
- // Never force-restart right before sending a new tx — that is what
688
- // blocked the MetaMask popup on Create Proposal after 3.3.7.
689
796
  await Promise.race([
690
797
  resumeWalletConnectRelayer(provider, { force: false }),
691
798
  new Promise((resolve) => setTimeout(resolve, 1000)),
692
799
  ]);
693
800
  const resultPromise = originalRequest(...args);
694
801
  if (isApproval) {
695
- // Do NOT auto-deeplink when a session already exists.
696
- // WC ethereum-provider already opens the peer wallet; our extra
697
- // metamask:// calls queue iOS "Open in MetaMask?" dialogs that stay
698
- // on screen after the tx is confirmed (JS cannot dismiss them).
699
- // Connect flow (no session) may still use a single scheduled open.
700
802
  if (!hasActiveWalletConnectSession(provider)) {
701
803
  cancelDeeplinks = scheduleOpenConnectedWalletApp(provider);
702
804
  }
703
805
  else {
704
806
  hideWalletConnectModal(provider);
705
807
  }
808
+ if (isMobileDevice()) {
809
+ return await raceApprovalWithResumeAndRejectFallback(resultPromise, provider, startNonce);
810
+ }
706
811
  }
707
812
  return await resultPromise;
708
813
  }
@@ -711,7 +816,6 @@ export function patchWalletConnectProviderForMobile(provider) {
711
816
  if (isApproval) {
712
817
  provider[PENDING_FLAG] = Math.max(0, (provider[PENDING_FLAG] || 1) - 1);
713
818
  provider.__abstraxnWcDeeplinkOpened = false;
714
- // Close picker that may have been left open after approve/reject.
715
819
  hideWalletConnectModal(provider);
716
820
  if (!hasPendingWalletConnectApproval(provider)) {
717
821
  provider[NEEDS_RESUME_FLAG] = false;
@@ -721,8 +825,8 @@ export function patchWalletConnectProviderForMobile(provider) {
721
825
  };
722
826
  }
723
827
  /**
724
- * When the dapp tab is shown again (user left MetaMask), restart the relay so
725
- * the pending WalletConnect response can be received.
828
+ * When the dapp tab is shown again, restart the relay so pending
829
+ * approve/reject responses can be delivered (WalletConnect mobile linking).
726
830
  */
727
831
  export function subscribeWalletConnectForegroundResume(provider) {
728
832
  if (typeof window === "undefined" || !provider)
@@ -732,35 +836,32 @@ export function subscribeWalletConnectForegroundResume(provider) {
732
836
  timers.splice(0).forEach((t) => clearTimeout(t));
733
837
  const pending = hasPendingWalletConnectApproval(provider);
734
838
  const needsResume = pending || !!provider[NEEDS_RESUME_FLAG];
735
- // Do not restart after connect-only backgrounding — that broke the next
736
- // session_request so MetaMask never opened for Create Proposal.
737
839
  if (!needsResume) {
738
840
  clearWalletReturnIfSameDocument();
739
841
  return;
740
842
  }
741
- // iOS often reports "visible" before networking is ready; retry force-resume.
742
- const delays = [50, 300, 1000, 2000, 4000];
743
- delays.forEach((ms) => {
744
- timers.push(setTimeout(() => {
745
- if (!hasPendingWalletConnectApproval(provider) &&
746
- !provider[NEEDS_RESUME_FLAG]) {
747
- return;
748
- }
749
- clearWalletReturnIfSameDocument();
750
- void resumeWalletConnectRelayer(provider, { force: true }).then(() => {
751
- if (!hasPendingWalletConnectApproval(provider)) {
752
- provider[NEEDS_RESUME_FLAG] = false;
753
- hideWalletConnectModal(provider);
754
- }
755
- });
756
- // Do not restoreWalletConnectModal here — that reopens "All Wallets".
757
- if (!hasActiveWalletConnectSession(provider)) {
758
- restoreWalletConnectModal();
759
- }
760
- else {
843
+ const run = () => {
844
+ if (!hasPendingWalletConnectApproval(provider) &&
845
+ !provider[NEEDS_RESUME_FLAG]) {
846
+ return;
847
+ }
848
+ clearWalletReturnIfSameDocument();
849
+ void forceResumeWalletConnectOnReturn(provider, 1_500).then(() => {
850
+ if (!hasPendingWalletConnectApproval(provider)) {
851
+ provider[NEEDS_RESUME_FLAG] = false;
761
852
  hideWalletConnectModal(provider);
762
853
  }
763
- }, ms));
854
+ });
855
+ if (!hasActiveWalletConnectSession(provider)) {
856
+ restoreWalletConnectModal();
857
+ }
858
+ else {
859
+ hideWalletConnectModal(provider);
860
+ }
861
+ };
862
+ run();
863
+ [800, 2_000].forEach((ms) => {
864
+ timers.push(setTimeout(run, ms));
764
865
  });
765
866
  };
766
867
  const onVisibility = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abstraxn/signer-react",
3
- "version": "3.3.9",
3
+ "version": "3.3.10",
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",