@abstraxn/signer-react 3.3.6 → 3.3.8
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,16 @@ 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.8] - 2026-09-16
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Create Proposal never opened MetaMask (loader stuck)** – 3.3.7 force-restarted the WalletConnect relay after every return from the wallet, including after *connect*. That restart raced the next `session_request`, so Create Proposal never deeplinked to MetaMask on iOS/Android. Relay restart now happens only for an in-flight approval. New transactions send first, then explicitly open the connected wallet app so the confirmation popup appears.
|
|
12
|
+
|
|
13
|
+
## [3.3.7] - 2026-09-15
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
8
18
|
## [3.3.6] - 2026-09-14
|
|
9
19
|
|
|
10
20
|
### Fixed
|
|
@@ -68,8 +68,11 @@ export function useQueryClientSafe() {
|
|
|
68
68
|
return new QueryClient({
|
|
69
69
|
defaultOptions: {
|
|
70
70
|
queries: {
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
//
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
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
|
}
|
|
@@ -34,6 +34,12 @@ export declare function getDappReturnUrl(): string;
|
|
|
34
34
|
* navigating Safari to a new URL (which reloads and drops in-memory state).
|
|
35
35
|
*/
|
|
36
36
|
export declare function getSourceBrowserNativeRedirect(): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Open the already-connected wallet app so the user sees the confirmation UI.
|
|
39
|
+
* WalletConnect often sends the session_request but does not deeplink on a
|
|
40
|
+
* later tx if the QR modal is hidden — Create Proposal then sits on a loader.
|
|
41
|
+
*/
|
|
42
|
+
export declare function openConnectedWalletApp(provider: any): void;
|
|
37
43
|
/**
|
|
38
44
|
* Lazy metadata so url/redirect are read at connect/request time, not at
|
|
39
45
|
* provider mount (which is often the homepage in Next.js layouts).
|
|
@@ -64,8 +70,16 @@ export declare function syncWalletConnectDappRedirect(provider: any): void;
|
|
|
64
70
|
/**
|
|
65
71
|
* Re-open the WalletConnect relay after the mobile browser comes back
|
|
66
72
|
* from the wallet app so pending approve/reject responses can be delivered.
|
|
73
|
+
*
|
|
74
|
+
* `force` must be used after the tab was backgrounded: iOS/Android often leave
|
|
75
|
+
* `relayer.connected === true` on a dead socket, so a ping/early-return skips
|
|
76
|
+
* the restart and the in-flight tx hash never arrives (loader hangs until refresh).
|
|
67
77
|
*/
|
|
68
|
-
export declare function resumeWalletConnectRelayer(provider: any
|
|
78
|
+
export declare function resumeWalletConnectRelayer(provider: any, options?: {
|
|
79
|
+
force?: boolean;
|
|
80
|
+
}): Promise<void>;
|
|
81
|
+
/** Resolves when the dapp tab is in the foreground again (mobile wallet return). */
|
|
82
|
+
export declare function waitUntilDocumentVisible(): Promise<void>;
|
|
69
83
|
/**
|
|
70
84
|
* Undo the inline `display: none !important` we apply after connect so the
|
|
71
85
|
* 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;
|
|
@@ -121,6 +123,79 @@ export function getSourceBrowserNativeRedirect() {
|
|
|
121
123
|
return "googlechrome://";
|
|
122
124
|
return undefined;
|
|
123
125
|
}
|
|
126
|
+
function isAndroid() {
|
|
127
|
+
if (typeof navigator === "undefined")
|
|
128
|
+
return false;
|
|
129
|
+
return /Android/i.test(navigator.userAgent || "");
|
|
130
|
+
}
|
|
131
|
+
function isMobileDevice() {
|
|
132
|
+
return isIOS() || isAndroid();
|
|
133
|
+
}
|
|
134
|
+
function isWalletHttpDeepLink(url) {
|
|
135
|
+
try {
|
|
136
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
137
|
+
return (host.includes("metamask.app.link") ||
|
|
138
|
+
host.includes("link.metamask.io") ||
|
|
139
|
+
host.includes("rainbow.me") ||
|
|
140
|
+
host.includes("link.trustwallet.com") ||
|
|
141
|
+
host.includes("walletconnect.com"));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function canOpenAsWalletApp(url) {
|
|
148
|
+
if (!url)
|
|
149
|
+
return false;
|
|
150
|
+
if (/^https?:\/\//i.test(url))
|
|
151
|
+
return isWalletHttpDeepLink(url);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Open the already-connected wallet app so the user sees the confirmation UI.
|
|
156
|
+
* WalletConnect often sends the session_request but does not deeplink on a
|
|
157
|
+
* later tx if the QR modal is hidden — Create Proposal then sits on a loader.
|
|
158
|
+
*/
|
|
159
|
+
export function openConnectedWalletApp(provider) {
|
|
160
|
+
if (typeof window === "undefined" || !isMobileDevice())
|
|
161
|
+
return;
|
|
162
|
+
restoreWalletConnectModal();
|
|
163
|
+
try {
|
|
164
|
+
if (typeof provider?.modal?.open === "function") {
|
|
165
|
+
void provider.modal.open();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Modal may not exist after we force-hid it post-connect.
|
|
170
|
+
}
|
|
171
|
+
const peer = provider?.session?.peer?.metadata ??
|
|
172
|
+
provider?.signer?.session?.peer?.metadata;
|
|
173
|
+
const native = String(peer?.redirect?.native || "");
|
|
174
|
+
const universal = String(peer?.redirect?.universal || "");
|
|
175
|
+
const name = String(peer?.name || "").toLowerCase();
|
|
176
|
+
let target = "";
|
|
177
|
+
if (canOpenAsWalletApp(native))
|
|
178
|
+
target = native;
|
|
179
|
+
else if (canOpenAsWalletApp(universal))
|
|
180
|
+
target = universal;
|
|
181
|
+
else if (name.includes("metamask") || !name) {
|
|
182
|
+
target = isIOS() ? "metamask://" : "https://metamask.app.link/wc";
|
|
183
|
+
}
|
|
184
|
+
else if (name.includes("rainbow")) {
|
|
185
|
+
target = "rainbow://";
|
|
186
|
+
}
|
|
187
|
+
else if (name.includes("trust")) {
|
|
188
|
+
target = "trust://";
|
|
189
|
+
}
|
|
190
|
+
if (!target)
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
window.location.href = target;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Ignore
|
|
197
|
+
}
|
|
198
|
+
}
|
|
124
199
|
function getWalletConnectRedirect() {
|
|
125
200
|
const native = getSourceBrowserNativeRedirect();
|
|
126
201
|
return native ? { native } : undefined;
|
|
@@ -249,60 +324,99 @@ function isRelayerConnected(relayer) {
|
|
|
249
324
|
relayer?.provider?.connected === true ||
|
|
250
325
|
relayer?.provider?.connection?.connected === true);
|
|
251
326
|
}
|
|
327
|
+
async function restartWalletConnectTransport(relayer) {
|
|
328
|
+
if (typeof relayer.restartTransport === "function") {
|
|
329
|
+
await relayer.restartTransport();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (typeof relayer.transportOpen === "function") {
|
|
333
|
+
try {
|
|
334
|
+
await relayer.transportClose?.();
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Ignore close errors and still try to open.
|
|
338
|
+
}
|
|
339
|
+
await relayer.transportOpen();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
252
342
|
/**
|
|
253
343
|
* Re-open the WalletConnect relay after the mobile browser comes back
|
|
254
344
|
* from the wallet app so pending approve/reject responses can be delivered.
|
|
345
|
+
*
|
|
346
|
+
* `force` must be used after the tab was backgrounded: iOS/Android often leave
|
|
347
|
+
* `relayer.connected === true` on a dead socket, so a ping/early-return skips
|
|
348
|
+
* the restart and the in-flight tx hash never arrives (loader hangs until refresh).
|
|
255
349
|
*/
|
|
256
|
-
export async function resumeWalletConnectRelayer(provider) {
|
|
350
|
+
export async function resumeWalletConnectRelayer(provider, options) {
|
|
257
351
|
if (!provider)
|
|
258
352
|
return;
|
|
259
353
|
const relayer = getWalletConnectRelayer(provider);
|
|
260
354
|
if (!relayer)
|
|
261
355
|
return;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (typeof ping === "function") {
|
|
267
|
-
await Promise.race([
|
|
268
|
-
ping.call(relayer.provider.connection),
|
|
269
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("ping timeout")), 1500)),
|
|
270
|
-
]);
|
|
271
|
-
}
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
catch {
|
|
275
|
-
// Ping failed — transport looks stale, restart below.
|
|
276
|
-
}
|
|
356
|
+
const existing = provider[RESUME_LOCK];
|
|
357
|
+
if (existing && !options?.force) {
|
|
358
|
+
try {
|
|
359
|
+
await existing;
|
|
277
360
|
}
|
|
278
|
-
|
|
279
|
-
|
|
361
|
+
catch {
|
|
362
|
+
// Previous resume failed.
|
|
280
363
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const run = (async () => {
|
|
367
|
+
try {
|
|
368
|
+
// New requests must not restart a live session — that races the
|
|
369
|
+
// session_request and MetaMask never shows a confirmation popup.
|
|
370
|
+
if (!options?.force) {
|
|
371
|
+
if (isRelayerConnected(relayer))
|
|
372
|
+
return;
|
|
373
|
+
await restartWalletConnectTransport(relayer);
|
|
374
|
+
return;
|
|
284
375
|
}
|
|
285
|
-
|
|
286
|
-
|
|
376
|
+
await restartWalletConnectTransport(relayer);
|
|
377
|
+
const session = provider?.session ?? provider?.signer?.session;
|
|
378
|
+
const topic = session?.topic;
|
|
379
|
+
const client = provider?.signer?.client || provider?.client;
|
|
380
|
+
if (topic && typeof client?.ping === "function") {
|
|
381
|
+
try {
|
|
382
|
+
await client.ping({ topic });
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// Session ping is best-effort; the request may still complete.
|
|
386
|
+
}
|
|
287
387
|
}
|
|
288
|
-
await relayer.transportOpen();
|
|
289
388
|
}
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
}
|
|
389
|
+
catch {
|
|
390
|
+
// Best-effort: a failed resume should not break the original request.
|
|
300
391
|
}
|
|
392
|
+
})();
|
|
393
|
+
provider[RESUME_LOCK] = run;
|
|
394
|
+
try {
|
|
395
|
+
await run;
|
|
301
396
|
}
|
|
302
|
-
|
|
303
|
-
|
|
397
|
+
finally {
|
|
398
|
+
if (provider[RESUME_LOCK] === run)
|
|
399
|
+
provider[RESUME_LOCK] = null;
|
|
304
400
|
}
|
|
305
401
|
}
|
|
402
|
+
/** Resolves when the dapp tab is in the foreground again (mobile wallet return). */
|
|
403
|
+
export function waitUntilDocumentVisible() {
|
|
404
|
+
if (typeof document === "undefined")
|
|
405
|
+
return Promise.resolve();
|
|
406
|
+
if (document.visibilityState === "visible")
|
|
407
|
+
return Promise.resolve();
|
|
408
|
+
return new Promise((resolve) => {
|
|
409
|
+
const onVis = () => {
|
|
410
|
+
if (document.visibilityState === "visible") {
|
|
411
|
+
document.removeEventListener("visibilitychange", onVis);
|
|
412
|
+
window.removeEventListener("pageshow", onVis);
|
|
413
|
+
resolve();
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
document.addEventListener("visibilitychange", onVis);
|
|
417
|
+
window.addEventListener("pageshow", onVis);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
306
420
|
/**
|
|
307
421
|
* Undo the inline `display: none !important` we apply after connect so the
|
|
308
422
|
* WalletConnect modal can show "Continue in wallet" and process the response.
|
|
@@ -347,18 +461,42 @@ export function patchWalletConnectProviderForMobile(provider) {
|
|
|
347
461
|
provider[PENDING_FLAG] = 0;
|
|
348
462
|
provider.request = async (...args) => {
|
|
349
463
|
const method = getRequestMethod(args);
|
|
350
|
-
|
|
464
|
+
const isApproval = isUserApprovalRpcMethod(method);
|
|
465
|
+
const wasBackgrounded = !!provider[NEEDS_RESUME_FLAG];
|
|
466
|
+
if (!isApproval && !wasBackgrounded) {
|
|
467
|
+
return originalRequest(...args);
|
|
468
|
+
}
|
|
469
|
+
if (!isApproval && wasBackgrounded) {
|
|
470
|
+
try {
|
|
471
|
+
await resumeWalletConnectRelayer(provider, { force: true });
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
provider[NEEDS_RESUME_FLAG] = false;
|
|
475
|
+
}
|
|
351
476
|
return originalRequest(...args);
|
|
352
477
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
478
|
+
if (isApproval) {
|
|
479
|
+
provider[PENDING_FLAG] = (provider[PENDING_FLAG] || 0) + 1;
|
|
480
|
+
restoreWalletConnectModal();
|
|
481
|
+
syncWalletConnectDappRedirect(provider);
|
|
482
|
+
}
|
|
356
483
|
try {
|
|
357
|
-
|
|
358
|
-
|
|
484
|
+
// Never force-restart right before sending a new tx — that is what
|
|
485
|
+
// blocked the MetaMask popup on Create Proposal after 3.3.7.
|
|
486
|
+
await Promise.race([
|
|
487
|
+
resumeWalletConnectRelayer(provider, { force: false }),
|
|
488
|
+
new Promise((resolve) => setTimeout(resolve, 1000)),
|
|
489
|
+
]);
|
|
490
|
+
const resultPromise = originalRequest(...args);
|
|
491
|
+
if (typeof window !== "undefined") {
|
|
492
|
+
window.setTimeout(() => openConnectedWalletApp(provider), 400);
|
|
493
|
+
}
|
|
494
|
+
return await resultPromise;
|
|
359
495
|
}
|
|
360
496
|
finally {
|
|
361
|
-
|
|
497
|
+
if (isApproval) {
|
|
498
|
+
provider[PENDING_FLAG] = Math.max(0, (provider[PENDING_FLAG] || 1) - 1);
|
|
499
|
+
}
|
|
362
500
|
}
|
|
363
501
|
};
|
|
364
502
|
}
|
|
@@ -373,15 +511,27 @@ export function subscribeWalletConnectForegroundResume(provider) {
|
|
|
373
511
|
const resume = () => {
|
|
374
512
|
if (timer)
|
|
375
513
|
clearTimeout(timer);
|
|
514
|
+
const pending = hasPendingWalletConnectApproval(provider);
|
|
515
|
+
const needsResume = pending || !!provider[NEEDS_RESUME_FLAG];
|
|
516
|
+
// Do not restart after connect-only backgrounding — that broke the next
|
|
517
|
+
// session_request so MetaMask never opened for Create Proposal.
|
|
518
|
+
if (!needsResume) {
|
|
519
|
+
clearWalletReturnIfSameDocument();
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
376
522
|
timer = setTimeout(() => {
|
|
377
523
|
clearWalletReturnIfSameDocument();
|
|
378
|
-
void resumeWalletConnectRelayer(provider);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
}, 250);
|
|
524
|
+
void resumeWalletConnectRelayer(provider, { force: true });
|
|
525
|
+
restoreWalletConnectModal();
|
|
526
|
+
}, 50);
|
|
383
527
|
};
|
|
384
528
|
const onVisibility = () => {
|
|
529
|
+
if (document.visibilityState === "hidden") {
|
|
530
|
+
if (hasPendingWalletConnectApproval(provider)) {
|
|
531
|
+
provider[NEEDS_RESUME_FLAG] = true;
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
385
535
|
if (document.visibilityState === "visible")
|
|
386
536
|
resume();
|
|
387
537
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abstraxn/signer-react",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.8",
|
|
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",
|