Skip to content

Commit d8263cb

Browse files
santgr110xmkh
andauthored
fix: stop wiping stored connections when a connector registers late (#5746)
Co-authored-by: MK <mago.khamidov@gmail.com>
1 parent d6cb98d commit d8263cb

5 files changed

Lines changed: 263 additions & 1 deletion

File tree

.changeset/quick-kids-peel.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@reown/appkit-adapter-tron': patch
3+
'@reown/appkit': patch
4+
'pay-test-exchange': patch
5+
'@reown/appkit-adapter-bitcoin': patch
6+
'@reown/appkit-adapter-ethers': patch
7+
'@reown/appkit-adapter-ethers5': patch
8+
'@reown/appkit-adapter-solana': patch
9+
'@reown/appkit-adapter-ton': patch
10+
'@reown/appkit-adapter-wagmi': patch
11+
'@reown/appkit-utils': patch
12+
'@reown/appkit-cdn': patch
13+
'@reown/appkit-cli': patch
14+
'@reown/appkit-codemod': patch
15+
'@reown/appkit-common': patch
16+
'@reown/appkit-controllers': patch
17+
'@reown/appkit-core': patch
18+
'@reown/appkit-experimental': patch
19+
'@reown/appkit-pay': patch
20+
'@reown/appkit-polyfills': patch
21+
'@reown/appkit-scaffold-ui': patch
22+
'@reown/appkit-siwe': patch
23+
'@reown/appkit-siwx': patch
24+
'@reown/appkit-testing': patch
25+
'@reown/appkit-ui': patch
26+
'@reown/appkit-universal-connector': patch
27+
'@reown/appkit-wallet': patch
28+
'@reown/appkit-wallet-button': patch
29+
---
30+
31+
Fix stored TRON wallet connections being silently wiped on reload when a wallet adapter (e.g. TronLink) is still resolving its `readyState` when boot sync runs. `TronAdapter.syncConnectors` now waits for a pending wallet adapter to settle before boot connection sync runs, but only when that adapter has an actual stored TRON connection to restore, so `useAppKitAccount` rehydrates correctly instead of staying disconnected, without adding a delay to every page load.

packages/adapters/tron/src/adapter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,15 @@ export class TronAdapter extends AdapterBlueprint<TronConnector> {
5757
this.walletAdapters = params?.walletAdapters || []
5858
}
5959

60-
syncConnectors() {
60+
async syncConnectors() {
6161
const chains = ChainController.getCaipNetworks()
6262

6363
this.cleanupWalletWatch?.()
6464
this.cleanupWalletWatch = TronConnectUtil.watchWalletAdapters(this.walletAdapters, adapter => {
6565
this.addConnector(new TronConnectConnector({ adapter, chains }))
6666
})
67+
68+
await TronConnectUtil.waitForLoadingAdapters(this.walletAdapters)
6769
}
6870

6971
override async connect(
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { WalletReadyState } from '@tronweb3/tronwallet-abstract-adapter'
2+
import type { Adapter } from '@tronweb3/tronwallet-abstract-adapter'
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
import { CoreHelperUtil } from '@reown/appkit-controllers'
6+
import { HelpersUtil } from '@reown/appkit-utils'
7+
8+
import { TronConnectUtil } from '../utils/TronConnectUtil'
9+
10+
function createFakeAdapter(readyState: WalletReadyState, name = 'FakeWallet'): Adapter {
11+
const listeners = new Map<string, Set<(state: WalletReadyState) => void>>()
12+
13+
return {
14+
name,
15+
readyState,
16+
on(event: string, listener: (state: WalletReadyState) => void) {
17+
const set = listeners.get(event) ?? new Set()
18+
set.add(listener)
19+
listeners.set(event, set)
20+
return this
21+
},
22+
removeListener(event: string, listener: (state: WalletReadyState) => void) {
23+
listeners.get(event)?.delete(listener)
24+
return this
25+
},
26+
emit(event: string, state: WalletReadyState) {
27+
listeners.get(event)?.forEach(listener => listener(state))
28+
return true
29+
}
30+
} as unknown as Adapter
31+
}
32+
33+
describe('TronConnectUtil.waitForLoadingAdapters', () => {
34+
beforeEach(() => {
35+
vi.spyOn(CoreHelperUtil, 'isClient').mockReturnValue(true)
36+
// Default to "has a stored, non-disconnected TRON session" so existing polling
37+
// assertions below exercise the wait mechanism itself.
38+
vi.spyOn(HelpersUtil, 'getConnectorStorageInfo').mockReturnValue({
39+
hasConnected: true,
40+
hasDisconnected: false
41+
})
42+
})
43+
44+
afterEach(() => {
45+
vi.useRealTimers()
46+
vi.restoreAllMocks()
47+
})
48+
49+
it('resolves immediately for a loading adapter with no stored connection', async () => {
50+
vi.spyOn(HelpersUtil, 'getConnectorStorageInfo').mockReturnValue({
51+
hasConnected: false,
52+
hasDisconnected: false
53+
})
54+
const loading = createFakeAdapter(WalletReadyState.Loading)
55+
56+
await expect(TronConnectUtil.waitForLoadingAdapters([loading])).resolves.toBeUndefined()
57+
})
58+
59+
it('resolves immediately for a loading adapter that was explicitly disconnected', async () => {
60+
vi.spyOn(HelpersUtil, 'getConnectorStorageInfo').mockReturnValue({
61+
hasConnected: true,
62+
hasDisconnected: true
63+
})
64+
const loading = createFakeAdapter(WalletReadyState.Loading)
65+
66+
await expect(TronConnectUtil.waitForLoadingAdapters([loading])).resolves.toBeUndefined()
67+
})
68+
69+
it('resolves immediately when no adapter is loading', async () => {
70+
const found = createFakeAdapter(WalletReadyState.Found)
71+
const notFound = createFakeAdapter(WalletReadyState.NotFound)
72+
73+
await expect(TronConnectUtil.waitForLoadingAdapters([found, notFound])).resolves.toBeUndefined()
74+
})
75+
76+
it('resolves immediately when not running on the client', async () => {
77+
vi.spyOn(CoreHelperUtil, 'isClient').mockReturnValue(false)
78+
const loading = createFakeAdapter(WalletReadyState.Loading)
79+
80+
await expect(TronConnectUtil.waitForLoadingAdapters([loading])).resolves.toBeUndefined()
81+
})
82+
83+
it('resolves on the next poll once a loading adapter settles, without waiting for the full timeout', async () => {
84+
vi.useFakeTimers()
85+
const loading = createFakeAdapter(WalletReadyState.Loading)
86+
87+
let resolved = false
88+
const promise = TronConnectUtil.waitForLoadingAdapters([loading], 3_000).then(() => {
89+
resolved = true
90+
})
91+
92+
;(loading as unknown as { readyState: WalletReadyState }).readyState = WalletReadyState.Found
93+
94+
await vi.advanceTimersByTimeAsync(200)
95+
await promise
96+
expect(resolved).toBe(true)
97+
})
98+
99+
it('does not resolve before every loading adapter settles', async () => {
100+
vi.useFakeTimers()
101+
const first = createFakeAdapter(WalletReadyState.Loading)
102+
const second = createFakeAdapter(WalletReadyState.Loading)
103+
104+
let resolved = false
105+
const promise = TronConnectUtil.waitForLoadingAdapters([first, second], 3_000).then(() => {
106+
resolved = true
107+
})
108+
109+
;(first as unknown as { readyState: WalletReadyState }).readyState = WalletReadyState.Found
110+
await vi.advanceTimersByTimeAsync(200)
111+
expect(resolved).toBe(false)
112+
;(second as unknown as { readyState: WalletReadyState }).readyState = WalletReadyState.NotFound
113+
await vi.advanceTimersByTimeAsync(200)
114+
await promise
115+
expect(resolved).toBe(true)
116+
})
117+
118+
it('resolves after the timeout when an adapter never settles', async () => {
119+
vi.useFakeTimers()
120+
const loading = createFakeAdapter(WalletReadyState.Loading)
121+
122+
let resolved = false
123+
const promise = TronConnectUtil.waitForLoadingAdapters([loading], 3_000).then(() => {
124+
resolved = true
125+
})
126+
127+
await vi.advanceTimersByTimeAsync(2_799)
128+
expect(resolved).toBe(false)
129+
130+
await vi.advanceTimersByTimeAsync(200)
131+
await promise
132+
expect(resolved).toBe(true)
133+
})
134+
})
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Adapter } from '@tronweb3/tronwallet-abstract-adapter'
2+
import { WalletReadyState } from '@tronweb3/tronwallet-abstract-adapter'
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
import { ChainController } from '@reown/appkit-controllers'
6+
7+
import { TronAdapter } from '../adapter'
8+
import { TronConnectUtil } from '../utils/TronConnectUtil'
9+
10+
function createFakeAdapter(readyState: WalletReadyState): Adapter {
11+
return {
12+
name: 'FakeWallet',
13+
readyState,
14+
on: vi.fn(),
15+
removeListener: vi.fn()
16+
} as unknown as Adapter
17+
}
18+
19+
describe('TronAdapter.syncConnectors', () => {
20+
beforeEach(() => {
21+
vi.spyOn(ChainController, 'getCaipNetworks').mockReturnValue([])
22+
})
23+
24+
afterEach(() => {
25+
vi.restoreAllMocks()
26+
})
27+
28+
it('awaits pending wallet adapters before resolving', async () => {
29+
const loading = createFakeAdapter(WalletReadyState.Loading)
30+
const adapter = new TronAdapter({ walletAdapters: [loading] })
31+
32+
let waitResolved = false
33+
const waitSpy = vi.spyOn(TronConnectUtil, 'waitForLoadingAdapters').mockImplementation(
34+
() =>
35+
new Promise(resolve => {
36+
setTimeout(() => {
37+
waitResolved = true
38+
resolve()
39+
}, 0)
40+
})
41+
)
42+
43+
await adapter.syncConnectors()
44+
45+
expect(waitSpy).toHaveBeenCalledWith([loading])
46+
expect(waitResolved).toBe(true)
47+
})
48+
})

packages/adapters/tron/src/utils/TronConnectUtil.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import type { Adapter } from '@tronweb3/tronwallet-abstract-adapter'
22
import { WalletReadyState } from '@tronweb3/tronwallet-abstract-adapter'
33
import { TronLinkAdapter } from '@tronweb3/tronwallet-adapter-tronlink'
44

5+
import { ConstantsUtil } from '@reown/appkit-common'
56
import { CoreHelperUtil } from '@reown/appkit-controllers'
7+
import { HelpersUtil } from '@reown/appkit-utils'
68

79
/**
810
* Validate that a "Found" adapter is genuinely the wallet it claims to be.
@@ -50,7 +52,52 @@ function listenForReady(
5052
cleanups.push(() => adapter.removeListener('readyStateChanged', handler))
5153
}
5254

55+
const READY_STATE_TIMEOUT_MS = 3_000
56+
const READY_STATE_POLL_INTERVAL_MS = 200
57+
5358
export const TronConnectUtil = {
59+
/**
60+
* Polls `adapter.readyState` instead of relying on the `readyStateChanged` event, since
61+
* not every wallet adapter is guaranteed to emit it. Callers should start
62+
* `watchWalletAdapters` before awaiting this, so an adapter that becomes ready during
63+
* the wait is already handled by the time this resolves.
64+
*
65+
* Only waits for adapters that have an actual stored (non-disconnected) TRON connection,
66+
* e.g. a previously connected TronLink session. Without this guard, boot sync would block
67+
* on every loading adapter regardless of whether it has anything to restore - for example a
68+
* wallet adapter that's simply not installed, or a session connected via WalletConnect
69+
* instead of an injected adapter - adding a multi-second delay to every page load.
70+
*/
71+
waitForLoadingAdapters(adapters: Adapter[], timeoutMs = READY_STATE_TIMEOUT_MS): Promise<void> {
72+
if (!CoreHelperUtil.isClient()) {
73+
return Promise.resolve()
74+
}
75+
76+
const loadingAdapters = adapters.filter(adapter => {
77+
if (adapter.readyState !== WalletReadyState.Loading) {
78+
return false
79+
}
80+
81+
const { hasConnected, hasDisconnected } = HelpersUtil.getConnectorStorageInfo(
82+
adapter.name,
83+
ConstantsUtil.CHAIN.TRON
84+
)
85+
86+
return hasConnected && !hasDisconnected
87+
})
88+
89+
if (loadingAdapters.length === 0) {
90+
return Promise.resolve()
91+
}
92+
93+
return HelpersUtil.withRetry({
94+
conditionFn: () =>
95+
loadingAdapters.every(adapter => adapter.readyState !== WalletReadyState.Loading),
96+
intervalMs: READY_STATE_POLL_INTERVAL_MS,
97+
maxRetries: Math.ceil(timeoutMs / READY_STATE_POLL_INTERVAL_MS)
98+
}).then(() => undefined)
99+
},
100+
54101
/**
55102
* Watch for TRON wallet adapters and invoke callback when one becomes available.
56103
* Returns a cleanup function to stop listening.

0 commit comments

Comments
 (0)