2026-06-02 22:18:35 +08:00
|
|
|
import { useCallback, useRef, useState } from "react";
|
|
|
|
|
import { useConnect, useDisconnect } from "wagmi";
|
2026-06-02 21:05:01 +08:00
|
|
|
import { bsc } from "wagmi/chains";
|
2026-06-02 03:43:13 +08:00
|
|
|
import { hasWalletConnectProjectId } from "./RainbowWalletProvider";
|
2026-06-02 21:05:01 +08:00
|
|
|
import type { WalletKind } from "./injected";
|
2026-06-02 21:52:15 +08:00
|
|
|
import { localWalletToken, useWallet } from "./WalletProvider";
|
2026-06-02 03:43:13 +08:00
|
|
|
|
|
|
|
|
export type WalletConnectLoginState = "idle" | "connecting" | "signing";
|
2026-06-02 21:52:15 +08:00
|
|
|
export type WalletConnectLoginMode = "deeplink" | "qr";
|
2026-06-02 03:43:13 +08:00
|
|
|
|
2026-06-02 21:05:01 +08:00
|
|
|
function isMobileDevice(): boolean {
|
|
|
|
|
if (typeof navigator === "undefined") return false;
|
|
|
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(
|
|
|
|
|
navigator.userAgent || "",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 21:52:15 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 03:43:13 +08:00
|
|
|
/**
|
|
|
|
|
* MetaMask / imToken QR fallback via RainbowKit + WalletConnect.
|
|
|
|
|
*
|
2026-06-02 21:52:15 +08:00
|
|
|
* 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.
|
2026-06-02 03:43:13 +08:00
|
|
|
*
|
|
|
|
|
* 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();
|
2026-06-02 21:05:01 +08:00
|
|
|
const { connectAsync, connectors } = useConnect();
|
2026-06-02 22:18:35 +08:00
|
|
|
const { disconnect, disconnectAsync } = useDisconnect();
|
2026-06-02 03:43:13 +08:00
|
|
|
const [state, setState] = useState<WalletConnectLoginState>("idle");
|
|
|
|
|
const [error, setError] = useState("");
|
2026-06-02 21:05:01 +08:00
|
|
|
const [qrUri, setQrUri] = useState("");
|
2026-06-02 22:18:35 +08:00
|
|
|
const [connectedAddress, setConnectedAddress] = useState("");
|
2026-06-02 03:43:13 +08:00
|
|
|
const pendingRef = useRef(false);
|
2026-06-02 21:05:01 +08:00
|
|
|
const cleanupMessageRef = useRef<(() => void) | null>(null);
|
2026-06-02 03:43:13 +08:00
|
|
|
|
|
|
|
|
const reset = useCallback(() => {
|
|
|
|
|
pendingRef.current = false;
|
2026-06-02 21:05:01 +08:00
|
|
|
cleanupMessageRef.current?.();
|
|
|
|
|
cleanupMessageRef.current = null;
|
2026-06-02 03:43:13 +08:00
|
|
|
setState("idle");
|
|
|
|
|
setError("");
|
2026-06-02 21:05:01 +08:00
|
|
|
setQrUri("");
|
2026-06-02 22:18:35 +08:00
|
|
|
setConnectedAddress("");
|
2026-06-02 03:43:13 +08:00
|
|
|
}, []);
|
|
|
|
|
|
2026-06-02 21:05:01 +08:00
|
|
|
const start = useCallback(
|
2026-06-02 21:52:15 +08:00
|
|
|
async (
|
|
|
|
|
preferredWallet?: WalletKind,
|
|
|
|
|
mode: WalletConnectLoginMode = "qr",
|
|
|
|
|
) => {
|
2026-06-02 21:05:01 +08:00
|
|
|
if (!available) return;
|
|
|
|
|
setError("");
|
|
|
|
|
setQrUri("");
|
2026-06-02 22:18:35 +08:00
|
|
|
setConnectedAddress("");
|
2026-06-02 21:05:01 +08:00
|
|
|
pendingRef.current = true;
|
|
|
|
|
setState("connecting");
|
|
|
|
|
|
2026-06-02 21:52:15 +08:00
|
|
|
// 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.
|
2026-06-02 21:05:01 +08:00
|
|
|
const connector =
|
2026-06-02 21:52:15 +08:00
|
|
|
connectors.find((item) => item.type === "walletConnect") ??
|
|
|
|
|
connectors.find((item) => item.id === "walletConnect");
|
2026-06-02 21:05:01 +08:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
});
|
2026-06-02 21:52:15 +08:00
|
|
|
if (mode === "qr") setQrUri(message.data);
|
|
|
|
|
const deeplink = walletConnectDeeplink(preferredWallet, message.data);
|
|
|
|
|
if (mode === "deeplink" && deeplink && isMobileDevice()) {
|
|
|
|
|
window.location.href = deeplink;
|
2026-06-02 21:05:01 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cleanupMessageRef.current?.();
|
|
|
|
|
connector.emitter.on("message", onMessage);
|
|
|
|
|
cleanupMessageRef.current = () =>
|
|
|
|
|
connector.emitter.off("message", onMessage);
|
|
|
|
|
|
|
|
|
|
try {
|
2026-06-02 22:18:35 +08:00
|
|
|
await disconnectAsync().catch(() => undefined);
|
2026-06-02 21:05:01 +08:00
|
|
|
await connector.disconnect().catch(() => undefined);
|
2026-06-02 22:18:35 +08:00
|
|
|
const result = await connectAsync({ chainId: bsc.id, connector });
|
|
|
|
|
const connectedAddress = result.accounts[0];
|
|
|
|
|
if (!connectedAddress)
|
|
|
|
|
throw new Error("Wallet connected without an account");
|
|
|
|
|
pendingRef.current = false;
|
|
|
|
|
setConnectedAddress(connectedAddress);
|
|
|
|
|
console.info("[wallet-login] walletconnect connected", {
|
|
|
|
|
address: connectedAddress,
|
|
|
|
|
chain: "BNB Chain",
|
|
|
|
|
chainId: bsc.id,
|
|
|
|
|
});
|
|
|
|
|
window.alert(`扫码成功,已拿到钱包地址:\n${connectedAddress}`);
|
|
|
|
|
completeLogin(localWalletToken(connectedAddress), connectedAddress);
|
|
|
|
|
console.info("[wallet-login] local wallet session completed", {
|
|
|
|
|
address: connectedAddress,
|
|
|
|
|
});
|
|
|
|
|
setQrUri("");
|
|
|
|
|
setState("idle");
|
|
|
|
|
disconnect();
|
2026-06-02 21:05:01 +08:00
|
|
|
} catch (err) {
|
|
|
|
|
pendingRef.current = false;
|
|
|
|
|
setState("idle");
|
|
|
|
|
setError(
|
|
|
|
|
err instanceof Error ? err.message : "WalletConnect login failed",
|
|
|
|
|
);
|
|
|
|
|
cleanupMessageRef.current?.();
|
|
|
|
|
cleanupMessageRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-06-02 22:18:35 +08:00
|
|
|
[
|
|
|
|
|
available,
|
|
|
|
|
completeLogin,
|
|
|
|
|
connectAsync,
|
|
|
|
|
connectors,
|
|
|
|
|
disconnect,
|
|
|
|
|
disconnectAsync,
|
|
|
|
|
],
|
2026-06-02 21:05:01 +08:00
|
|
|
);
|
2026-06-02 03:43:13 +08:00
|
|
|
|
2026-06-02 22:18:35 +08:00
|
|
|
return {
|
|
|
|
|
available,
|
|
|
|
|
state,
|
|
|
|
|
error,
|
|
|
|
|
qrUri,
|
|
|
|
|
address: connectedAddress,
|
|
|
|
|
isConnected: Boolean(connectedAddress),
|
|
|
|
|
start,
|
|
|
|
|
reset,
|
|
|
|
|
};
|
2026-06-02 03:43:13 +08:00
|
|
|
}
|