Skip to content

Commit 96b4d07

Browse files
authored
feat: add network picker (#414)
* netowrk-picker * lint fix * err handling * simplify logic * rmv icon * tweaks * address review comments
1 parent 78bc9eb commit 96b4d07

5 files changed

Lines changed: 515 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './connections';
2+
export * from './networks';
23
export * from './permissions';
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import globalContext from '../..';
2+
3+
const NETWORKS = [
4+
// Main networks
5+
{
6+
name: 'Ethereum Mainnet',
7+
chainId: '0x1',
8+
color: '#627eea',
9+
category: 'main',
10+
},
11+
{
12+
name: 'Linea',
13+
chainId: '0xe708',
14+
color: '#000000',
15+
category: 'main',
16+
},
17+
{
18+
name: 'Base Mainnet',
19+
chainId: '0x2105',
20+
color: '#0052ff',
21+
category: 'main',
22+
},
23+
{
24+
name: 'Arbitrum One',
25+
chainId: '0xa4b1',
26+
color: '#28a0f0',
27+
category: 'main',
28+
},
29+
{
30+
name: 'Avalanche Network C-Chain',
31+
chainId: '0xa86a',
32+
color: '#e84142',
33+
category: 'main',
34+
},
35+
{
36+
name: 'Binance Smart Chain',
37+
chainId: '0x38',
38+
color: '#f3ba2f',
39+
category: 'main',
40+
},
41+
{
42+
name: 'OP Mainnet',
43+
chainId: '0xa',
44+
color: '#ff0420',
45+
category: 'main',
46+
},
47+
{
48+
name: 'Polygon Mainnet',
49+
chainId: '0x89',
50+
color: '#8247e5',
51+
category: 'main',
52+
},
53+
{
54+
name: 'Sei Network',
55+
chainId: '0x1a',
56+
color: '#ff6b35',
57+
category: 'main',
58+
},
59+
{
60+
name: 'zkSync Era Mainnet',
61+
chainId: '0x144',
62+
color: '#8e71c7',
63+
category: 'main',
64+
},
65+
66+
// Test networks
67+
{
68+
name: 'Sepolia',
69+
chainId: '0xaa36a7',
70+
color: '#f6c343',
71+
category: 'test',
72+
},
73+
{
74+
name: 'Linea Sepolia',
75+
chainId: '0xe705',
76+
color: '#000000',
77+
category: 'test',
78+
},
79+
{
80+
name: 'Mega Testnet',
81+
chainId: '0x1a4',
82+
color: '#ff6b35',
83+
category: 'test',
84+
},
85+
{
86+
name: 'Monad Testnet',
87+
chainId: '0x1a5',
88+
color: '#ff6b35',
89+
category: 'test',
90+
},
91+
];
92+
93+
export function populateNetworkLists() {
94+
const mainNetworks = document.getElementById('mainNetworks');
95+
const testNetworks = document.getElementById('testNetworks');
96+
97+
NETWORKS.forEach((network) => {
98+
const networkItem = createNetworkItem(network);
99+
100+
switch (network.category) {
101+
case 'main':
102+
mainNetworks.appendChild(networkItem);
103+
break;
104+
case 'test':
105+
testNetworks.appendChild(networkItem);
106+
break;
107+
default:
108+
break;
109+
}
110+
});
111+
}
112+
113+
export function createNetworkItem(network) {
114+
const item = document.createElement('div');
115+
item.className = 'network-modal-item';
116+
item.dataset.chainId = network.chainId;
117+
item.innerHTML = `
118+
<div class="network-modal-item-content">
119+
<div class="network-modal-item-icon" style="background-color: ${network.color}"></div>
120+
<div class="network-modal-item-info">
121+
<div class="network-modal-item-name">${network.name}</div>
122+
<div class="network-modal-item-chain-id">${network.chainId}</div>
123+
</div>
124+
</div>
125+
`;
126+
127+
item.addEventListener('click', async () => {
128+
hideNetworkError(); // Hide any existing error before attempting to switch
129+
await switchNetwork(network.chainId);
130+
document.querySelector('.network-modal').style.display = 'none';
131+
});
132+
133+
return item;
134+
}
135+
136+
export function showNetworkError(message) {
137+
// Remove any existing error message
138+
hideNetworkError();
139+
140+
// Create error message element
141+
const errorDiv = document.createElement('div');
142+
errorDiv.id = 'networkError';
143+
errorDiv.className = 'error-message';
144+
errorDiv.style.marginTop = '10px';
145+
errorDiv.style.width = '100%';
146+
errorDiv.innerHTML = `<div class="error-message-text">${message}</div>`;
147+
148+
// Find the network picker button and insert error after it
149+
const networkButton = document.getElementById('openNetworkPicker');
150+
const cardBody = networkButton.closest('.card-body');
151+
cardBody.appendChild(errorDiv);
152+
153+
// Auto-hide after 5 seconds
154+
setTimeout(() => {
155+
hideNetworkError();
156+
}, 5000);
157+
}
158+
159+
export function hideNetworkError() {
160+
const existingError = document.getElementById('networkError');
161+
if (existingError) {
162+
existingError.remove();
163+
}
164+
}
165+
166+
export async function switchNetwork(chainId) {
167+
if (!globalContext.provider) {
168+
console.error('No provider available');
169+
return;
170+
}
171+
172+
try {
173+
await globalContext.provider.request({
174+
method: 'wallet_switchEthereumChain',
175+
params: [{ chainId }],
176+
});
177+
} catch (switchError) {
178+
// This error code indicates that the chain has not been added to MetaMask.
179+
if (switchError.code === 4902) {
180+
const network = NETWORKS.find((n) => n.chainId === chainId);
181+
const networkName = network ? network.name : `Chain ID ${chainId}`;
182+
showNetworkError(`${networkName} is not available in your wallet`);
183+
} else {
184+
console.error('Error switching network:', switchError);
185+
showNetworkError('Failed to switch network');
186+
}
187+
}
188+
}
189+
190+
export function updateCurrentNetworkDisplay() {
191+
const currentNetworkName = document.getElementById('currentNetworkName');
192+
193+
if (!globalContext.chainIdInt) {
194+
currentNetworkName.textContent = 'Current Network: Not Connected';
195+
return;
196+
}
197+
const network = NETWORKS.find((n) => {
198+
const networkChainId = parseInt(n.chainId, 16);
199+
return networkChainId === globalContext.chainIdInt;
200+
});
201+
// Fallback to chain ID if network not found
202+
currentNetworkName.textContent = network
203+
? `Current Network: ${network.name}`
204+
: `Current Network: Chain ID 0x${globalContext.chainIdInt.toString(16)}`;
205+
}
206+
207+
export function updateActiveNetworkInModal() {
208+
const networkItems = document.querySelectorAll('.network-modal-item');
209+
210+
networkItems.forEach((item) => {
211+
const itemChainId = item.dataset.chainId;
212+
const itemChainIdInt = parseInt(itemChainId, 16);
213+
const isActive = itemChainIdInt === globalContext.chainIdInt;
214+
215+
if (isActive) {
216+
item.classList.add('active');
217+
} else {
218+
item.classList.remove('active');
219+
}
220+
});
221+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
populateNetworkLists,
3+
updateCurrentNetworkDisplay,
4+
updateActiveNetworkInModal,
5+
hideNetworkError,
6+
} from './networks-helpers';
7+
8+
export function networksComponent(parentContainer) {
9+
parentContainer.insertAdjacentHTML(
10+
'beforeend',
11+
`<div class="col-xl-4 col-lg-6 col-md-12 col-sm-12 col-12">
12+
<div class="card">
13+
<div class="card-body">
14+
<h4 class="card-title">
15+
Network Picker
16+
</h4>
17+
<button id="openNetworkPicker" class="btn btn-outline-primary btn-lg btn-block mb-3">
18+
<span id="currentNetworkName">Select Network</span>
19+
<i class="fas fa-chevron-down ml-2"></i>
20+
</button>
21+
</div>
22+
</div>
23+
</div>`,
24+
);
25+
26+
const modal = document.createElement('div');
27+
modal.className = 'network-modal';
28+
modal.innerHTML = `
29+
<div class="network-modal-content">
30+
<div class="network-modal-header">
31+
<h5>Select Network</h5>
32+
<button class="network-modal-close">&times;</button>
33+
</div>
34+
<div class="network-modal-body">
35+
<div class="network-category">
36+
<h6>Main Networks</h6>
37+
<div class="network-list" id="mainNetworks"></div>
38+
</div>
39+
<div class="network-category">
40+
<h6>Test Networks</h6>
41+
<div class="network-list" id="testNetworks"></div>
42+
</div>
43+
</div>
44+
</div>
45+
`;
46+
document.body.appendChild(modal);
47+
48+
const openButton = document.getElementById('openNetworkPicker');
49+
const closeButton = modal.querySelector('.network-modal-close');
50+
51+
populateNetworkLists();
52+
53+
openButton.addEventListener('click', () => {
54+
modal.style.display = 'flex';
55+
updateActiveNetworkInModal();
56+
});
57+
58+
closeButton.addEventListener('click', () => {
59+
modal.style.display = 'none';
60+
hideNetworkError();
61+
});
62+
63+
modal.addEventListener('click', (e) => {
64+
if (e.target === modal) {
65+
modal.style.display = 'none';
66+
hideNetworkError();
67+
}
68+
});
69+
70+
updateCurrentNetworkDisplay();
71+
}

0 commit comments

Comments
 (0)