terry-wallet-login #15
103
.unipi/docs/debug/2026-06-02-metamask-wallet-login-debug.md
Normal file
103
.unipi/docs/debug/2026-06-02-metamask-wallet-login-debug.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: "MetaMask Wallet Login Does Not Surface Address — Debug Report"
|
||||
type: debug
|
||||
date: 2026-06-02
|
||||
severity: high
|
||||
status: root-caused
|
||||
---
|
||||
|
||||
# MetaMask Wallet Login Does Not Surface Address — Debug Report
|
||||
|
||||
## Summary
|
||||
MetaMask QR login and mobile deeplink login can be approved in MetaMask, but the ARK frontend does not write the approved wallet address into the local wallet session; the mobile QR waiting text is also incorrectly TokenPocket-specific for all wallets.
|
||||
|
||||
## Expected Behavior
|
||||
- Selecting MetaMask and approving the connection should result in the page showing the connected wallet address.
|
||||
- Mobile MetaMask deeplink login should return/reconnect to the page and complete local no-signature login with `local-wallet:<address>`.
|
||||
- QR login copy should be generic or absent; it should not say “waiting in TokenPocket” when the selected wallet is MetaMask or imToken.
|
||||
|
||||
## Actual Behavior
|
||||
- Terry can approve MetaMask QR/deeplink login, but the web page does not show the authorized address.
|
||||
- The QR panel uses TokenPocket-specific copy for any wallet on mobile, e.g. imToken QR login still shows a TokenPocket waiting message.
|
||||
|
||||
## Reproduction Steps
|
||||
1. Open the public frontend and open the wallet login modal.
|
||||
2. Select MetaMask.
|
||||
3. Use either:
|
||||
- QR login: scan the QR using MetaMask and approve, or
|
||||
- Mobile app login: tap “Open wallet app”, approve in MetaMask, then return to the web page.
|
||||
4. Observe that the page does not show the wallet address.
|
||||
5. Select imToken QR login on mobile and observe that the QR panel displays TokenPocket-specific waiting text.
|
||||
|
||||
## Environment
|
||||
- Project: Arkie Library Frontend (`ark-database-web`)
|
||||
- Branch context: `terry-wallet-login`
|
||||
- Stack: React 18, Vite, TypeScript, RainbowKit, Wagmi, WalletConnect/Reown
|
||||
- Wallets involved: MetaMask Mobile, TokenPocket, imToken
|
||||
- Backend auth endpoints intentionally not required for this flow; login is local no-signature wallet session.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### Failure Chain
|
||||
1. `WalletLoginModal` calls `wc.start(kind, mode)` for all wallet app/QR flows.
|
||||
2. `useWalletConnectLogin.start()` currently chooses the first generic WalletConnect connector for non-injected flows:
|
||||
- `connectors.find((item) => item.type === "walletConnect") ?? connectors.find((item) => item.id === "walletConnect")`
|
||||
3. On MetaMask mobile, RainbowKit’s own MetaMask wallet definition intentionally uses Wagmi’s `metaMask()` / MetaMask SDK connector, not the generic WalletConnect connector.
|
||||
4. The custom hook bypasses that MetaMask-specific connector on mobile, so MetaMask SDK deeplink/reconnect handling is not used.
|
||||
5. The hook only calls `completeLogin(localWalletToken(address), address)` inside the awaited `connectAsync(...)` result path.
|
||||
6. If MetaMask approval completes while the browser is backgrounded, after a page reload, or through a restored Wagmi connection, there is no `useAccount`/reconnect bridge that converts the Wagmi connected address into the app’s local wallet session.
|
||||
7. `RainbowWalletProvider` calls `useReconnect()`, but this only restores Wagmi connection state; it does not update `WalletProvider` unless `useWalletConnectLogin` observes the restored account and calls `completeLogin`.
|
||||
8. Therefore MetaMask may be approved/connected at the wallet/Wagmi layer but the ARK UI still has no `local-wallet:<address>` token and shows logged-out/no address.
|
||||
|
||||
### Root Cause
|
||||
The MetaMask flow is treated as a generic WalletConnect flow, but RainbowKit/Wagmi have MetaMask-specific mobile behavior. Additionally, local ARK wallet login is tied only to the synchronous `connectAsync` return path instead of being derived from Wagmi account state/reconnect events. This misses MetaMask connections that resolve via mobile app backgrounding, deep link return, QR approval, or page reload.
|
||||
|
||||
### Evidence
|
||||
- File: `src/wallet/useWalletConnectLogin.ts` — connector selection prefers `item.type === "walletConnect"` for all wallets, so mobile MetaMask does not use the RainbowKit/Wagmi MetaMask connector.
|
||||
- File: `src/wallet/useWalletConnectLogin.ts` — `completeLogin(localWalletToken(...), ...)` only runs after `await connectAsync(...)`; there is no `useAccount` effect to complete local login when Wagmi is already/reconnected.
|
||||
- File: `src/wallet/RainbowWalletProvider.tsx` — `WalletReconnectOnMount` calls `useReconnect()`, but no downstream code maps the restored Wagmi account into `WalletProvider`.
|
||||
- File: `node_modules/@rainbow-me/rainbowkit/dist/wallets/walletConnectors/chunk-BQHQU37S.js` — RainbowKit MetaMask wallet uses `metaMask()` connector on mobile and comments that “MetaMask mobile deep linking [is] handled by wagmi”. The custom hook bypasses this by selecting a generic WalletConnect connector.
|
||||
- File: `src/wallet/WalletLoginModal.tsx` — QR text uses `mobileDevice ? t("walletTpWaiting") : t("walletQrUseAnotherDevice")` for every selected wallet, causing TokenPocket-specific copy for MetaMask/imToken.
|
||||
- File: `src/locales/en.ts` and `src/locales/zh-CN.ts` — `walletQrUseAnotherDevice` also explicitly mentions TokenPocket, so even desktop/generic QR copy is wallet-specific.
|
||||
|
||||
## Affected Files
|
||||
- `src/wallet/useWalletConnectLogin.ts` — connector choice, deeplink generation, QR URI generation, local-login completion.
|
||||
- `src/wallet/WalletLoginModal.tsx` — misleading QR panel copy.
|
||||
- `src/wallet/RainbowWalletProvider.tsx` — currently reconnects Wagmi but does not by itself complete local login.
|
||||
- `src/locales/*.ts` — QR copy currently contains TokenPocket-specific text.
|
||||
|
||||
## Suggested Fix
|
||||
Use the wallet-specific connector when a wallet is selected, especially MetaMask, and add a Wagmi account/reconnect bridge that completes the local wallet session whenever Wagmi has an address. Remove or generalize TokenPocket-specific QR waiting copy.
|
||||
|
||||
### Fix Strategy
|
||||
1. In `useWalletConnectLogin.ts`, prefer a connector matching `preferredWallet` before falling back to generic WalletConnect:
|
||||
- MetaMask: `id === "metaMask"` or `type === "metaMask"`
|
||||
- imToken/TokenPocket: matching wallet id when present, otherwise generic WalletConnect
|
||||
2. Add `useAccount()` inside `useWalletConnectLogin` and a `useEffect` that calls `completeLogin(localWalletToken(address), address)` when Wagmi reports `isConnected && address`.
|
||||
3. For MetaMask QR, transform the displayed QR value with the same wallet-specific URI RainbowKit uses: `https://metamask.app.link/wc?uri=<encoded_wc_uri>` instead of always rendering raw `wc:`.
|
||||
4. Remove the QR panel paragraph entirely, or replace it with generic copy such as “Please approve the connection in your wallet app.”
|
||||
5. Re-test MetaMask separately for desktop QR scan, mobile Chrome deeplink return, and MetaMask in-app browser injected login.
|
||||
|
||||
### Risk Assessment
|
||||
- Risk: Selecting MetaMask’s SDK connector may behave differently from WalletConnect for QR mode. Mitigate by falling back to WalletConnect if the MetaMask connector does not emit a `display_uri` in QR mode.
|
||||
- Risk: Auto-completing local login from any Wagmi connected account may log in a stale account after reconnect. Mitigate by only completing when modal/pending/login-in-progress context exists or by clearing stale flags.
|
||||
- Risk: Changing QR value for MetaMask may affect wallets that scan raw WalletConnect URIs. Mitigate by only applying MetaMask app-link QR transformation for MetaMask.
|
||||
|
||||
## Verification Plan
|
||||
1. Run `npx tsc --noEmit`.
|
||||
2. Run `npm run format:check`.
|
||||
3. Run `npm test`.
|
||||
4. Desktop Chrome + MetaMask Mobile QR: Select MetaMask → QR login → scan/approve → confirm address and `local-wallet:<address>`.
|
||||
5. Mobile Chrome + MetaMask app login: Select MetaMask → Open wallet app → approve → return/refresh browser → confirm address appears.
|
||||
6. Regression test TokenPocket and imToken app login.
|
||||
7. Confirm imToken/MetaMask QR login no longer displays TokenPocket-specific text.
|
||||
|
||||
## Related Issues
|
||||
- TokenPocket was previously fixed by handling mobile return/reload behavior.
|
||||
- imToken was previously fixed by adding in-app browser fallback and injected no-signature login.
|
||||
- Existing local-memory context indicates MetaMask QR approval has been observed to not update frontend state.
|
||||
|
||||
## Notes
|
||||
- This report is diagnosis only; no source fix was applied during `/unipi:debug`.
|
||||
- The local no-signature session model means the frontend does not need backend wallet nonce/verify endpoints for this fix.
|
||||
- The current debug UI showing `Wallet debug` may be useful during verification but should be removed or hidden before final production cleanup if no longer needed.
|
||||
@@ -165,13 +165,8 @@ export function WalletLoginModal() {
|
||||
) : null}
|
||||
|
||||
{selected && wc.qrUri ? (
|
||||
<div className="mt-4 grid place-items-center gap-2 rounded-2xl bg-white p-4 text-center">
|
||||
<div className="mt-4 grid place-items-center rounded-2xl bg-white p-4 text-center">
|
||||
<QRCodeSVG value={wc.qrUri} size={180} level="M" />
|
||||
<p className="text-xs font-medium text-neutral-700">
|
||||
{mobileDevice
|
||||
? t("walletTpWaiting")
|
||||
: t("walletQrUseAnotherDevice")}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useConnect, useDisconnect } from "wagmi";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useAccount, useConnect, useDisconnect } from "wagmi";
|
||||
import { bsc } from "wagmi/chains";
|
||||
import { hasWalletConnectProjectId } from "./RainbowWalletProvider";
|
||||
import {
|
||||
@@ -24,18 +24,39 @@ function currentUrl(): string {
|
||||
return window.location.href;
|
||||
}
|
||||
|
||||
function isWalletConnectUri(uri: string): boolean {
|
||||
return uri.startsWith("wc:");
|
||||
}
|
||||
|
||||
function metaMaskWalletConnectLink(uri: string): string {
|
||||
if (!isWalletConnectUri(uri)) return uri;
|
||||
return `https://metamask.app.link/wc?uri=${encodeURIComponent(uri)}`;
|
||||
}
|
||||
|
||||
function walletConnectQrValue(
|
||||
kind: WalletKind | undefined,
|
||||
uri: string,
|
||||
): string {
|
||||
if (kind === "metaMask") return metaMaskWalletConnectLink(uri);
|
||||
return uri;
|
||||
}
|
||||
|
||||
function walletConnectDeeplink(
|
||||
kind: WalletKind | undefined,
|
||||
uri: string,
|
||||
): string | null {
|
||||
if (kind === "tokenPocket") {
|
||||
return `tpoutside://wc?uri=${encodeURIComponent(uri)}`;
|
||||
return isWalletConnectUri(uri)
|
||||
? `tpoutside://wc?uri=${encodeURIComponent(uri)}`
|
||||
: uri;
|
||||
}
|
||||
if (kind === "metaMask") {
|
||||
return `https://metamask.app.link/wc?uri=${encodeURIComponent(uri)}`;
|
||||
return metaMaskWalletConnectLink(uri);
|
||||
}
|
||||
if (kind === "imToken") {
|
||||
return `imtokenv2://wc?uri=${encodeURIComponent(uri)}`;
|
||||
return isWalletConnectUri(uri)
|
||||
? `imtokenv2://wc?uri=${encodeURIComponent(uri)}`
|
||||
: uri;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -61,6 +82,26 @@ function openWalletDeeplink(
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function connectorMatchesWallet(
|
||||
connector: { id: string; name?: string; type?: string },
|
||||
kind: WalletKind | undefined,
|
||||
): boolean {
|
||||
if (!kind) return false;
|
||||
const id = connector.id.toLowerCase();
|
||||
const name = connector.name?.toLowerCase() ?? "";
|
||||
const type = connector.type?.toLowerCase() ?? "";
|
||||
if (kind === "metaMask") {
|
||||
return id === "metamask" || type === "metamask" || name === "metamask";
|
||||
}
|
||||
if (kind === "tokenPocket") {
|
||||
return id === "tokenpocket" || name === "tokenpocket";
|
||||
}
|
||||
if (kind === "imToken") {
|
||||
return id === "imtoken" || name === "imtoken";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* MetaMask / imToken QR fallback via RainbowKit + WalletConnect.
|
||||
*
|
||||
@@ -74,7 +115,8 @@ function openWalletDeeplink(
|
||||
*/
|
||||
export function useWalletConnectLogin() {
|
||||
const available = hasWalletConnectProjectId();
|
||||
const { completeLogin } = useWallet();
|
||||
const { address: localAddress, completeLogin } = useWallet();
|
||||
const { address: wagmiAddress, isConnected: wagmiConnected } = useAccount();
|
||||
const { connectAsync, connectors } = useConnect();
|
||||
const { disconnect, disconnectAsync } = useDisconnect();
|
||||
const [state, setState] = useState<WalletConnectLoginState>("idle");
|
||||
@@ -82,10 +124,12 @@ export function useWalletConnectLogin() {
|
||||
const [qrUri, setQrUri] = useState("");
|
||||
const [connectedAddress, setConnectedAddress] = useState("");
|
||||
const pendingRef = useRef(false);
|
||||
const completedAddressRef = useRef<string | null>(null);
|
||||
const cleanupMessageRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
pendingRef.current = false;
|
||||
completedAddressRef.current = null;
|
||||
cleanupMessageRef.current?.();
|
||||
cleanupMessageRef.current = null;
|
||||
setState("idle");
|
||||
@@ -94,6 +138,33 @@ export function useWalletConnectLogin() {
|
||||
setConnectedAddress("");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wagmiConnected || !wagmiAddress) return;
|
||||
const alreadyCompleted =
|
||||
completedAddressRef.current?.toLowerCase() === wagmiAddress.toLowerCase();
|
||||
if (alreadyCompleted) return;
|
||||
|
||||
completedAddressRef.current = wagmiAddress;
|
||||
pendingRef.current = false;
|
||||
setConnectedAddress(wagmiAddress);
|
||||
setQrUri("");
|
||||
setState("idle");
|
||||
|
||||
if (localAddress?.toLowerCase() !== wagmiAddress.toLowerCase()) {
|
||||
console.info("[wallet-login] wagmi account connected", {
|
||||
address: wagmiAddress,
|
||||
chain: "BNB Chain",
|
||||
chainId: bsc.id,
|
||||
});
|
||||
completeLogin(localWalletToken(wagmiAddress), wagmiAddress);
|
||||
console.info("[wallet-login] local wallet session completed", {
|
||||
address: wagmiAddress,
|
||||
});
|
||||
}
|
||||
|
||||
void disconnect();
|
||||
}, [completeLogin, disconnect, localAddress, wagmiAddress, wagmiConnected]);
|
||||
|
||||
const start = useCallback(
|
||||
async (
|
||||
preferredWallet?: WalletKind,
|
||||
@@ -103,6 +174,7 @@ export function useWalletConnectLogin() {
|
||||
setError("");
|
||||
setQrUri("");
|
||||
setConnectedAddress("");
|
||||
completedAddressRef.current = null;
|
||||
pendingRef.current = true;
|
||||
setState("connecting");
|
||||
|
||||
@@ -126,9 +198,13 @@ export function useWalletConnectLogin() {
|
||||
}
|
||||
}
|
||||
|
||||
// This modal is QR/WalletConnect-only. RainbowKit also exposes wallet-
|
||||
// specific injected connectors (for example `tokenPocket`) when an
|
||||
// Prefer the connector RainbowKit created for the selected wallet. This
|
||||
// is especially important for MetaMask Mobile: RainbowKit/Wagmi use the
|
||||
// MetaMask SDK connector there instead of the generic WalletConnect one.
|
||||
const connector =
|
||||
connectors.find((item) =>
|
||||
connectorMatchesWallet(item, preferredWallet),
|
||||
) ??
|
||||
connectors.find((item) => item.type === "walletConnect") ??
|
||||
connectors.find((item) => item.id === "walletConnect");
|
||||
|
||||
@@ -158,7 +234,9 @@ export function useWalletConnectLogin() {
|
||||
preferredWallet,
|
||||
connectorId: connector.id,
|
||||
});
|
||||
if (mode === "qr") setQrUri(message.data);
|
||||
if (mode === "qr") {
|
||||
setQrUri(walletConnectQrValue(preferredWallet, message.data));
|
||||
}
|
||||
const deeplink = walletConnectDeeplink(preferredWallet, message.data);
|
||||
if (mode === "deeplink" && deeplink && isMobileDevice()) {
|
||||
openWalletDeeplink(preferredWallet, deeplink);
|
||||
@@ -178,6 +256,7 @@ export function useWalletConnectLogin() {
|
||||
if (!connectedAddress)
|
||||
throw new Error("Wallet connected without an account");
|
||||
pendingRef.current = false;
|
||||
completedAddressRef.current = connectedAddress;
|
||||
setConnectedAddress(connectedAddress);
|
||||
console.info("[wallet-login] walletconnect connected", {
|
||||
address: connectedAddress,
|
||||
|
||||
Reference in New Issue
Block a user