Files
Arkie-Library-Frontend/src/wallet/useWalletConnectLogin.ts

157 lines
5.2 KiB
TypeScript
Raw Normal View History

import { useCallback, useEffect, useRef, useState } from "react";
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.
*
* 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 { connectAsync, connectors } = useConnect();
const { disconnect } = useDisconnect();
const [state, setState] = useState<WalletConnectLoginState>("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(
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;
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]);
return { available, state, error, qrUri, start, reset };
}