Skip to main content
This guide covers every MetaMask connection scenario you’ll encounter in a headless integration: desktop with the extension installed, desktop without the extension (QR code), and mobile (deeplink). Each scenario uses a different SDK function, but they all follow the same connect-then-verify pattern. Prerequisites:

Scenario 1: Desktop — Extension installed

When the MetaMask browser extension is installed, it announces itself via EIP-6963 and the SDK registers it as a wallet provider. Connect directly through the provider — no QR code or deeplink needed.

Detect the extension

Use getMetaMaskEvmExtensionWalletProviderKey to check whether MetaMask is available as a browser extension. It returns the wallet provider key (e.g. 'metamaskevm') when installed, or undefined when not.

Connect and verify

Pass the detected key to connectAndVerifyWithWalletProvider. This opens the MetaMask extension popup, the user approves the connection, then signs a verification message — all in one call.
If you want to separate connection from verification (e.g. connect first, verify later on a button click):

Scenario 2: Desktop — No extension (QR code)

When MetaMask isn’t installed as a browser extension, use the MetaMask SDK URI pairing. This generates a URI that you render as a QR code. The user scans it with the MetaMask mobile app, approves the connection, and your dapp receives the wallet account.

Connect and verify

connectAndVerifyWithMetaMaskUriEvm returns { uri, approval }. Render uri as a QR code; approval() resolves after the user approves the connection and signs the verification message in their MetaMask mobile app.

Connect without verifying

If you want to defer verification, use connectWithMetaMaskUriEvm instead:

Clear stale sessions

If the user previously connected via QR and the session expired, the MetaMask SDK may spend up to 10 seconds trying to resume it before emitting a fresh URI. Call clearMetaMaskSessionStorage before starting a new pairing to skip the resume timeout:

On mobile, you can’t show a QR code (the user is already on their phone). Instead, generate the same MetaMask SDK URI and append it to MetaMask’s deeplink. This opens the MetaMask app directly, where the user approves the connection.

Connect and verify

Instead of hardcoding the deeplink URL, you can fetch it from the SDK’s wallet catalogue:

Putting it all together

Here’s a complete example that detects the user’s environment and picks the right MetaMask connection flow automatically:

Using the Wallet Options Catalogue

For a more automated approach, getWalletOptionsCatalogue merges all connection methods (extension, MetaMask SDK URI, WalletConnect, in-app browser) into a single ordered list per wallet. MetaMask’s entry will include a withWalletProvider option when the extension is installed, plus metamaskSdkUri options for QR/deeplink.
See Build a Wallet Picker for the full pattern with click handlers.

React Native

The same @dynamic-labs-sdk/evm/metamask functions work unchanged in React Native (Expo or bare) — complete Setup on Bare React Native or Setup on Expo first. As with WalletConnect’s React Native section, there’s no QR code (the user is already on their phone) and no window.open — open MetaMask with Linking.openURL instead. MetaMask also publishes its own guide for building the native connect UI end-to-end: MetaMask Connect: React Native quickstart.

Resolve Metro bundling errors

First, set up the ws alias described in Adding EVM Extensions → React Native — every @dynamic-labs-sdk/evm user on React Native needs it, not just MetaMask. MetaMask’s connect SDK (@metamask/connect-evm, used internally by connectWithMetaMaskUriEvm / connectAndVerifyWithMetaMaskUriEvm) dynamically imports eciesjs in addition to ws. Metro still has to resolve eciesjs at bundle time even though the import is dynamic, and depending on which eciesjs version your install resolves, its package build can trip Metro’s ESM/CJS interop — symptoms range from a bundling error to a runtime crash reading a property off undefined.
We haven’t reproduced an exact, version-independent error signature for this one — if Metro fails while bundling eciesjs, the fix below (forcing resolution to the CJS build) is the one to reach for regardless of the exact message.
Extend the resolveRequest function from the EVM extensions guide with one more branch, right after the config = getDefaultConfig(__dirname) line in that same metro.config.js:
metro.config.js
Using pnpm with strict node_modules? require.resolve('eciesjs') only finds packages your own package.json declares. Add eciesjs as a direct devDependency if Metro reports it can’t be resolved.

Last modified on August 12, 2026