2026-06-03 00:12:50 +08:00
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
|
import { useAccount, 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 23:32:39 +08:00
|
|
|
import {
|
|
|
|
|
getInjectedWallet,
|
2026-06-04 07:23:05 +08:00
|
|
|
signInWithInjectedWallet,
|
2026-06-02 23:32:39 +08:00
|
|
|
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 23:32:39 +08:00
|
|
|
function currentUrl(): string {
|
|
|
|
|
if (typeof window === "undefined") return "https://ark-library.com";
|
|
|
|
|
return window.location.href;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 00:12:50 +08:00
|
|
|
function isWalletConnectUri(uri: string): boolean {
|
|
|
|
|
return uri.startsWith("wc:");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function metaMaskWalletConnectLink(uri: string): string {
|
|
|
|
|
if (!isWalletConnectUri(uri)) return uri;
|
|
|
|
|
return `https://metamask.app.link/wc?uri=${encodeURIComponent(uri)}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 21:52:15 +08:00
|
|
|
function walletConnectDeeplink(
|
|
|
|
|
kind: WalletKind | undefined,
|
|
|
|
|
uri: string,
|
|
|
|
|
): string | null {
|
|
|
|
|
if (kind === "tokenPocket") {
|
2026-06-03 00:12:50 +08:00
|
|
|
return isWalletConnectUri(uri)
|
|
|
|
|
? `tpoutside://wc?uri=${encodeURIComponent(uri)}`
|
|
|
|
|
: uri;
|
2026-06-02 21:52:15 +08:00
|
|
|
}
|
|
|
|
|
if (kind === "metaMask") {
|
2026-06-03 00:12:50 +08:00
|
|
|
return metaMaskWalletConnectLink(uri);
|
2026-06-02 21:52:15 +08:00
|
|
|
}
|
|
|
|
|
if (kind === "imToken") {
|
2026-06-03 00:12:50 +08:00
|
|
|
return isWalletConnectUri(uri)
|
|
|
|
|
? `imtokenv2://wc?uri=${encodeURIComponent(uri)}`
|
|
|
|
|
: uri;
|
2026-06-02 21:52:15 +08:00
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 23:32:39 +08:00
|
|
|
function inAppBrowserFallback(kind: WalletKind | undefined): string | null {
|
|
|
|
|
if (kind === "imToken") {
|
|
|
|
|
return `imtokenv2://navigate/DappView?url=${encodeURIComponent(currentUrl())}`;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openWalletDeeplink(
|
|
|
|
|
kind: WalletKind | undefined,
|
|
|
|
|
deeplink: string,
|
|
|
|
|
): void {
|
|
|
|
|
window.location.href = deeplink;
|
|
|
|
|
const fallback = inAppBrowserFallback(kind);
|
|
|
|
|
if (!fallback) return;
|
|
|
|
|
window.setTimeout(() => {
|
|
|
|
|
if (document.visibilityState === "visible") {
|
|
|
|
|
window.location.href = fallback;
|
|
|
|
|
}
|
|
|
|
|
}, 1500);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 00:12:50 +08:00
|
|
|
function connectorMatchesWallet(
|
|
|
|
|
connector: { id: string; name?: string; type?: string },
|
|
|
|
|
kind: WalletKind | undefined,
|
|
|
|
|
): boolean {
|
|
|
|
|
if (!kind) return false;
|
|
|
|
|
const id = connector.id.toLowerCase();
|
|
|
|
|
const name = connector.name?.toLowerCase() ?? "";
|
|
|
|
|
const type = connector.type?.toLowerCase() ?? "";
|
|
|
|
|
if (kind === "metaMask") {
|
|
|
|
|
return id === "metamask" || type === "metamask" || name === "metamask";
|
|
|
|
|
}
|
|
|
|
|
if (kind === "tokenPocket") {
|
|
|
|
|
return id === "tokenpocket" || name === "tokenpocket";
|
|
|
|
|
}
|
|
|
|
|
if (kind === "imToken") {
|
|
|
|
|
return id === "imtoken" || name === "imtoken";
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
2026-06-04 07:23:05 +08:00
|
|
|
* connected, complete a local frontend wallet session. WalletConnect fallback
|
|
|
|
|
* does not require message signature, backend nonce, or verify call.
|
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();
|
2026-06-03 00:12:50 +08:00
|
|
|
const { address: localAddress, completeLogin } = useWallet();
|
|
|
|
|
const { address: wagmiAddress, isConnected: wagmiConnected } = useAccount();
|
2026-06-02 21:05:01 +08:00
|
|
|
const { connectAsync, connectors } = useConnect();
|
2026-06-03 00:25:46 +08:00
|
|
|
const { 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-03 00:12:50 +08:00
|
|
|
const completedAddressRef = useRef<string | null>(null);
|
2026-06-02 21:05:01 +08:00
|
|
|
const cleanupMessageRef = useRef<(() => void) | null>(null);
|
2026-06-03 00:25:46 +08:00
|
|
|
const cleanupPollingRef = useRef<(() => void) | null>(null);
|
2026-06-02 03:43:13 +08:00
|
|
|
|
|
|
|
|
const reset = useCallback(() => {
|
|
|
|
|
pendingRef.current = false;
|
2026-06-03 00:12:50 +08:00
|
|
|
completedAddressRef.current = null;
|
2026-06-02 21:05:01 +08:00
|
|
|
cleanupMessageRef.current?.();
|
|
|
|
|
cleanupMessageRef.current = null;
|
2026-06-03 00:25:46 +08:00
|
|
|
cleanupPollingRef.current?.();
|
|
|
|
|
cleanupPollingRef.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-03 00:12:50 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!wagmiConnected || !wagmiAddress) return;
|
|
|
|
|
const alreadyCompleted =
|
|
|
|
|
completedAddressRef.current?.toLowerCase() === wagmiAddress.toLowerCase();
|
|
|
|
|
if (alreadyCompleted) return;
|
|
|
|
|
|
|
|
|
|
completedAddressRef.current = wagmiAddress;
|
|
|
|
|
pendingRef.current = false;
|
|
|
|
|
setConnectedAddress(wagmiAddress);
|
|
|
|
|
setQrUri("");
|
|
|
|
|
setState("idle");
|
|
|
|
|
|
|
|
|
|
if (localAddress?.toLowerCase() !== wagmiAddress.toLowerCase()) {
|
|
|
|
|
console.info("[wallet-login] wagmi account connected", {
|
|
|
|
|
address: wagmiAddress,
|
|
|
|
|
chain: "BNB Chain",
|
|
|
|
|
chainId: bsc.id,
|
|
|
|
|
});
|
|
|
|
|
completeLogin(localWalletToken(wagmiAddress), wagmiAddress);
|
|
|
|
|
console.info("[wallet-login] local wallet session completed", {
|
|
|
|
|
address: wagmiAddress,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-06-03 00:25:46 +08:00
|
|
|
}, [completeLogin, localAddress, wagmiAddress, wagmiConnected]);
|
2026-06-03 00:12:50 +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-03 00:12:50 +08:00
|
|
|
completedAddressRef.current = null;
|
2026-06-02 21:05:01 +08:00
|
|
|
pendingRef.current = true;
|
|
|
|
|
setState("connecting");
|
|
|
|
|
|
2026-06-03 00:25:46 +08:00
|
|
|
if (
|
|
|
|
|
mode === "deeplink" &&
|
|
|
|
|
preferredWallet &&
|
|
|
|
|
getInjectedWallet(preferredWallet)
|
|
|
|
|
) {
|
2026-06-02 23:32:39 +08:00
|
|
|
try {
|
2026-06-04 07:23:05 +08:00
|
|
|
setState("signing");
|
|
|
|
|
const result = await signInWithInjectedWallet(preferredWallet);
|
|
|
|
|
console.info("[wallet-login] injected verified", {
|
2026-06-02 23:32:39 +08:00
|
|
|
preferredWallet,
|
2026-06-04 07:23:05 +08:00
|
|
|
address: result.wallet,
|
2026-06-02 23:32:39 +08:00
|
|
|
chain: "BNB Chain",
|
|
|
|
|
chainId: bsc.id,
|
|
|
|
|
});
|
2026-06-04 07:23:05 +08:00
|
|
|
completeLogin(result.token, result.wallet);
|
2026-06-02 23:32:39 +08:00
|
|
|
setState("idle");
|
|
|
|
|
return;
|
|
|
|
|
} catch (err) {
|
2026-06-04 07:23:05 +08:00
|
|
|
pendingRef.current = false;
|
|
|
|
|
setState("idle");
|
|
|
|
|
setError(err instanceof Error ? err.message : "Wallet login failed");
|
|
|
|
|
console.info("[wallet-login] injected verification failed", {
|
2026-06-02 23:32:39 +08:00
|
|
|
preferredWallet,
|
|
|
|
|
message: err instanceof Error ? err.message : String(err),
|
|
|
|
|
});
|
2026-06-04 07:23:05 +08:00
|
|
|
return;
|
2026-06-02 23:32:39 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 00:25:46 +08:00
|
|
|
// QR mode must always use a WalletConnect-compatible connector so the
|
|
|
|
|
// desktop page can render a scannable `wc:` URI instead of opening a
|
|
|
|
|
// local browser extension. Deeplink mode can prefer wallet-specific
|
|
|
|
|
// connectors (notably MetaMask SDK on mobile).
|
|
|
|
|
const walletConnectConnector =
|
2026-06-02 21:52:15 +08:00
|
|
|
connectors.find((item) => item.type === "walletConnect") ??
|
|
|
|
|
connectors.find((item) => item.id === "walletConnect");
|
2026-06-03 00:25:46 +08:00
|
|
|
const walletSpecificConnector = connectors.find((item) =>
|
|
|
|
|
connectorMatchesWallet(item, preferredWallet),
|
|
|
|
|
);
|
|
|
|
|
const connector =
|
|
|
|
|
mode === "qr"
|
|
|
|
|
? walletConnectConnector
|
|
|
|
|
: (walletSpecificConnector ?? walletConnectConnector);
|
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-03 00:12:50 +08:00
|
|
|
if (mode === "qr") {
|
2026-06-03 00:25:46 +08:00
|
|
|
setQrUri(message.data);
|
2026-06-03 00:12:50 +08:00
|
|
|
}
|
2026-06-02 21:52:15 +08:00
|
|
|
const deeplink = walletConnectDeeplink(preferredWallet, message.data);
|
|
|
|
|
if (mode === "deeplink" && deeplink && isMobileDevice()) {
|
2026-06-02 23:32:39 +08:00
|
|
|
openWalletDeeplink(preferredWallet, deeplink);
|
2026-06-02 21:05:01 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cleanupMessageRef.current?.();
|
|
|
|
|
connector.emitter.on("message", onMessage);
|
|
|
|
|
cleanupMessageRef.current = () =>
|
|
|
|
|
connector.emitter.off("message", onMessage);
|
|
|
|
|
|
2026-06-03 00:25:46 +08:00
|
|
|
cleanupPollingRef.current?.();
|
|
|
|
|
const finishFromAddress = (address: string, source: string) => {
|
|
|
|
|
const alreadyCompleted =
|
|
|
|
|
completedAddressRef.current?.toLowerCase() === address.toLowerCase();
|
|
|
|
|
if (alreadyCompleted) return;
|
|
|
|
|
pendingRef.current = false;
|
|
|
|
|
completedAddressRef.current = address;
|
|
|
|
|
setConnectedAddress(address);
|
|
|
|
|
setQrUri("");
|
|
|
|
|
setState("idle");
|
|
|
|
|
cleanupMessageRef.current?.();
|
|
|
|
|
cleanupMessageRef.current = null;
|
|
|
|
|
cleanupPollingRef.current?.();
|
|
|
|
|
cleanupPollingRef.current = null;
|
|
|
|
|
console.info("[wallet-login] wallet account connected", {
|
|
|
|
|
source,
|
|
|
|
|
preferredWallet,
|
|
|
|
|
address,
|
|
|
|
|
chain: "BNB Chain",
|
|
|
|
|
chainId: bsc.id,
|
|
|
|
|
});
|
|
|
|
|
completeLogin(localWalletToken(address), address);
|
|
|
|
|
console.info("[wallet-login] local wallet session completed", {
|
|
|
|
|
address,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const pollId = window.setInterval(() => {
|
|
|
|
|
void connector
|
|
|
|
|
.getAccounts()
|
|
|
|
|
.then((accounts) => {
|
|
|
|
|
const account = accounts[0];
|
|
|
|
|
if (account) finishFromAddress(account, "connector-poll");
|
|
|
|
|
})
|
|
|
|
|
.catch(() => undefined);
|
|
|
|
|
}, 1000);
|
|
|
|
|
cleanupPollingRef.current = () => window.clearInterval(pollId);
|
|
|
|
|
|
2026-06-02 21:05:01 +08:00
|
|
|
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");
|
2026-06-03 00:25:46 +08:00
|
|
|
finishFromAddress(connectedAddress, "connectAsync");
|
2026-06-02 21:05:01 +08:00
|
|
|
} catch (err) {
|
2026-06-03 00:25:46 +08:00
|
|
|
if (completedAddressRef.current) return;
|
2026-06-02 21:05:01 +08:00
|
|
|
pendingRef.current = false;
|
|
|
|
|
setState("idle");
|
|
|
|
|
setError(
|
|
|
|
|
err instanceof Error ? err.message : "WalletConnect login failed",
|
|
|
|
|
);
|
|
|
|
|
cleanupMessageRef.current?.();
|
|
|
|
|
cleanupMessageRef.current = null;
|
2026-06-03 00:25:46 +08:00
|
|
|
cleanupPollingRef.current?.();
|
|
|
|
|
cleanupPollingRef.current = null;
|
2026-06-02 21:05:01 +08:00
|
|
|
}
|
|
|
|
|
},
|
2026-06-03 00:25:46 +08:00
|
|
|
[available, completeLogin, connectAsync, connectors, 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
|
|
|
}
|