feat: redesign wallet login and favorites, fix desktop/mobile bugs

- Remove forced BNB chain switch on injected login (signature is chain-agnostic)
- Refine isMobileDevice so touch Macs stay on desktop flow
- Wire RainbowKit/WalletConnect as a real MetaMask/imToken QR fallback,
  gated on a valid VITE_WALLETCONNECT_PROJECT_ID
- Rebuild login modal: single desktop primary action, collapsible other
  methods, mobile open-app fallback feedback, brand icons
- Add My Favorites entry points (header, mobile menu, wallet dropdown)
- Favorites page: error retry, mobile filter drawer
- Auto sign-out and re-login prompt on favorites 401
- Full native translations for all wallet strings across 7 locales

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
TerryM
2026-06-02 03:43:13 +08:00
parent f935f122f9
commit 7abe4a868c
17 changed files with 715 additions and 155 deletions

View File

@@ -0,0 +1,84 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useConnectModal } from "@rainbow-me/rainbowkit";
import { useAccount, useDisconnect, useSignMessage } from "wagmi";
import { requestWalletNonce, verifyWalletSignature } from "./api";
import { hasWalletConnectProjectId } from "./RainbowWalletProvider";
import { useWallet } from "./WalletProvider";
export type WalletConnectLoginState = "idle" | "connecting" | "signing";
/**
* 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.
*
* 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
* disable the entry instead of triggering a connect with a fake project id.
*/
export function useWalletConnectLogin() {
const available = hasWalletConnectProjectId();
const { completeLogin } = useWallet();
const { address, isConnected } = useAccount();
const { signMessageAsync } = useSignMessage();
const { disconnect } = useDisconnect();
const { openConnectModal } = useConnectModal();
const [state, setState] = useState<WalletConnectLoginState>("idle");
const [error, setError] = useState("");
const pendingRef = useRef(false);
const reset = useCallback(() => {
pendingRef.current = false;
setState("idle");
setError("");
}, []);
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]);
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",
);
setState("idle");
}
} finally {
// We only needed a one-off signature, not a persistent wagmi session.
disconnect();
}
})();
return () => {
cancelled = true;
};
}, [address, completeLogin, disconnect, isConnected, signMessageAsync]);
return { available, state, error, start, reset };
}