85 lines
3.1 KiB
TypeScript
85 lines
3.1 KiB
TypeScript
|
|
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 };
|
||
|
|
}
|