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 ? (
+
+ startWalletLogin(kind, "deeplink")}
+ disabled={!wc.available || busy}
+ className="rounded-full bg-ark-gold px-3 py-2 text-sm font-bold text-black transition hover:bg-ark-gold2 disabled:cursor-wait disabled:opacity-70"
+ >
+ {t("walletOpenWalletApp")}
+
+ startWalletLogin(kind, "qr")}
+ disabled={!wc.available || busy}
+ className="rounded-full border border-ark-gold/50 px-3 py-2 text-sm font-semibold text-ark-gold transition hover:bg-ark-gold/10 disabled:cursor-wait disabled:opacity-70"
+ >
+ {t("walletQrLogin")}
+
+
) : 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 };
}