diff --git a/src/components/connections/index.js b/src/components/connections/index.js index 823238c4..6fc5d007 100644 --- a/src/components/connections/index.js +++ b/src/components/connections/index.js @@ -1,2 +1,3 @@ export * from './connections'; +export * from './networks'; export * from './permissions'; diff --git a/src/components/connections/networks-helpers.js b/src/components/connections/networks-helpers.js new file mode 100644 index 00000000..c57427f3 --- /dev/null +++ b/src/components/connections/networks-helpers.js @@ -0,0 +1,221 @@ +import globalContext from '../..'; + +const NETWORKS = [ + // Main networks + { + name: 'Ethereum Mainnet', + chainId: '0x1', + color: '#627eea', + category: 'main', + }, + { + name: 'Linea', + chainId: '0xe708', + color: '#000000', + category: 'main', + }, + { + name: 'Base Mainnet', + chainId: '0x2105', + color: '#0052ff', + category: 'main', + }, + { + name: 'Arbitrum One', + chainId: '0xa4b1', + color: '#28a0f0', + category: 'main', + }, + { + name: 'Avalanche Network C-Chain', + chainId: '0xa86a', + color: '#e84142', + category: 'main', + }, + { + name: 'Binance Smart Chain', + chainId: '0x38', + color: '#f3ba2f', + category: 'main', + }, + { + name: 'OP Mainnet', + chainId: '0xa', + color: '#ff0420', + category: 'main', + }, + { + name: 'Polygon Mainnet', + chainId: '0x89', + color: '#8247e5', + category: 'main', + }, + { + name: 'Sei Network', + chainId: '0x1a', + color: '#ff6b35', + category: 'main', + }, + { + name: 'zkSync Era Mainnet', + chainId: '0x144', + color: '#8e71c7', + category: 'main', + }, + + // Test networks + { + name: 'Sepolia', + chainId: '0xaa36a7', + color: '#f6c343', + category: 'test', + }, + { + name: 'Linea Sepolia', + chainId: '0xe705', + color: '#000000', + category: 'test', + }, + { + name: 'Mega Testnet', + chainId: '0x1a4', + color: '#ff6b35', + category: 'test', + }, + { + name: 'Monad Testnet', + chainId: '0x1a5', + color: '#ff6b35', + category: 'test', + }, +]; + +export function populateNetworkLists() { + const mainNetworks = document.getElementById('mainNetworks'); + const testNetworks = document.getElementById('testNetworks'); + + NETWORKS.forEach((network) => { + const networkItem = createNetworkItem(network); + + switch (network.category) { + case 'main': + mainNetworks.appendChild(networkItem); + break; + case 'test': + testNetworks.appendChild(networkItem); + break; + default: + break; + } + }); +} + +export function createNetworkItem(network) { + const item = document.createElement('div'); + item.className = 'network-modal-item'; + item.dataset.chainId = network.chainId; + item.innerHTML = ` +
+
+
+
${network.name}
+
${network.chainId}
+
+
+ `; + + item.addEventListener('click', async () => { + hideNetworkError(); // Hide any existing error before attempting to switch + await switchNetwork(network.chainId); + document.querySelector('.network-modal').style.display = 'none'; + }); + + return item; +} + +export function showNetworkError(message) { + // Remove any existing error message + hideNetworkError(); + + // Create error message element + const errorDiv = document.createElement('div'); + errorDiv.id = 'networkError'; + errorDiv.className = 'error-message'; + errorDiv.style.marginTop = '10px'; + errorDiv.style.width = '100%'; + errorDiv.innerHTML = `
${message}
`; + + // Find the network picker button and insert error after it + const networkButton = document.getElementById('openNetworkPicker'); + const cardBody = networkButton.closest('.card-body'); + cardBody.appendChild(errorDiv); + + // Auto-hide after 5 seconds + setTimeout(() => { + hideNetworkError(); + }, 5000); +} + +export function hideNetworkError() { + const existingError = document.getElementById('networkError'); + if (existingError) { + existingError.remove(); + } +} + +export async function switchNetwork(chainId) { + if (!globalContext.provider) { + console.error('No provider available'); + return; + } + + try { + await globalContext.provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId }], + }); + } catch (switchError) { + // This error code indicates that the chain has not been added to MetaMask. + if (switchError.code === 4902) { + const network = NETWORKS.find((n) => n.chainId === chainId); + const networkName = network ? network.name : `Chain ID ${chainId}`; + showNetworkError(`${networkName} is not available in your wallet`); + } else { + console.error('Error switching network:', switchError); + showNetworkError('Failed to switch network'); + } + } +} + +export function updateCurrentNetworkDisplay() { + const currentNetworkName = document.getElementById('currentNetworkName'); + + if (!globalContext.chainIdInt) { + currentNetworkName.textContent = 'Current Network: Not Connected'; + return; + } + const network = NETWORKS.find((n) => { + const networkChainId = parseInt(n.chainId, 16); + return networkChainId === globalContext.chainIdInt; + }); + // Fallback to chain ID if network not found + currentNetworkName.textContent = network + ? `Current Network: ${network.name}` + : `Current Network: Chain ID 0x${globalContext.chainIdInt.toString(16)}`; +} + +export function updateActiveNetworkInModal() { + const networkItems = document.querySelectorAll('.network-modal-item'); + + networkItems.forEach((item) => { + const itemChainId = item.dataset.chainId; + const itemChainIdInt = parseInt(itemChainId, 16); + const isActive = itemChainIdInt === globalContext.chainIdInt; + + if (isActive) { + item.classList.add('active'); + } else { + item.classList.remove('active'); + } + }); +} diff --git a/src/components/connections/networks.js b/src/components/connections/networks.js new file mode 100644 index 00000000..8c69ccb6 --- /dev/null +++ b/src/components/connections/networks.js @@ -0,0 +1,71 @@ +import { + populateNetworkLists, + updateCurrentNetworkDisplay, + updateActiveNetworkInModal, + hideNetworkError, +} from './networks-helpers'; + +export function networksComponent(parentContainer) { + parentContainer.insertAdjacentHTML( + 'beforeend', + `
+
+
+

+ Network Picker +

+ +
+
+
`, + ); + + const modal = document.createElement('div'); + modal.className = 'network-modal'; + modal.innerHTML = ` +
+
+
Select Network
+ +
+
+
+
Main Networks
+
+
+
+
Test Networks
+
+
+
+
+ `; + document.body.appendChild(modal); + + const openButton = document.getElementById('openNetworkPicker'); + const closeButton = modal.querySelector('.network-modal-close'); + + populateNetworkLists(); + + openButton.addEventListener('click', () => { + modal.style.display = 'flex'; + updateActiveNetworkInModal(); + }); + + closeButton.addEventListener('click', () => { + modal.style.display = 'none'; + hideNetworkError(); + }); + + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.style.display = 'none'; + hideNetworkError(); + } + }); + + updateCurrentNetworkDisplay(); +} diff --git a/src/index.css b/src/index.css index 6afd3bcb..5a76cae4 100644 --- a/src/index.css +++ b/src/index.css @@ -108,3 +108,217 @@ header { .warning-invisible { display: none; } + +.networks-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.network-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + text-align: left; + width: 100%; +} + +.network-item:hover { + background: #e9ecef; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.network-item.active { + background: #007bff; + color: white; + border-color: #007bff; +} + +.network-item.active:hover { + background: #0056b3; +} + +.network-name { + font-weight: 500; + font-size: 14px; +} + +.network-chain-id { + font-size: 12px; + opacity: 0.7; + font-family: monospace; +} + +.network-item.active .network-chain-id { + opacity: 0.9; +} + +.network-modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + justify-content: center; + align-items: center; +} + +.network-modal-content { + background-color: white; + border-radius: 12px; + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow: hidden; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); +} + +.network-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid #e9ecef; +} + +.network-modal-header h5 { + margin: 0; + font-weight: 600; + color: #212529; +} + +.network-modal-close { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #6c757d; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background-color 0.2s; +} + +.network-modal-close:hover { + background-color: #f8f9fa; + color: #495057; +} + +.network-modal-body { + padding: 20px 24px; + max-height: 60vh; + overflow-y: auto; +} + +.network-category { + margin-bottom: 24px; +} + +.network-category:last-child { + margin-bottom: 0; +} + +.network-category h6 { + margin: 0 0 12px 0; + font-weight: 600; + color: #495057; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.network-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.network-modal-item { + display: flex; + align-items: center; + padding: 12px 16px; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid transparent; +} + +.network-modal-item:hover { + background-color: #f8f9fa; + border-color: #dee2e6; +} + +.network-modal-item.active { + background-color: #e3f2fd; + border-color: #2196f3; +} + +.network-modal-item-content { + display: flex; + align-items: center; + width: 100%; +} + +.network-modal-item-icon { + width: 32px; + height: 32px; + border-radius: 50%; + margin-right: 12px; + flex-shrink: 0; +} + +.network-modal-item-info { + flex: 1; +} + +.network-modal-item-name { + font-weight: 500; + font-size: 14px; + color: #212529; + margin-bottom: 2px; +} + +.network-modal-item-chain-id { + font-size: 12px; + color: #6c757d; + font-family: monospace; +} + +.network-modal-item.active .network-modal-item-name { + color: #1976d2; + font-weight: 600; +} + +.network-modal-item.active .network-modal-item-chain-id { + color: #1976d2; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .network-modal-content { + width: 95%; + margin: 20px; + } + + .network-modal-body { + padding: 16px 20px; + } + + .network-modal-header { + padding: 16px 20px; + } +} diff --git a/src/index.js b/src/index.js index 1d6629cd..8f7bab13 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ import { NETWORKS_BY_CHAIN_ID } from './onchain-sample-contracts'; import { connectionsComponent, + networksComponent, permissionsComponent, } from './components/connections'; import { @@ -46,6 +47,10 @@ import { } from './components/interactions'; import { sendFormComponent } from './components/forms/send-form'; import { eip5792Component } from './components/transactions/eip5792'; +import { + updateCurrentNetworkDisplay, + updateActiveNetworkInModal, +} from './components/connections/networks-helpers'; const { hstBytecode, @@ -161,6 +166,7 @@ connectionsRow.className = 'row d-flex justify-content-center'; connectionsSection.appendChild(connectionsRow); connectionsComponent(connectionsRow); permissionsComponent(connectionsRow); +networksComponent(connectionsRow); // Connection buttons set up by this file const onboardButton = document.getElementById('connectButton'); @@ -433,6 +439,8 @@ const handleNewChain = (chainId) => { if (!scrollToHandled) { handleScrollTo({ delay: true }); } + updateCurrentNetworkDisplay(); + updateActiveNetworkInModal(); }; function handleNewNetwork(networkId) {