fix(wallet): support no-signature wallet connect
This commit is contained in:
@@ -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<WalletConnectLoginState>("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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user