From f0209eb894b970de11bc1d97a5cca0d50ab402c6 Mon Sep 17 00:00:00 2001 From: TerryM Date: Tue, 2 Jun 2026 21:05:01 +0800 Subject: [PATCH] fix(wallet): improve mobile login and logout flows --- src/locales/en.ts | 2 + src/locales/id.ts | 2 + src/locales/ja.ts | 2 + src/locales/ko.ts | 2 + src/locales/ms.ts | 2 + src/locales/vi.ts | 2 + src/locales/zh-CN.ts | 2 + src/wallet/WalletButton.tsx | 18 ++ src/wallet/WalletLoginModal.tsx | 475 ++++++++++++---------------- src/wallet/useWalletConnectLogin.ts | 98 +++++- 10 files changed, 317 insertions(+), 288 deletions(-) diff --git a/src/locales/en.ts b/src/locales/en.ts index fb94a67..a3205a7 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -186,6 +186,8 @@ export const enDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", walletImToken: "imToken", + walletBack: "Back", + walletChooseMethod: "Choose how to log in", walletTokenPocketLogin: "TokenPocket login", walletTpMobileDesc: "Open TokenPocket to sign, then come back here to finish. You stay in this browser instead of the wallet's in-app browser.", diff --git a/src/locales/id.ts b/src/locales/id.ts index 9633c86..dbbb5b4 100644 --- a/src/locales/id.ts +++ b/src/locales/id.ts @@ -186,6 +186,8 @@ export const idDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", walletImToken: "imToken", + walletBack: "Kembali", + walletChooseMethod: "Pilih cara masuk", walletTokenPocketLogin: "Masuk TokenPocket", walletTpMobileDesc: "Buka TokenPocket untuk menandatangani, lalu kembali ke sini untuk menyelesaikan. Anda tetap di browser ini, bukan browser dalam aplikasi dompet.", diff --git a/src/locales/ja.ts b/src/locales/ja.ts index db8d2b8..6ef629d 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -206,6 +206,8 @@ export const jaDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", walletImToken: "imToken", + walletBack: "戻る", + walletChooseMethod: "ログイン方法を選択", walletTokenPocketLogin: "TokenPocket ログイン", walletTpMobileDesc: "TokenPocket で署名するとこのページに戻ってログインが完了します。ウォレット内ブラウザには移動せず、現在のブラウザのままです。", diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 0f20c9a..53c897a 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -183,6 +183,8 @@ export const koDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", walletImToken: "imToken", + walletBack: "뒤로", + walletChooseMethod: "로그인 방법 선택", walletTokenPocketLogin: "TokenPocket 로그인", walletTpMobileDesc: "TokenPocket에서 서명하면 이 페이지로 돌아와 로그인이 완료됩니다. 지갑 내장 브라우저로 이동하지 않고 현재 브라우저에 머무릅니다.", diff --git a/src/locales/ms.ts b/src/locales/ms.ts index 68bac45..c7c0bbd 100644 --- a/src/locales/ms.ts +++ b/src/locales/ms.ts @@ -185,6 +185,8 @@ export const msDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", walletImToken: "imToken", + walletBack: "Kembali", + walletChooseMethod: "Pilih cara log masuk", walletTokenPocketLogin: "Log masuk TokenPocket", walletTpMobileDesc: "Buka TokenPocket untuk menandatangani, kemudian kembali ke sini untuk selesai. Anda kekal dalam pelayar ini, bukan pelayar dalam aplikasi dompet.", diff --git a/src/locales/vi.ts b/src/locales/vi.ts index 8ffc601..2ed9b56 100644 --- a/src/locales/vi.ts +++ b/src/locales/vi.ts @@ -183,6 +183,8 @@ export const viDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", walletImToken: "imToken", + walletBack: "Quay lại", + walletChooseMethod: "Chọn cách đăng nhập", walletTokenPocketLogin: "Đăng nhập TokenPocket", walletTpMobileDesc: "Mở TokenPocket để ký, rồi quay lại đây để hoàn tất. Bạn vẫn ở trong trình duyệt này thay vì trình duyệt trong ví.", diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index a5e2e34..561adcf 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -176,6 +176,8 @@ export const zhDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", walletImToken: "imToken", + walletBack: "返回", + walletChooseMethod: "选择登录方式", walletTokenPocketLogin: "TokenPocket 登录", walletTpMobileDesc: "在 TokenPocket 中签名后会自动返回本页面完成登录,留在当前浏览器,不会跳进钱包内置浏览器。", diff --git a/src/wallet/WalletButton.tsx b/src/wallet/WalletButton.tsx index 33deaf7..edb6258 100644 --- a/src/wallet/WalletButton.tsx +++ b/src/wallet/WalletButton.tsx @@ -37,6 +37,24 @@ export function WalletButton({ }, [open]); if (wallet.status === "loggedIn" && wallet.address) { + if (compact) { + return ( +
+
+ + {shortenAddress(wallet.address)} +
+ +
+ ); + } + return (
+ ) : null} +
+

+ {t("walletLoginTitle")} +

+

+ {step === "wallet" + ? t("walletLoginDesc") + : t("walletChooseMethod")} +

+
-
- {/* Browser wallet: sign directly with the injected provider — the - reliable path for a BNB-chain extension (desktop) or a wallet's - in-app browser. No WalletConnect relay involved. */} - {!mobileDevice || hasInjected ? ( - - ) : null} - - {/* TokenPocket login — universal path that returns to this browser. */} -
-

- {t("walletTokenPocketLogin")} -

-

- {mobileDevice - ? t("walletTpMobileDesc") - : t("walletTokenPocketQrDesc")} -

- {!tpRequest ? ( + {/* Step 1 — choose a wallet. */} + {step === "wallet" ? ( +
+ {wallets.map((kind) => ( - ) : mobileDevice ? ( -
-

- - {t("walletTpWaiting")} -

+ ))} +
+ ) : selected ? ( +
+ {/* Selected wallet header. */} +
+ + + {walletName(selected)} + +
+ + {!mobileDevice && !injectedAvailable(selected) ? ( +

+ {t("walletInstallSelected").replace( + "{wallet}", + walletName(selected), + )} +

+ ) : null} + + {/* Method: browser wallet (injected). */} + {(() => { + const ok = injectedAvailable(selected); + return ( -
- ) : ( -
- -

- {t("walletQrUseAnotherDevice")} -

-
- )} -
- - {/* Other methods: open a wallet app (mobile) and WalletConnect QR. */} -
- - - {showOther ? ( -
- {mobileDevice && !hasInjected ? ( -
-

- {t("walletOpenWalletApp")} -

- {appWallets.map((option) => ( - - ))} - {openingWallet ? ( -
-

{withWallet("walletOpening", openingWallet)}

-

{t("walletAppNotInstalled")}

-
- - {withWallet("walletDownloadApp", openingWallet)} - - -
-
+ + {state === "signing" ? ( + ) : null} -
- ) : null} + {mobileDevice ? t("walletUseCurrent") : t("walletInjected")} + + + {ok + ? t("walletInjectedDesc") + : mobileDevice + ? t("walletOpenWalletAppDesc") + : t("walletInstallSelected").replace( + "{wallet}", + walletName(selected), + )} + + + ); + })()} - {/* MetaMask / imToken QR via WalletConnect — needs a real id. */} -
-

- {t("walletRainbowFallback")} -

-

- {t("walletRainbowFallbackDesc")} -

- {wc.available ? ( - <> - -

- {t("walletNetworkWarning")} -

- - ) : ( -

- {t("walletRainbowUnavailable")} + {/* Method: scan to log in. */} + {(() => { + const ok = qrAvailable(); + const isTp = selected === "tokenPocket"; + const qrBusy = wc.state !== "idle"; + const qrLabel = + wc.state === "connecting" + ? t("walletConnecting") + : wc.state === "signing" + ? t("walletSigning") + : isTp && mobileDevice + ? t("walletTpLoginBtn") + : t("walletQrLogin"); + return ( +

+ + + {ok ? ( +

+ {t("walletNetworkWarning")}

- )} + ) : null} + + {wc.qrUri ? ( +
+ +

+ {mobileDevice + ? t("walletTpWaiting") + : t("walletQrUseAnotherDevice")} +

+
+ ) : null}
-
- ) : null} + ); + })()}
-
+ ) : null} {error || wc.error ? (

diff --git a/src/wallet/useWalletConnectLogin.ts b/src/wallet/useWalletConnectLogin.ts index 4240053..17ee12e 100644 --- a/src/wallet/useWalletConnectLogin.ts +++ b/src/wallet/useWalletConnectLogin.ts @@ -1,12 +1,20 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { useConnectModal } from "@rainbow-me/rainbowkit"; -import { useAccount, useDisconnect, useSignMessage } from "wagmi"; +import { useAccount, useConnect, useDisconnect, useSignMessage } 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"; export type WalletConnectLoginState = "idle" | "connecting" | "signing"; +function isMobileDevice(): boolean { + if (typeof navigator === "undefined") return false; + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test( + navigator.userAgent || "", + ); +} + /** * MetaMask / imToken QR fallback via RainbowKit + WalletConnect. * @@ -24,27 +32,90 @@ export function useWalletConnectLogin() { const { completeLogin } = useWallet(); const { address, isConnected } = useAccount(); const { signMessageAsync } = useSignMessage(); + const { connectAsync, connectors } = useConnect(); const { disconnect } = useDisconnect(); - const { openConnectModal } = useConnectModal(); const [state, setState] = useState("idle"); const [error, setError] = useState(""); + const [qrUri, setQrUri] = useState(""); const pendingRef = useRef(false); + const cleanupMessageRef = useRef<(() => void) | null>(null); const reset = useCallback(() => { pendingRef.current = false; + cleanupMessageRef.current?.(); + cleanupMessageRef.current = null; setState("idle"); setError(""); + setQrUri(""); }, []); - const start = useCallback(() => { - if (!available) return; - setError(""); - pendingRef.current = true; - setState("connecting"); - // When already connected, openConnectModal is undefined; the effect below - // picks up the existing account and proceeds straight to signing. - openConnectModal?.(); - }, [available, openConnectModal]); + const start = useCallback( + async (preferredWallet?: WalletKind) => { + if (!available) return; + setError(""); + setQrUri(""); + pendingRef.current = true; + setState("connecting"); + + const connector = + connectors.find((item) => item.id === preferredWallet) ?? + connectors.find((item) => item.id === "walletConnect") ?? + connectors.find((item) => item.type === "walletConnect"); + + if (!connector) { + pendingRef.current = false; + setQrUri(""); + setState("idle"); + setError("WalletConnect is not available"); + return; + } + + console.info("[wallet-login] walletconnect connector", { + preferredWallet, + connectorId: connector.id, + connectorName: connector.name, + connectorType: connector.type, + }); + + const onMessage = (message: { type: string; data?: unknown }) => { + if ( + message.type !== "display_uri" || + typeof message.data !== "string" + ) { + return; + } + console.info("[wallet-login] walletconnect display_uri", { + preferredWallet, + connectorId: connector.id, + }); + setQrUri(message.data); + if (preferredWallet === "tokenPocket" && isMobileDevice()) { + window.location.href = `tpoutside://wc?uri=${encodeURIComponent( + message.data, + )}`; + } + }; + + cleanupMessageRef.current?.(); + connector.emitter.on("message", onMessage); + cleanupMessageRef.current = () => + connector.emitter.off("message", onMessage); + + try { + await connector.disconnect().catch(() => undefined); + await connectAsync({ chainId: bsc.id, connector }); + } catch (err) { + pendingRef.current = false; + setState("idle"); + setError( + err instanceof Error ? err.message : "WalletConnect login failed", + ); + cleanupMessageRef.current?.(); + cleanupMessageRef.current = null; + } + }, + [available, connectAsync, connectors], + ); useEffect(() => { if (!pendingRef.current || !isConnected || !address) return; @@ -68,6 +139,7 @@ export function useWalletConnectLogin() { setError( err instanceof Error ? err.message : "WalletConnect login failed", ); + setQrUri(""); setState("idle"); } } finally { @@ -80,5 +152,5 @@ export function useWalletConnectLogin() { }; }, [address, completeLogin, disconnect, isConnected, signMessageAsync]); - return { available, state, error, start, reset }; + return { available, state, error, qrUri, start, reset }; }