A cross-wallet onboarding flow is a critical component for any Web3 application aiming for broad user adoption. Unlike a single-wallet integration, it supports multiple wallet providers—such as MetaMask, WalletConnect, Coinbase Wallet, and Phantom—through a single, consistent interface. The primary goal is to reduce friction for new users by detecting installed wallets, offering installation prompts for missing ones, and handling connection logic uniformly. This approach is essential because wallet preference is highly fragmented; no single provider dominates all user segments or blockchains.
How to Design a Cross-Wallet User Onboarding Flow
How to Design a Cross-Wallet User Onboarding Flow
A cross-wallet onboarding flow allows users to connect any wallet from a supported list, abstracting away the complexity of individual wallet SDKs and providing a unified user experience.
The technical architecture relies on a modular adapter pattern. Instead of writing direct integrations for each wallet's unique SDK (e.g., @metamask/sdk, @coinbase/wallet-sdk), you implement a standard interface. Each wallet adapter is responsible for its specific connection logic, event listeners, and chain switching. A central manager, like wagmi or web3-react, can orchestrate these adapters. This design allows you to add support for a new wallet by creating a single adapter module without refactoring your core connection UI or state management logic.
User experience design follows a clear decision tree. The flow typically starts with a button like "Connect Wallet." Upon click, a modal displays available wallet options. Wallet detection is key: use window.ethereum checks for EVM browsers or injectors, and consider using libraries like @web3-onboard for broader detection. For wallets not detected (e.g., a mobile wallet on desktop), show an installation prompt with a direct link. The connection state—connecting, connected, wrong network, disconnected—should provide clear, actionable feedback to the user at each step.
State management must handle the asynchronous and often error-prone nature of wallet interactions. Your application state should track the connected wallet's provider, account address, and chain ID. Listen for critical events: accountsChanged, chainChanged, and disconnect. For example, when accountsChanged fires with an empty array, the user has disconnected all accounts in their wallet UI. Robust error handling for rejected requests (like a user denying a connection) is mandatory to prevent the UI from hanging and to guide the user to retry.
A complete implementation includes post-connection steps vital for security and usability. Always verify the connected chain matches your application's required network (e.g., Ethereum Mainnet, Arbitrum). If not, prompt the user to switch networks programmatically using the wallet's wallet_switchEthereumChain method. Subsequently, it is a best practice to perform a signature verification request, such as personal_sign, to cryptographically prove the user controls the address. This prevents session spoofing. Finally, sync the derived user state (like balances or NFTs) with your application's backend or indexer.
How to Design a Cross-Wallet User Onboarding Flow
Before building a cross-wallet onboarding flow, you need to understand the core concepts and technical components that enable users to connect from any wallet.
A cross-wallet onboarding flow allows users to connect to your dApp using any Ethereum Virtual Machine (EVM) compatible wallet, such as MetaMask, Coinbase Wallet, or WalletConnect. The foundation is the EIP-1193 standard, which defines a universal provider interface for Ethereum. This standard is what allows libraries like ethers.js or viem to interact with a user's wallet through a common window.ethereum object, regardless of the specific wallet extension or mobile app. Understanding this abstraction layer is the first prerequisite for designing a flexible connection experience.
You must implement a wallet detection and connection logic that handles both injected providers (browser extensions) and external connectors. For injected wallets, you check for window.ethereum. For others, like WalletConnect or Coinbase Wallet SDK, you initialize a new provider instance. A robust flow will also manage chain switching, prompting users to connect to the correct network (e.g., Ethereum Mainnet, Arbitrum, Base) if their wallet is on an unsupported chain. This often involves calling methods like wallet_switchEthereumChain as defined in EIP-3326.
State management is critical for tracking the user's connection status, account address, and active chain ID across your application. Use a context provider or global state store (like Zustand or Redux) to make this data accessible. Your flow should also handle event listeners for accounts changed (accountsChanged) and chain changed (chainChanged) events to update the UI in real time. Always design with security in mind: never store private keys or seed phrases, and treat all user input from the wallet as untrusted.
A key user experience consideration is session persistence. Using localStorage or a similar mechanism to remember a user's connected wallet type (e.g., "injected" or "walletconnect") between page reloads improves usability. However, you should never store sensitive connection details. Instead, persist only a connector identifier and re-initialize the connection flow silently on revisit, prompting the user to approve the connection again from their wallet for security. This balances convenience with the self-custodial nature of Web3.
Finally, your design must account for mobile and desktop environments. Injected wallets work on desktop browsers, while mobile users often connect via WalletConnect QR codes or dedicated wallet app deep links (like wc: or ethereum: URLs). Test your flow on multiple platforms. Provide clear error states for common issues: rejected connection requests, missing wallet extensions, or unsupported networks. A well-designed onboarding flow is the first and most important interaction a user has with your dApp.
Key Concepts for Cross-Wallet Onboarding
Designing a seamless onboarding flow requires understanding wallet standards, connection patterns, and user experience best practices. This guide covers the essential technical concepts for integrating multiple wallet types.
State Management & Session Persistence
Maintaining wallet state across page reloads is critical for UX. Simply storing the address is insufficient; you must also track the connected chain ID and provider type.
- Storage Strategy: Use
localStorageorIndexedDBto persist the connector type (e.g.,"injected","walletconnect"), account address, and chain ID. - Reconnection Logic: On app load, read stored state and attempt to reconnect (e.g., re-initialize WalletConnect session).
- Listeners: Subscribe to provider events like
accountsChanged,chainChanged, anddisconnectto update your app state in real time.
Step 1: Detecting Injected Wallet Providers
The first technical step in a cross-wallet onboarding flow is detecting which wallets are available in the user's browser. This guide explains how to check for the `window.ethereum` object and identify specific providers.
Modern Ethereum wallets like MetaMask, Coinbase Wallet, and Rabby operate by injecting a provider object into the user's browser window, typically under window.ethereum. This object is the primary interface your dApp uses to request blockchain interactions, such as connecting accounts, signing messages, and sending transactions. The detection process is asynchronous and must account for the possibility that no wallet is installed, or that the page is still loading.
The simplest check is to see if the window.ethereum object exists. However, since the introduction of EIP-1193 and the rise of multiple, simultaneous wallet injections, this object can be an array of providers. You should always check for this array. A robust detection function looks like this:
javascriptfunction getWalletProviders() { if (typeof window === 'undefined') return []; const ethereum = window.ethereum; if (!ethereum) return []; // Handle both single provider and array of providers const providers = Array.isArray(ethereum.providers) ? ethereum.providers : [ethereum]; return providers; }
Once you have the provider array, you can identify specific wallets. Many wallets add identifying properties to their provider object. For example, MetaMask sets window.ethereum.isMetaMask to true, Coinbase Wallet sets window.ethereum.isCoinbaseWallet, and Rabby sets window.ethereum.isRabby. You can iterate through your provider list and check for these flags to present the user with a list of detected wallets. It's crucial to handle the case where the user has multiple wallets installed, as they may need to select one.
Timing is critical. Wallet injection can happen after your dApp's initial JavaScript execution. You must listen for the ethereum#initialized event on the window object to detect a provider that loads late. The standard pattern is to set up an event listener immediately, then check for existing providers. This ensures you capture all possibilities.
javascript// Listen for late-injecting wallets window.addEventListener('ethereum#initialized', handleEthereum, { once: true, }); // Check if already available if (window.ethereum) { handleEthereum(); }
After detection, your UI should clearly communicate the state to the user. Show a list of detected wallets with their correct names and logos. If no wallet is found, provide clear instructions and links to install popular options like MetaMask or Coinbase Wallet. Never auto-connect or trigger a connection prompt without explicit user action, as this is a poor user experience and may be blocked by browser pop-up blockers.
This detection layer forms the foundation of a reliable onboarding flow. The next step is using the detected provider to request account access via the eth_requestAccounts RPC method, which we will cover in the following section. Proper error handling here—for rejected requests, wrong networks, or disconnected providers—is essential for a polished dApp.
Step 2: Implementing WalletConnect v2 for Mobile
This guide details the technical implementation of WalletConnect v2 for mobile applications, covering client setup, session management, and transaction signing.
To begin, install the official WalletConnect SDKs. For React Native, use @walletconnect/modal-react-native and @walletconnect/universal-provider. For native iOS or Android, use the Kotlin/Swift libraries. Initialize the UniversalProvider with your project's metadata and the required namespaces, which define the blockchains and methods your app will use. For an EVM-focused app, your core namespace would be eip155 with chains like eip155:1 (Ethereum Mainnet) and methods like eth_sendTransaction. This setup is crucial for establishing a shared protocol between your app and any connecting wallet.
The connection flow starts by calling provider.connect() with your namespace configuration. This returns a URI and a pairing topic. You must present this URI to the user as a QR code (using a library like react-native-qrcode-svg) or via a deep link. When a wallet scans the QR code, it establishes a pairing. Listen for the session_proposal event to receive the wallet's approval, which includes the wallet's addresses and supported methods. Always validate the proposal's requiredNamespaces against your app's requirements before approving it with provider.approveSession() to establish the active session.
Once a session is active, you can request transactions or signatures. Use provider.request() with the session topic, chain ID, and request parameters. For a transfer, the request would specify method eth_sendTransaction with parameters like from, to, value, and gas. The request is sent to the wallet, and you must listen for the response on the same topic. Handle all possible outcomes: the session_request event for a successful response, session_delete for a disconnected session, and session_event for other notifications. Implement robust error handling for rejected requests or network issues.
Managing session state is critical for user experience. Store the active session's topic and pairing data persistently (e.g., using AsyncStorage or SecureStore). This allows your app to reconnect to existing sessions when restarted by calling provider.connect({ pairingTopic: savedTopic }). You should also listen for session_expire events and clean up local state. Provide users with a clear way to disconnect via provider.disconnect(), which triggers a session_delete event. For security, never log or transmit private keys or mnemonics; WalletConnect only exchanges signed messages and transaction payloads.
For advanced functionality, leverage WalletConnect's multi-chain capabilities. A single session can include multiple namespaces (e.g., eip155 for EVM and solana for Solana). You can also use the Auth module to request wallet signatures for login, compliant with EIP-4361 (Sign-In with Ethereum). When debugging, use the WalletConnect Cloud Relay Explorer to monitor relayed messages. Always test your integration on testnets like Sepolia or Goerli before mainnet deployment, and refer to the official WalletConnect Docs for the latest API specifications and best practices.
Step 3: Handling Chain-Specific Account Formats
A robust onboarding flow must seamlessly translate between the diverse account formats used across different blockchain ecosystems.
The core challenge in cross-wallet onboarding is that not all chains use the same account format. Ethereum Virtual Machine (EVM) chains like Ethereum, Arbitrum, and Polygon use Externally Owned Accounts (EOAs) with 20-byte hexadecimal addresses (e.g., 0x742d35Cc6634C0532925a3b844Bc9e...). In contrast, non-EVM chains have fundamentally different systems. Solana uses Ed25519 key pairs, resulting in a Base58-encoded public key. Bitcoin and its derivatives use a different derivation path and encoding entirely. Your flow must detect the target chain and handle its native format correctly.
To manage this, implement a chain registry in your application. This is a mapping of chainId to format metadata. For each supported chain, store its address validation regex, display prefix (e.g., 0x for EVM), and the appropriate Address Checksum rule. For EVM addresses, always use the checksummed version defined in EIP-55, which mixes uppercase and lowercase letters to provide a checksum. Use libraries like ethers.js's getAddress() or viem's getAddress function to perform this formatting automatically and prevent user error from invalid or non-checksummed inputs.
When a user connects a wallet, you must request the account in the chain's native format. For EVM wallets via EIP-1193 (window.ethereum), you call eth_requestAccounts. For Solana wallets via the Wallet Standard, you use connect(). The returned account must then be normalized. For example, convert a Solana PublicKey to its base58 string, or ensure an EVM address is checksummed. Always display this normalized format to the user for confirmation. This step is critical for preventing funds from being sent to an incorrectly formatted or misinterpreted address, which would result in permanent loss.
Your backend systems must also be format-aware. When storing user accounts, save both the normalized address and the chainId it belongs to. For operations like querying balances or broadcasting transactions, your API endpoints must accept the chain-specific format and use the appropriate RPC provider. Never assume an Ethereum address can be used directly on Solana's RPC, or vice-versa. This chain-context pairing is essential for all subsequent operations, from displaying NFTs to constructing cross-chain messages.
Finally, design clear user interface cues. When displaying an address, prefix it with the chain's logo or name (e.g., ETH: 0x...). If your flow involves copying an address, ensure the clipboard receives the correctly formatted string. For advanced flows involving account abstraction (ERC-4337) or smart contract wallets, the principle remains: abstract the complexity from the user while rigorously handling the technical specifics under the hood to guarantee security and interoperability across the fragmented chain landscape.
Wallet Provider Comparison
Key technical and user experience differences between popular wallet connection SDKs for onboarding.
| Feature / Metric | WalletConnect v2 | RainbowKit | Dynamic | Web3Modal |
|---|---|---|---|---|
SDK Bundle Size | ~180 KB | ~250 KB | ~350 KB | ~220 KB |
Auto-Connect Support | ||||
SIWE (Sign-In with Ethereum) | ||||
Social / Email Login | ||||
Gas Sponsorship API | ||||
Average Connection Time | < 2 sec | < 3 sec | < 5 sec | < 3 sec |
Multi-Chain Auto-Switch | ||||
Custom UI Styling | Limited | High | High | Moderate |
Smart Wallet Abstraction |
Step 4: Building a Unified UX Flow
Design a seamless, wallet-agnostic onboarding experience that guides users from connection to their first transaction.
A unified UX flow abstracts the complexities of multiple wallet providers behind a single, consistent interface. The core challenge is handling the asynchronous and varied nature of wallet connections—some wallets inject an ethereum object via window.ethereum, others require deep linking to mobile apps, and some use WalletConnect's modal. Your application should detect available options and present a unified connection button. Use libraries like wagmi or Web3Modal to manage this logic, which handle provider detection, connection state, and chain switching, providing a normalized interface for your app's components.
After connection, the flow must guide users through essential post-connection steps. This often includes a network validation check to ensure the user is on the correct blockchain (e.g., Ethereum Mainnet, Polygon). If not, present a clear, one-click button to switch networks using methods like wallet_switchEthereumChain. Following this, implement a balance and token check. Inform the user if they lack the native token (e.g., ETH for gas) or a specific ERC-20 token required for your dApp's core action. Provide clear next steps, such as a link to a fiat on-ramp or a bridge.
The final stage is designing the core transaction flow. Use a multi-step modal pattern to break down complex interactions. For a swap, this could be: 1) Select tokens and amounts, 2) Review quote and slippage, 3) Sign approval (if needed for an ERC-20), 4) Confirm the main transaction. Each step should have clear titles, actionable buttons, and status indicators. Implement robust transaction state feedback: 'Preparing...', 'Awaiting Wallet Confirmation', 'Processing...', 'Success/Failed'. Always link to a block explorer like Etherscan upon completion. This guided, transparent process significantly reduces user error and abandonment.
Common Issues and Troubleshooting
Addressing frequent developer challenges when designing user flows that work across multiple wallet providers like MetaMask, WalletConnect, and Coinbase Wallet.
Wallet detection fails primarily due to timing issues or missing provider injections. The global window.ethereum object is not always immediately available.
Common causes and fixes:
- Race Conditions: Check for the provider after the window
loadevent or useDOMContentLoaded. Implement a retry mechanism with a timeout (e.g., 3 seconds). - Multiple Wallets: Users may have several wallet extensions. Use
window.ethereum.providers(EIP-5749) to detect an array or use a library like@web3-onboardorwagmithat handles provider discovery. - Non-EVM Chains: For Solana (
window.solana), Cosmos (window.keplr), etc., you must check for chain-specific objects. Always implement a fallback UI prompting users to install a wallet.
Example detection snippet:
javascriptasync function connectWallet() { if (window.ethereum) { // Modern multi-wallet detection const provider = window.ethereum.providers?.[0] || window.ethereum; await provider.request({ method: 'eth_requestAccounts' }); } else { // Show install prompt document.getElementById('wallet-missing').style.display = 'block'; } }
Essential Resources and Tools
Designing a cross-wallet user onboarding flow requires standard discovery, consistent signing UX, and minimal wallet-specific logic. These resources focus on concrete protocols, SDKs, and design patterns used in production Web3 applications.
Onboarding UX Patterns for Cross-Wallet Consistency
Beyond tooling, onboarding UX patterns determine whether users successfully connect across wallets and devices.
Proven patterns:
- Wallet selection before connection, not after errors
- Explicit network display and automatic chain switching
- Progressive disclosure of permissions and signing requests
Anti-patterns to avoid:
- Auto-connecting to the first injected wallet
- Triggering signature requests without context
- Forcing network changes before explaining why
Production teams often log wallet type, connection errors, and rejection reasons to iteratively improve onboarding. Even small changes, like previewing the exact message to be signed, significantly reduce drop-off across MetaMask, WalletConnect, and mobile wallets.
Frequently Asked Questions
Common technical questions and solutions for developers implementing cross-wallet user onboarding.
Implement a multi-wallet connector library like WalletConnect v2, Web3Modal, or RainbowKit. These libraries provide a unified interface that abstracts the detection of injected providers (e.g., MetaMask) and connection methods for non-injected wallets (e.g., mobile wallets via QR codes). The key is to handle the window.ethereum provider for EVM chains while also supporting WalletConnect's universal linking. For a robust implementation, listen for the accountsChanged and chainChanged events on the active provider to keep your app state synchronized. Always provide a clear disconnect function to reset the connection state.
Conclusion and Next Steps
This guide has outlined the core principles and technical components for building a cross-wallet onboarding flow. The next step is to implement these concepts in a real application.
To begin implementation, start by integrating a robust wallet connection library like WalletConnect v2 or Dynamic. These SDKs abstract the complexity of connecting to dozens of wallets, providing a unified API for eth_requestAccounts, signing, and listening to account changes. Your first milestone should be a functional connection modal that lists popular wallets (MetaMask, Coinbase Wallet, Phantom) and displays the user's connected address and chain ID. Use the useAccount and useConnect hooks from wagmi or similar to manage this state in your React or Vue application.
Next, focus on the post-connection UX. Implement logic to check the user's network and prompt them to switch to your app's required chain using methods like wallet_switchEthereumChain. For cross-chain apps, use a bridge aggregator API like Socket or LI.FI to facilitate asset transfers. A critical step is session persistence; store the connection state and selected wallet type (e.g., injected, walletConnect) in localStorage or a state manager, and attempt to auto-reconnect on the next visit using the library's built-in methods.
Finally, iterate based on analytics and user feedback. Track metrics such as connection success rate, time-to-connect, and fallback method usage (e.g., how many users switch from WalletConnect to email). Use A/B testing to compare different modal designs or wallet listing orders. For further learning, study the documentation of the major wallet providers, review the source code of leading dApps like Uniswap or OpenSea, and explore advanced patterns for smart account (ERC-4337) onboarding. The goal is a seamless, secure gateway that removes friction for both crypto-native and new users.