Skip to content

Commit 1903e6f

Browse files
authored
feat: track active provider via local storage state (#154)
* feat: track active provider via local storage state
1 parent ec2a88e commit 1903e6f

5 files changed

Lines changed: 200 additions & 4 deletions

File tree

playground/browser-playground/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Track active provider via local storage state ([#154](https://github.com/MetaMask/connect-monorepo/pull/154))
13+
1014
### Changed
1115

1216
- Update to use hex chain ID format for `@metamask/connect-evm` API compatibility ([#150](https://github.com/MetaMask/connect-monorepo/pull/150))

playground/browser-playground/src/App.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,24 @@ import DynamicInputs, { INPUT_LABEL_TYPE } from './components/DynamicInputs';
99
import { ScopeCard } from './components/ScopeCard';
1010
import { LegacyEVMCard } from './components/LegacyEVMCard';
1111
import { WagmiCard } from './components/WagmiCard';
12+
import {
13+
isProviderActive,
14+
setProviderActive,
15+
clearAllActiveProviders,
16+
} from './utils/activeProviderStorage';
1217
import { Buffer } from 'buffer';
1318

1419
global.Buffer = Buffer;
1520

1621
function App() {
1722
const [customScopes, setCustomScopes] = useState<string[]>(['eip155:1']);
1823
const [caipAccountIds, setCaipAccountIds] = useState<CaipAccountId[]>([]);
24+
25+
// Track whether wagmi should be shown based on localStorage
26+
const [wagmiIsActiveProvider, setWagmiIsActiveProvider] = useState(() =>
27+
isProviderActive('wagmi'),
28+
);
29+
1930
const {
2031
error,
2132
status,
@@ -37,6 +48,16 @@ function App() {
3748
const { connectors, connectAsync: wagmiConnectAsync, status: wagmiStatus } = useConnect();
3849
const { disconnect: wagmiDisconnect } = useDisconnect();
3950

51+
// On mount, check if wagmi is connected but not marked as active provider
52+
// If so, disconnect wagmi to clear stale state
53+
useEffect(() => {
54+
if (wagmiConnected && !isProviderActive('wagmi')) {
55+
// Wagmi thinks it's connected but our localStorage says it shouldn't be
56+
// Disconnect to clear stale state
57+
wagmiDisconnect();
58+
}
59+
}, []);
60+
4061
const handleCheckboxChange = useCallback(
4162
(value: string, isChecked: boolean) => {
4263
if (isChecked) {
@@ -112,6 +133,8 @@ function App() {
112133
connector: metaMaskConnector,
113134
chainId,
114135
});
136+
setProviderActive('wagmi');
137+
setWagmiIsActiveProvider(true);
115138
} catch (error) {
116139
console.error('Wagmi connection error:', error);
117140
}
@@ -123,6 +146,9 @@ function App() {
123146
status === 'disconnected' || status === 'pending' || status === 'loaded';
124147

125148
const disconnect = useCallback(async () => {
149+
clearAllActiveProviders();
150+
setWagmiIsActiveProvider(false);
151+
126152
// Disconnect all connections if connected
127153
if (isConnected) {
128154
await sdkDisconnect();
@@ -195,7 +221,7 @@ function App() {
195221
</button>
196222
)}
197223

198-
{!wagmiConnected && (
224+
{(!wagmiConnected || !wagmiIsActiveProvider) && (
199225
<button
200226
type="button"
201227
data-testid={TEST_IDS.app.btnConnect('wagmi')}
@@ -274,7 +300,7 @@ function App() {
274300
</div>
275301
</section>
276302
)}
277-
{wagmiConnected && wagmiAddress && (
303+
{wagmiConnected && wagmiAddress && wagmiIsActiveProvider && (
278304
<section className="mb-6">
279305
<h2 className="text-2xl font-bold text-gray-800 mb-6">
280306
Wagmi Connection

playground/browser-playground/src/sdk/LegacyEVMSDKProvider.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import {
1212
useState,
1313
} from 'react';
1414

15+
import {
16+
isProviderActive,
17+
setProviderActive,
18+
removeProviderActive,
19+
} from '../utils/activeProviderStorage';
20+
1521
/**
1622
* Converts CAIP-2 keyed RPC URLs map to hex-keyed format.
1723
* Example: { 'eip155:1': 'url' } -> { '0x1': 'url' }
@@ -107,6 +113,23 @@ export const LegacyEVMSDKProvider = ({
107113

108114
setSDK(clientSDK);
109115
setProvider(providerInstance);
116+
117+
// Check if legacy-evm was previously active and restore connection state
118+
// This handles page refresh scenarios where the SDK may have restored
119+
// the session but didn't emit a connect event
120+
if (isProviderActive('legacy-evm')) {
121+
// Check if the SDK actually has a valid session
122+
if (clientSDK.accounts.length > 0) {
123+
setConnected(true);
124+
setAccounts(clientSDK.accounts);
125+
if (clientSDK.selectedChainId) {
126+
setChainId(clientSDK.selectedChainId);
127+
}
128+
} else {
129+
// SDK doesn't have accounts, clear stale localStorage state
130+
removeProviderActive('legacy-evm');
131+
}
132+
}
110133
}
111134

112135
return clientSDK;
@@ -125,6 +148,7 @@ export const LegacyEVMSDKProvider = ({
125148
// Ensure at least one chain ID is provided, default to mainnet if empty
126149
const chainIdsToUse = chainIds.length > 0 ? chainIds : ['0x1' as Hex];
127150
await sdkInstance.connect({ chainIds: chainIdsToUse });
151+
setProviderActive('legacy-evm');
128152
} catch (error) {
129153
console.error('Failed to connect:', error);
130154
}
@@ -140,6 +164,7 @@ export const LegacyEVMSDKProvider = ({
140164
setConnected(false);
141165
setAccounts([]);
142166
setChainId(undefined);
167+
removeProviderActive('legacy-evm');
143168
} catch (error) {
144169
console.error('Failed to disconnect:', error);
145170
}

playground/browser-playground/src/sdk/SDKProvider.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ import {
1717
useCallback,
1818
useContext,
1919
useEffect,
20-
useMemo,
2120
useRef,
2221
useState,
2322
} from 'react';
2423

24+
import {
25+
isProviderActive,
26+
setProviderActive,
27+
removeProviderActive,
28+
} from '../utils/activeProviderStorage';
29+
2530
const SDKContext = createContext<
2631
| {
2732
session: SessionData | undefined;
@@ -63,7 +68,12 @@ export const SDKProvider = ({ children }: { children: React.ReactNode }) => {
6368
payload.method === 'wallet_createSession' ||
6469
payload.method === 'wallet_getSession'
6570
) {
66-
setSession(payload.params as SessionData);
71+
// Only restore session if 'multichain' is marked as active in localStorage
72+
// This prevents showing multichain cards when the session was created
73+
// by legacy-evm or wagmi connections
74+
if (isProviderActive('multichain')) {
75+
setSession(payload.params as SessionData);
76+
}
6777
} else if (payload.method === 'stateChanged') {
6878
setStatus(payload.params as ConnectionStatus);
6979
}
@@ -79,6 +89,8 @@ export const SDKProvider = ({ children }: { children: React.ReactNode }) => {
7989
throw new Error('SDK not initialized');
8090
}
8191
const sdkInstance = await sdkRef.current;
92+
setSession(undefined);
93+
removeProviderActive('multichain');
8294
return sdkInstance.disconnect();
8395
} catch (error) {
8496
setError(error as Error);
@@ -92,8 +104,13 @@ export const SDKProvider = ({ children }: { children: React.ReactNode }) => {
92104
throw new Error('SDK not initialized');
93105
}
94106
const sdkInstance = await sdkRef.current;
107+
// Track this provider as active BEFORE connecting
108+
// This ensures the onNotification handler will accept the session
109+
setProviderActive('multichain');
95110
await sdkInstance.connect(scopes, caipAccountIds);
96111
} catch (error) {
112+
// If connection fails, remove the active provider tracking
113+
removeProviderActive('multichain');
97114
setError(error as Error);
98115
}
99116
},
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/* eslint-disable no-restricted-globals -- localStorage is intentionally used for browser storage */
2+
/**
3+
* Utility for managing active provider state in localStorage.
4+
* This tracks which connection type(s) are currently active to ensure
5+
* proper state restoration after page refresh.
6+
*/
7+
8+
const STORAGE_KEY = 'browser-playground.active-provider';
9+
10+
export type ProviderType = 'multichain' | 'legacy-evm' | 'wagmi';
11+
12+
/**
13+
* Gets the currently active providers from localStorage.
14+
*
15+
* @returns Array of active provider types, or empty array if none
16+
*/
17+
export function getActiveProviders(): ProviderType[] {
18+
try {
19+
const stored = localStorage.getItem(STORAGE_KEY);
20+
if (!stored) {
21+
return [];
22+
}
23+
const parsed = JSON.parse(stored);
24+
if (Array.isArray(parsed)) {
25+
return parsed as ProviderType[];
26+
}
27+
return [];
28+
} catch (error) {
29+
console.error(
30+
'[activeProviderStorage] Failed to get active providers:',
31+
error,
32+
);
33+
return [];
34+
}
35+
}
36+
37+
/**
38+
* Checks if a specific provider is marked as active.
39+
*
40+
* @param provider - The provider type to check
41+
* @returns true if the provider is active
42+
*/
43+
export function isProviderActive(provider: ProviderType): boolean {
44+
return getActiveProviders().includes(provider);
45+
}
46+
47+
/**
48+
* Sets a provider as active. Handles mutual exclusivity between
49+
* legacy-evm and wagmi (since they share the same underlying provider).
50+
*
51+
* @param provider - The provider type to set as active
52+
*/
53+
export function setProviderActive(provider: ProviderType): void {
54+
try {
55+
const current = getActiveProviders();
56+
57+
// Handle mutual exclusivity between legacy-evm and wagmi
58+
// They share the same underlying EVM provider, so only one can be active
59+
let updated: ProviderType[];
60+
if (provider === 'legacy-evm') {
61+
updated = current.filter(
62+
(providerType) =>
63+
providerType !== 'wagmi' && providerType !== 'legacy-evm',
64+
);
65+
updated.push('legacy-evm');
66+
} else if (provider === 'wagmi') {
67+
updated = current.filter(
68+
(providerType) =>
69+
providerType !== 'legacy-evm' && providerType !== 'wagmi',
70+
);
71+
updated.push('wagmi');
72+
} else if (current.includes(provider)) {
73+
// multichain already in list, no change needed
74+
updated = current;
75+
} else {
76+
// multichain can coexist with either
77+
updated = [...current, provider];
78+
}
79+
80+
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
81+
} catch (error) {
82+
console.error(
83+
`[activeProviderStorage] Failed to set provider "${provider}" as active:`,
84+
error,
85+
);
86+
}
87+
}
88+
89+
/**
90+
* Removes a specific provider from the active list.
91+
*
92+
* @param provider - The provider type to remove
93+
*/
94+
export function removeProviderActive(provider: ProviderType): void {
95+
try {
96+
const current = getActiveProviders();
97+
const updated = current.filter((providerType) => providerType !== provider);
98+
if (updated.length === 0) {
99+
localStorage.removeItem(STORAGE_KEY);
100+
} else {
101+
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
102+
}
103+
} catch (error) {
104+
console.error(
105+
`[activeProviderStorage] Failed to remove provider "${provider}" from active list:`,
106+
error,
107+
);
108+
}
109+
}
110+
111+
/**
112+
* Clears all active provider state from localStorage.
113+
* Called when disconnecting all connections.
114+
*/
115+
export function clearAllActiveProviders(): void {
116+
try {
117+
localStorage.removeItem(STORAGE_KEY);
118+
} catch (error) {
119+
console.error(
120+
'[activeProviderStorage] Failed to clear all active providers:',
121+
error,
122+
);
123+
}
124+
}

0 commit comments

Comments
 (0)