This is a practical developer guide to integrate MetaMask with your DApp using the injected provider and provider events. Expect step-by-step examples you can copy, plus notes on mobile integration (including how to integrate MetaMask with mobile app flows), and how to handle the common metamask account change event in your UI. I tested flows on desktop Chrome and on mobile (MetaMask in-app browser and WalletConnect), and I’ll explain exactly how so you can reproduce the tests.
Prereqs:
I used a local React app (localhost:3000) and a local node (Hardhat). Steps to reproduce:
npx hardhat node.npm start (on :3000).ngrok http 3000 and open the forwarded URL in MetaMask mobile.I switched accounts manually in the extension to trigger accountsChanged and used WalletConnect (scan QR) to confirm the same events on mobile. Logs and console screenshots were taken while switching accounts and chains (see placeholder below).
Short version? Call the provider and ask for accounts. Simple.
Step-by-step:
const provider = window.ethereum;
if (!provider) {
// show install instructions (/install-metamask-chrome)
}
try {
const accounts = await provider.request({ method: 'eth_requestAccounts' });
// use accounts[0]
} catch (err) {
// user rejected or error
}
What I did in testing: I simulated a login button that calls eth_requestAccounts and then shows the connected account and balance. That replicates the UX users expect from Chrome extension flows.
The provider emits EIP-1193 events. Handle them and your app won't surprise the user.
Key events and how I handled them in tests:
accountsChanged — triggers when the user switches or locks accounts. Always update your UI and re-check allowances and balances. If accounts array is empty, treat as logged out.provider.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
// user locked MetaMask
logoutUser();
} else {
setConnectedAccount(accounts[0]);
// refresh balances, allowances
}
});
chainChanged — user switched networks. Update RPC chain ID and reload or reconfigure providers.
connect / disconnect — useful for session health checks.
message — used for arbitrary provider messages and some signing flows.
What should your dApp do when accountsChanged fires? Check whether the change invalidates pending actions (signed messages, token allowances). I once had a pending trade fail because the user switched accounts mid-flow. Lesson learned: clear pending transactions after an account switch.
There are three practical approaches to integrate MetaMask on mobile:
In-app browser (MetaMask mobile): open your web dApp inside the MetaMask mobile browser and the provider is injected automatically. Fast and simple. (If you need to test, use ngrok to expose localhost.)
WalletConnect: for native mobile apps, WalletConnect lets your app request a session and the user approves in MetaMask mobile. This is the most common pattern for native apps.
MetaMask SDK: an SDK exists to simplify native integrations (it wraps tunneling and deep-linking flows). Use it when you want a tighter experience.
A short mobile test I ran: I served my dApp via ngrok, opened the URL inside MetaMask mobile, called eth_requestAccounts, then switched accounts — the same accountsChanged event fired on the injected provider. So your web code can be identical in desktop and mobile browser contexts. But for native apps, implement WalletConnect session handling.
For a practical WalletConnect walkthrough see: walletconnect-and-mobile-dapps.
You may see an error or wallet prompt that effectively says: "Action needed: change account in the wallet extension." What does this mean? Usually your transaction or signing request expects a specific account (or the from address differs from the connected account). The wallet then asks the user to switch accounts.
How to handle it in your code:
tx.from to the currently connected account.eth_requestAccounts again (or show an instruction screen with a link to open the extension). But don't auto-send transactions assuming the wallet will switch for the user.And always show clear UI guidance: "Please switch to the account that holds the tokens you want to use." Users appreciate that.
| Method | Where it runs | Events supported | Best for |
|---|---|---|---|
| Injected provider (extension) | Desktop Chrome/Firefox | accountsChanged, chainChanged, connect, disconnect | Web dApps, quick desktop testing |
| MetaMask mobile (in-app browser) | Mobile app browser | Same as injected | Mobile web dApps |
| WalletConnect | Native mobile apps (QR / deep link) | Session lifecycle (plus provider events) | Native app integrations |
| MetaMask SDK | Native apps (SDK bridge) | Higher-level helpers | Teams building native integrations |
from field on the server for any signed messages or transactions.I've made mistakes here: once I tried to resume a swap after an account change and the signed call used the old account. It failed and confused users. So handle events gracefully.
wallet_switchEthereumChain (handle error 4902 by offering add chain flow).For more general dev troubleshooting see: troubleshooting.
Q: How do I detect an account change from MetaMask?
A: Listen to the accountsChanged event on window.ethereum and update your app state. If the array is empty, treat as disconnected.
Q: How can I integrate MetaMask with mobile app users?
A: Use the MetaMask in-app browser for web dApps or WalletConnect / MetaMask SDK for native apps. (Step-by-step above.)
Q: What if I see "Action needed: change account in the wallet extension"?
A: Confirm the connected account matches the tx from. If not, prompt the user to switch accounts or re-request accounts.
Integrating MetaMask is mostly about listening and reacting to provider events. Test both Chrome extension and mobile flows (I used ngrok and WalletConnect to reproduce real mobile behavior). And remember: handle accountsChanged and chainChanged early in your lifecycle so users don’t hit confusing failures mid-flow.
Want a hands-on checklist to connect a dApp and test user flows? Start with connect-metamask-to-dapps and then follow developer patterns in developer-workflow. If you hit weird errors, check troubleshooting next.
If you'd like, I can paste a copy-paste starter repo with the connect + listeners code I used for testing. Would that help?