diff --git a/metamask-connect/evm/guides/manage-user-accounts.md b/metamask-connect/evm/guides/manage-user-accounts.md
index 4856c13fccb..ff2d52c7d1b 100644
--- a/metamask-connect/evm/guides/manage-user-accounts.md
+++ b/metamask-connect/evm/guides/manage-user-accounts.md
@@ -311,6 +311,30 @@ This one-step flow is unique to MetaMask Connect EVM's `connectWith` method.
It's not part of Wagmi or other wallet libraries.
:::
+## Switch or re-select accounts
+
+Persisted sessions reconnect silently with the previously used account, so users don't get a chance
+to pick a different one after reconnecting. To re-prompt for account selection, call
+[`connect`](../reference/methods.md#connect) with `forceRequest: true`:
+
+```javascript
+const { accounts } = await evmClient.connect({
+ chainIds: ['0x1'],
+ forceRequest: true,
+})
+// The user is prompted to select an account, even if a session already exists.
+```
+
+:::note `wallet_requestPermissions` and `wallet_revokePermissions`
+The SDK provider intercepts these EIP-2255 methods and maps them to client methods:
+
+- `wallet_requestPermissions` maps to `connect({ forceRequest: true })`. If you call it right after
+ connecting to show the account picker (a common injected-wallet pattern), the user sees a second
+ connection prompt. Skip this call when the provider is the SDK's.
+- `wallet_revokePermissions` maps to `disconnect`.
+
+:::
+
## Best practices
Follow these best practices when authenticating users.
@@ -351,6 +375,13 @@ The following table lists common authentication errors and their codes:
| `-32002` | Request already pending | Disable the connect button while the request is pending. |
| `-32603` | Internal JSON-RPC error | Check if MetaMask is properly installed. |
+:::note
+Cancellations from `connect`, `connectAndSign`, and `connectWith` don't always surface as a
+top-level `code: 4001`. A cancellation can arrive wrapped with an outer `code: 53` and the wallet's
+real code on `err.rpcCode`. Normalize the error before branching on it. See
+[Normalize cancellations from `connect`](../../troubleshooting/index.md#normalize-cancellations-from-connect).
+:::
+
## Next steps
See the following guides to add more functionality to your dapp:
diff --git a/metamask-connect/evm/guides/sign-data/siwe.md b/metamask-connect/evm/guides/sign-data/siwe.md
index 1d60d8d2c0b..2d94cc153af 100644
--- a/metamask-connect/evm/guides/sign-data/siwe.md
+++ b/metamask-connect/evm/guides/sign-data/siwe.md
@@ -109,6 +109,15 @@ The following HTML displays the SIWE button:
Result:
```
+:::caution Sign-only dapps and network configuration
+After `connect` resolves, the provider's active chain follows the wallet's selected network.
+If that network isn't in `api.supportedNetworks`, the `personal_sign` request throws
+`Chain eip155: is not configured in supportedNetworks`.
+Because SIWE users can be on any network, list every network they're likely to be on in
+`supportedNetworks`, or pin the provider to a supported chain after connecting.
+See [Chain not configured in `supportedNetworks`](../../../troubleshooting/index.md#chain-not-configured-in-supportednetworks).
+:::
+
See the [live example](https://metamask.github.io/test-dapp/#siwe) and
[test dapp source code](https://github.com/MetaMask/test-dapp) for more information.
diff --git a/metamask-connect/evm/quickstart/wagmi.md b/metamask-connect/evm/quickstart/wagmi.md
index 670b8b05ef0..8ba55f3959d 100644
--- a/metamask-connect/evm/quickstart/wagmi.md
+++ b/metamask-connect/evm/quickstart/wagmi.md
@@ -97,11 +97,11 @@ TOD0: Update with new screenshot and link
Install MetaMask Connect EVM along with its peer dependencies to an existing React project:
:::note Version requirements
-This quickstart requires `wagmi@^3.6.0`. The `metaMask` connector is imported from the `wagmi/connectors` path (part of the `wagmi` package) and requires `@metamask/connect-evm@^2.1.0`.
+This quickstart requires `wagmi@^3.6.0`. The `metaMask` connector is imported from the `wagmi/connectors` path (part of the `wagmi` package) and requires `@metamask/connect-evm@^2.1.1`.
:::
```bash npm2yarn
-npm install @metamask/connect-evm@^2.1.0 wagmi@^3.6.0 viem@2.x @tanstack/react-query
+npm install @metamask/connect-evm@^2.1.1 wagmi@^3.6.0 viem@2.x @tanstack/react-query
```
### 2. Import required dependencies
@@ -320,7 +320,7 @@ If you previously used `@metamask/sdk` with Wagmi, the MetaMask connector now us
```bash npm2yarn
npm uninstall @metamask/sdk
- npm install @metamask/connect-evm@^2.1.0 wagmi@^3.6.0
+ npm install @metamask/connect-evm@^2.1.1 wagmi@^3.6.0
```
2. Update hook usage for Wagmi v3:
diff --git a/metamask-connect/evm/reference/methods.md b/metamask-connect/evm/reference/methods.md
index 855aedfbd2c..cd7f459f712 100644
--- a/metamask-connect/evm/reference/methods.md
+++ b/metamask-connect/evm/reference/methods.md
@@ -36,22 +36,22 @@ Creates a new EVM client instance.
### Parameters
-| Name | Type | Required | Description |
-| -------------------------- | ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `dapp.name` | `string` | Yes | Name of your dapp. |
-| `dapp.url` | `string` | No | URL of your dapp. In browsers this is often set automatically; required in Node.js and React Native. |
-| `dapp.iconUrl` | `string` | No | URL of your dapp icon. |
-| `dapp.base64Icon` | `string` | No | Base64-encoded icon when a hosted URL is unavailable (for example, some React Native setups). Use instead of `iconUrl` when needed. |
-| `api.supportedNetworks` | `Record` | No | Map of hex chain IDs to RPC URLs for read-only requests and transport. Use [`getInfuraRpcUrls`](#getinfurarpcurls) to generate Infura URLs, then merge custom endpoints. |
-| `ui.headless` | `boolean` | No | Enables or disables [headless mode](../guides/headless-mode.md). The default is `false`. |
-| `ui.preferExtension` | `boolean` | No | Directly connects through the MetaMask extension when it's installed. The default is `true`. |
-| `mobile.preferredOpenLink` | `(deeplink: string) => void` | No | A function that's called to open a deeplink to the MetaMask Mobile App. Required in React Native. |
-| `mobile.useDeeplink` | `boolean` | No | Controls use of deeplinks for mobile connection flows. |
-| `ui.showInstallModal` | `boolean` | No | Shows the MetaMask install modal when the extension isn't detected. |
-| `eventHandlers` | `object` | No | Optional callbacks for connection lifecycle and [provider events](provider-api.md#events). |
-| `debug` | `boolean` | No | Enables verbose SDK logging. The default is `false`. |
-| `skipAutoAnnounce` | `boolean` | No | Opts out of automatic EIP-6963 provider announcement (default since `@metamask/connect-evm` 2.0.0). Call `client.announceProvider()` to announce manually. |
-| `analytics.enabled` | `boolean` | No | Set to `false` to opt out of analytics. |
+| Name | Type | Required | Description |
+| -------------------------- | ---------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `dapp.name` | `string` | Yes | Name of your dapp. |
+| `dapp.url` | `string` | No | URL of your dapp. In browsers this is often set automatically; required in Node.js and React Native. |
+| `dapp.iconUrl` | `string` | No | URL of your dapp icon. |
+| `dapp.base64Icon` | `string` | No | Base64-encoded icon when a hosted URL is unavailable (for example, some React Native setups). Use instead of `iconUrl` when needed. |
+| `api.supportedNetworks` | `Record` | No | Map of hex chain IDs to RPC URLs for read-only requests and transport. Use [`getInfuraRpcUrls`](#getinfurarpcurls) to generate Infura URLs, then merge custom endpoints. |
+| `ui.headless` | `boolean` | No | Enables or disables [headless mode](../guides/headless-mode.md). The default is `false`. |
+| `ui.preferExtension` | `boolean` | No | Directly connects through the MetaMask extension when it's installed. The default is `true`. |
+| `mobile.preferredOpenLink` | `(deeplink: string) => void` | No | A function that's called to open a deeplink to the MetaMask Mobile App. Required in React Native. |
+| `mobile.useDeeplink` | `boolean` | No | Controls use of deeplinks for mobile connection flows. |
+| `ui.showInstallModal` | `boolean` | No | Shows the MetaMask install modal when the extension isn't detected. On mobile web, setting this to `true` also forces the QR/install modal and suppresses the MetaMask app deeplink. Leave at the default `false` for mobile deeplinking. |
+| `eventHandlers` | `object` | No | Optional callbacks for connection lifecycle and [provider events](provider-api.md#events). |
+| `debug` | `boolean` | No | Enables verbose SDK logging. The default is `false`. |
+| `skipAutoAnnounce` | `boolean` | No | Opts out of automatic EIP-6963 provider announcement (default since `@metamask/connect-evm` 2.0.0). Call `client.announceProvider()` to announce manually. |
+| `analytics.enabled` | `boolean` | No | Set to `false` to opt out of analytics. |
### Returns
diff --git a/metamask-connect/evm/reference/provider-api.md b/metamask-connect/evm/reference/provider-api.md
index 5bf69f5bfde..aab1573462d 100644
--- a/metamask-connect/evm/reference/provider-api.md
+++ b/metamask-connect/evm/reference/provider-api.md
@@ -199,6 +199,15 @@ the same permissions.
This means that the provider emits `accountsChanged` when the user's exposed account address changes.
Listen to this event to [handle accounts](../guides/manage-user-accounts.md).
+:::note Account ordering in SDK sessions
+The "most recently used account first" ordering is the injected-provider convention. When you use
+MetaMask Connect, the account order comes from the session's permitted accounts, which isn't a
+selection-first guarantee. Sessions also merge across reconnections, accumulating previously used
+accounts, so a stale account can appear first. Don't assume `accounts[0]` is the active account, and
+don't treat every `accountsChanged` emission as a user-initiated account switch. See
+[Unexpected account switch after reconnect](../../troubleshooting/index.md#unexpected-account-switch-after-reconnect).
+:::
+
### `chainChanged`
```typescript
@@ -210,6 +219,13 @@ The provider emits this event when the currently connected chain changes.
The `chainId` is a hex string (for example, `0x1`), not a decimal number.
Listen to this event to [detect a user's network](../guides/manage-networks.md).
+:::note
+Unlike the injected provider, which emits `chainChanged` only on an actual network switch, MetaMask
+Connect also emits `chainChanged` as part of the connect handshake. If you attach a handler that
+reacts to network changes (for example, reloading the page), it can fire mid-connection. Guard such
+handlers so they don't run during the initial connect.
+:::
+
### `connect`
```typescript
diff --git a/metamask-connect/troubleshooting/index.md b/metamask-connect/troubleshooting/index.md
index 1f921094871..74e8f446574 100644
--- a/metamask-connect/troubleshooting/index.md
+++ b/metamask-connect/troubleshooting/index.md
@@ -116,6 +116,43 @@ try {
}
```
+#### Normalize cancellations from `connect`
+
+Rejections from [`connect`](../evm/reference/methods.md#connect),
+[`connectAndSign`](../evm/reference/methods.md#connectandsign), and
+[`connectWith`](../evm/reference/methods.md#connectwith) don't always surface as a top-level
+`code: 4001`. Depending on the transport, a cancellation can arrive as a wrapped
+[`RPCInvokeMethodErr`](#error-codes) with an outer `code: 53` and the wallet's real code on
+`err.rpcCode` (for example, `4001`). Requests made through
+[`provider.request`](../evm/reference/provider-api.md#request) are normalized to `err.code = rpcCode`,
+but `connect` rejections are not.
+
+Normalize defensively before branching on the error. This mirrors the SDK's own rejection heuristic,
+which checks `code === 4001` or a message containing `reject`, `denied`, or `cancel`:
+
+```javascript
+function isUserRejection(err) {
+ if (typeof err !== 'object' || err === null) {
+ return false
+ }
+ if (err.code === 4001 || err.rpcCode === 4001) {
+ return true
+ }
+ const message = String(err.message ?? '').toLowerCase()
+ return message.includes('reject') || message.includes('denied') || message.includes('cancel')
+}
+
+try {
+ await client.connect({ chainIds: ['0x1'] })
+} catch (err) {
+ if (isUserRejection(err)) {
+ // User cancelled — show retry UI, don't log as an error
+ return
+ }
+ throw err
+}
+```
+
### Connection already pending (code `-32002`)
A previous `connect` call has not yet resolved.
@@ -126,11 +163,25 @@ Do **not** call `connect` again; the original promise resolves once the user act
### Chain not configured in `supportedNetworks`
-The chain ID passed to `connect` or `wallet_switchEthereumChain` is not listed in the
-`api.supportedNetworks` configuration.
-The wallet rejects the request with `err.code === 4902` (unrecognized chain ID).
+This appears in two situations.
+
+**A request throws even though `connect` succeeded.**
+After `connect` resolves, the provider's active chain follows the wallet's selected network.
+If the user's active network isn't in `api.supportedNetworks`, any non-cached request, including
+[`personal_sign`](../evm/reference/json-rpc-api/personal_sign.mdx) for Sign-In with Ethereum, throws:
+
+```text
+Chain eip155: is not configured in supportedNetworks. Requests cannot be made to chains not explicitly configured in supportedNetworks.
+```
+
+This is a plain `Error` without an `err.code`. Through ethers v6 `BrowserProvider` it surfaces as
+`could not coalesce error (... code=UNKNOWN_ERROR)`.
-Add every chain the dapp needs to `supportedNetworks` with a valid RPC URL:
+**A network switch is rejected.**
+When `connect` or `wallet_switchEthereumChain` targets a chain that isn't added to the wallet, the
+wallet rejects with `err.code === 4902` (unrecognized chain ID).
+
+In both cases, add every chain the dapp needs to `supportedNetworks` with a valid RPC URL:
```javascript
const client = await createEVMClient({
@@ -147,6 +198,29 @@ const client = await createEVMClient({
})
```
+#### Sign-only dapps
+
+If your dapp only signs (for example, SIWE login) and never sends transactions, users can be on any
+network when they connect. To avoid the gating error:
+
+- List every network your users are likely to be on in `supportedNetworks`. Ethereum Mainnet (`0x1`)
+ is always included in the connection request as a bootstrap fallback.
+- Alternatively, after `connect` resolves, pin the provider to a supported chain using the
+ `selectedChainId` setter so signature requests always route to a configured chain:
+
+```javascript
+const provider = client.getProvider()
+const { chainId } = await client.connect({ chainIds: ['0x1'] })
+
+// Pin to a supported chain (falling back to mainnet) before signing.
+provider.selectedChainId = client.selectedChainId ?? '0x1'
+
+const signature = await provider.request({
+ method: 'personal_sign',
+ params: [messageHex, address],
+})
+```
+
### QR code not appearing
**Cause A:** Headless mode is enabled but no `display_uri` listener is registered.
@@ -222,6 +296,25 @@ If you cannot upgrade, add `data: blob:` to `connect-src` as a fallback.
For the full reference, see
[Content Security Policy](https://github.com/MetaMask/connect-monorepo#content-security-policy) in `metamask/connect-monorepo`.
+### Mobile shows the QR modal instead of deeplinking to the app
+
+On mobile web, tapping connect shows the QR/install modal instead of deeplinking into the MetaMask
+mobile app.
+
+**Cause:** `ui.showInstallModal` is set to `true`. Although the option reads as desktop-oriented,
+setting it `true` forces the install modal on mobile web too, which suppresses the app deeplink.
+
+**Fix:** Leave `ui.showInstallModal` at its default (`false`). Mobile then deeplinks to the MetaMask
+app, and desktop still shows the QR modal when no extension is detected.
+
+```javascript
+const client = await createEVMClient({
+ dapp: { name: 'My Dapp' },
+ api: { supportedNetworks: { '0x1': '' } },
+ // Omit ui.showInstallModal, or set it to false, so mobile deeplinks work.
+})
+```
+
### MetaMask wallet not appearing in Solana Wallet Adapter
**Cause A:** `createSolanaClient` has not resolved before the `WalletProvider` renders.
@@ -280,6 +373,40 @@ provider.on('wallet_sessionChanged', session => {
Do not call `connect` again immediately on page load if a session already exists.
+### Unexpected account switch after reconnect
+
+After a reconnect, an `accountsChanged` emission lists a previously used account first, and a dapp
+that treats `accounts[0]` as the active account interprets this as a user switching accounts. This
+can trigger unwanted side effects, such as a duplicate SIWE prompt or re-authentication.
+
+**Cause:** Sessions merge across reconnections, so the permitted account set accumulates accounts
+from earlier connections unless the dapp revokes them. The account order in an SDK session isn't a
+selection-first guarantee, so a stale account can appear first, unlike the injected-provider
+convention.
+
+**Fix:** Don't assume `accounts[0]` is the active account for session-based connections. Track the
+account you actually connected with, and compare against it before reacting:
+
+```javascript
+let activeAccount
+
+const { accounts } = await evmClient.connect({ chainIds: ['0x1'], forceRequest: true })
+activeAccount = accounts[0]
+
+const provider = evmClient.getProvider()
+provider.on('accountsChanged', accounts => {
+ if (accounts.length === 0) {
+ // Disconnected.
+ return
+ }
+ // Only react if the connected account actually changed.
+ if (!accounts.includes(activeAccount)) {
+ activeAccount = accounts[0]
+ // Handle a real account switch (for example, re-authenticate).
+ }
+})
+```
+
### `disconnect` doesn't fully disconnect
Calling `disconnect(scopes)` with specific CAIP scopes only revokes those scopes, not the entire