From 4d38c4513d418b5f2c3c202508a7b540f4294f98 Mon Sep 17 00:00:00 2001 From: TerryM Date: Tue, 2 Jun 2026 21:52:15 +0800 Subject: [PATCH] fix(wallet): support no-signature wallet connect --- src/wallet/WalletLoginModal.tsx | 82 ++++++++++++++++++-------- src/wallet/WalletProvider.tsx | 19 ++++++ src/wallet/useWalletConnectLogin.ts | 90 ++++++++++++++--------------- 3 files changed, 119 insertions(+), 72 deletions(-) diff --git a/src/wallet/WalletLoginModal.tsx b/src/wallet/WalletLoginModal.tsx index 562e711..38723dc 100644 --- a/src/wallet/WalletLoginModal.tsx +++ b/src/wallet/WalletLoginModal.tsx @@ -51,9 +51,14 @@ export function WalletLoginModal() { wc.reset(); }; - const startWalletLogin = (kind: WalletKind) => { + const selectWallet = (kind: WalletKind) => { setSelected(kind); - void wc.start(kind); + wc.reset(); + }; + + const startWalletLogin = (kind: WalletKind, mode: "deeplink" | "qr") => { + setSelected(kind); + void wc.start(kind, mode); }; return ( @@ -92,34 +97,63 @@ export function WalletLoginModal() { const connecting = active && wc.state === "connecting"; const signing = active && wc.state === "signing"; return ( - + + {mobileDevice && active ? ( +
+ + +
) : null} - + ); })} diff --git a/src/wallet/WalletProvider.tsx b/src/wallet/WalletProvider.tsx index af43668..eac3f6d 100644 --- a/src/wallet/WalletProvider.tsx +++ b/src/wallet/WalletProvider.tsx @@ -15,6 +15,18 @@ import { clearWalletToken, readWalletToken, writeWalletToken } from "./token"; type WalletStatus = "loading" | "loggedOut" | "loggedIn"; +const localWalletTokenPrefix = "local-wallet:"; + +export function localWalletToken(wallet: string): string { + return `${localWalletTokenPrefix}${wallet}`; +} + +function walletFromLocalToken(token: string): string | null { + return token.startsWith(localWalletTokenPrefix) + ? token.slice(localWalletTokenPrefix.length) + : null; +} + type WalletContextValue = { address: string | null; token: string | null; @@ -52,6 +64,13 @@ export function WalletProvider({ children }: { children: ReactNode }) { return; } + const localWallet = walletFromLocalToken(token); + if (localWallet) { + setAddress(localWallet); + setStatus("loggedIn"); + return; + } + setStatus("loading"); fetchWalletMe(token) .then((me) => { diff --git a/src/wallet/useWalletConnectLogin.ts b/src/wallet/useWalletConnectLogin.ts index 17ee12e..f61d5ea 100644 --- a/src/wallet/useWalletConnectLogin.ts +++ b/src/wallet/useWalletConnectLogin.ts @@ -1,12 +1,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { useAccount, useConnect, useDisconnect, useSignMessage } from "wagmi"; +import { useAccount, useConnect, useDisconnect } from "wagmi"; import { bsc } from "wagmi/chains"; -import { requestWalletNonce, verifyWalletSignature } from "./api"; import { hasWalletConnectProjectId } from "./RainbowWalletProvider"; import type { WalletKind } from "./injected"; -import { useWallet } from "./WalletProvider"; +import { localWalletToken, useWallet } from "./WalletProvider"; export type WalletConnectLoginState = "idle" | "connecting" | "signing"; +export type WalletConnectLoginMode = "deeplink" | "qr"; function isMobileDevice(): boolean { if (typeof navigator === "undefined") return false; @@ -15,13 +15,28 @@ function isMobileDevice(): boolean { ); } +function walletConnectDeeplink( + kind: WalletKind | undefined, + uri: string, +): string | null { + if (kind === "tokenPocket") { + return `tpoutside://wc?uri=${encodeURIComponent(uri)}`; + } + if (kind === "metaMask") { + return `https://metamask.app.link/wc?uri=${encodeURIComponent(uri)}`; + } + if (kind === "imToken") { + return `imtokenv2://wc?uri=${encodeURIComponent(uri)}`; + } + return null; +} + /** * MetaMask / imToken QR fallback via RainbowKit + WalletConnect. * - * Flow: open the RainbowKit connect modal (WalletConnect QR) -> once an account - * is connected, request a nonce, sign it with `personal_sign` through wagmi, - * verify against the backend and complete our own JWT login. The wagmi/WC - * session is only needed for the signature, so we disconnect right after. + * Flow: connect through RainbowKit/Wagmi on BNB Chain -> once an account is + * connected, complete a local frontend wallet session. No message signature, + * backend nonce, or verify call is required. * * Entirely gated behind a real `VITE_WALLETCONNECT_PROJECT_ID`: when it is * missing `available` is false and `start` is a no-op, so callers can hide or @@ -31,7 +46,6 @@ export function useWalletConnectLogin() { const available = hasWalletConnectProjectId(); const { completeLogin } = useWallet(); const { address, isConnected } = useAccount(); - const { signMessageAsync } = useSignMessage(); const { connectAsync, connectors } = useConnect(); const { disconnect } = useDisconnect(); const [state, setState] = useState("idle"); @@ -50,17 +64,24 @@ export function useWalletConnectLogin() { }, []); const start = useCallback( - async (preferredWallet?: WalletKind) => { + async ( + preferredWallet?: WalletKind, + mode: WalletConnectLoginMode = "qr", + ) => { if (!available) return; setError(""); setQrUri(""); pendingRef.current = true; setState("connecting"); + // This modal is QR/WalletConnect-only. RainbowKit also exposes wallet- + // specific injected connectors (for example `tokenPocket`) when an + // extension is installed; using those here makes the click try the local + // browser extension and can fail with "wallet must has at least one + // account" before a QR is shown. const connector = - connectors.find((item) => item.id === preferredWallet) ?? - connectors.find((item) => item.id === "walletConnect") ?? - connectors.find((item) => item.type === "walletConnect"); + connectors.find((item) => item.type === "walletConnect") ?? + connectors.find((item) => item.id === "walletConnect"); if (!connector) { pendingRef.current = false; @@ -88,11 +109,10 @@ export function useWalletConnectLogin() { preferredWallet, connectorId: connector.id, }); - setQrUri(message.data); - if (preferredWallet === "tokenPocket" && isMobileDevice()) { - window.location.href = `tpoutside://wc?uri=${encodeURIComponent( - message.data, - )}`; + if (mode === "qr") setQrUri(message.data); + const deeplink = walletConnectDeeplink(preferredWallet, message.data); + if (mode === "deeplink" && deeplink && isMobileDevice()) { + window.location.href = deeplink; } }; @@ -120,37 +140,11 @@ export function useWalletConnectLogin() { useEffect(() => { if (!pendingRef.current || !isConnected || !address) return; pendingRef.current = false; - setState("signing"); - let cancelled = false; - void (async () => { - try { - const nonce = await requestWalletNonce(address); - const signature = await signMessageAsync({ message: nonce.message }); - const verified = await verifyWalletSignature({ - address, - message: nonce.message, - signature, - }); - if (cancelled) return; - completeLogin(verified.token, verified.wallet); - setState("idle"); - } catch (err) { - if (!cancelled) { - setError( - err instanceof Error ? err.message : "WalletConnect login failed", - ); - setQrUri(""); - setState("idle"); - } - } finally { - // We only needed a one-off signature, not a persistent wagmi session. - disconnect(); - } - })(); - return () => { - cancelled = true; - }; - }, [address, completeLogin, disconnect, isConnected, signMessageAsync]); + completeLogin(localWalletToken(address), address); + setQrUri(""); + setState("idle"); + disconnect(); + }, [address, completeLogin, disconnect, isConnected]); return { available, state, error, qrUri, start, reset }; }