feat: connect wallet favorites to backend
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
||||
getInjectedWallet,
|
||||
type WalletKind,
|
||||
} from "./injected";
|
||||
import { localWalletToken, useWallet } from "./WalletProvider";
|
||||
import { useWallet } from "./WalletProvider";
|
||||
|
||||
const AUTO_LOGIN_PARAMS = ["autoLogin", "autologin"];
|
||||
const ETHEREUM_WAIT_MS = 8000;
|
||||
@@ -52,7 +52,7 @@ function waitForInjected(kind: WalletKind): Promise<boolean> {
|
||||
}
|
||||
|
||||
export function AutoInjectedLogin() {
|
||||
const { completeLogin, status } = useWallet();
|
||||
const { loginAddress, status } = useWallet();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
@@ -69,7 +69,7 @@ export function AutoInjectedLogin() {
|
||||
try {
|
||||
const address = await connectInjectedWallet(kind);
|
||||
if (cancelled) return;
|
||||
completeLogin(localWalletToken(address), address);
|
||||
await loginAddress(address);
|
||||
} catch (err) {
|
||||
console.warn("[wallet-autologin] failed", err);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getInjectedWallet,
|
||||
type WalletKind,
|
||||
} from "./injected";
|
||||
import { localWalletToken, useWallet } from "./WalletProvider";
|
||||
import { useWallet } from "./WalletProvider";
|
||||
import { WalletBrandIcon } from "./WalletBrandIcon";
|
||||
|
||||
const AUTO_LOGIN_PARAM = "autologin";
|
||||
@@ -51,7 +51,7 @@ function isMobileDevice(): boolean {
|
||||
|
||||
export function WalletLoginModal() {
|
||||
const { t } = useI18n();
|
||||
const { closeLoginModal, completeLogin, loginModalOpen } = useWallet();
|
||||
const { closeLoginModal, loginAddress, loginModalOpen } = useWallet();
|
||||
const [selected, setSelected] = useState<WalletKind | null>(null);
|
||||
const [mobileDevice, setMobileDevice] = useState(() => isMobileDevice());
|
||||
const [state, setState] = useState<LoginState>("idle");
|
||||
@@ -101,7 +101,7 @@ export function WalletLoginModal() {
|
||||
try {
|
||||
const address = await connectInjectedWallet(kind);
|
||||
if (mobileDevice) {
|
||||
completeLogin(localWalletToken(address), address);
|
||||
await loginAddress(address);
|
||||
return;
|
||||
}
|
||||
setPendingLogin({ kind, address });
|
||||
@@ -112,9 +112,16 @@ export function WalletLoginModal() {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmPendingLogin = () => {
|
||||
const confirmPendingLogin = async () => {
|
||||
if (!pendingLogin) return;
|
||||
completeLogin(localWalletToken(pendingLogin.address), pendingLogin.address);
|
||||
setState("connecting");
|
||||
setError("");
|
||||
try {
|
||||
await loginAddress(pendingLogin.address);
|
||||
} catch (err) {
|
||||
setState("idle");
|
||||
setError(walletErrorMessage(err, t));
|
||||
}
|
||||
};
|
||||
|
||||
const cancelPendingLogin = () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "react";
|
||||
import { useToast } from "../components/Toast";
|
||||
import { useI18n } from "../i18n";
|
||||
import { fetchWalletMe } from "./api";
|
||||
import { fetchWalletMe, loginWithWallet } from "./api";
|
||||
import { signInWithInjectedWallet, type WalletKind } from "./injected";
|
||||
import { clearWalletToken, readWalletToken, writeWalletToken } from "./token";
|
||||
|
||||
@@ -22,18 +22,6 @@ function walletErrorMessage(error: unknown, t: Translate): string {
|
||||
return t(error.message) || t("walletLoginFailed");
|
||||
}
|
||||
|
||||
const localWalletTokenPrefix = "local-wallet:";
|
||||
|
||||
export function localWalletToken(wallet: string): string {
|
||||
return `${localWalletTokenPrefix}${wallet}`;
|
||||
}
|
||||
|
||||
function walletFromLocalToken(token: string): string | null {
|
||||
return token.startsWith(localWalletTokenPrefix)
|
||||
? token.slice(localWalletTokenPrefix.length)
|
||||
: null;
|
||||
}
|
||||
|
||||
type WalletContextValue = {
|
||||
address: string | null;
|
||||
token: string | null;
|
||||
@@ -43,6 +31,7 @@ type WalletContextValue = {
|
||||
closeLoginModal: () => void;
|
||||
signInInjected: (kind?: WalletKind) => Promise<void>;
|
||||
completeLogin: (token: string, wallet: string) => void;
|
||||
loginAddress: (address: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
};
|
||||
|
||||
@@ -71,13 +60,6 @@ export function WalletProvider({ children }: { children: ReactNode }) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localWallet = walletFromLocalToken(token);
|
||||
if (localWallet) {
|
||||
setAddress(localWallet);
|
||||
setStatus("loggedIn");
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("loading");
|
||||
fetchWalletMe(token)
|
||||
.then((me) => {
|
||||
@@ -110,6 +92,14 @@ export function WalletProvider({ children }: { children: ReactNode }) {
|
||||
[showToast, t],
|
||||
);
|
||||
|
||||
const loginAddress = useCallback(
|
||||
async (walletAddress: string) => {
|
||||
const res = await loginWithWallet(walletAddress);
|
||||
completeLogin(res.token, res.wallet);
|
||||
},
|
||||
[completeLogin],
|
||||
);
|
||||
|
||||
const signInInjected = useCallback(
|
||||
async (kind?: WalletKind) => {
|
||||
try {
|
||||
@@ -141,12 +131,14 @@ export function WalletProvider({ children }: { children: ReactNode }) {
|
||||
closeLoginModal: () => setLoginModalOpen(false),
|
||||
signInInjected,
|
||||
completeLogin,
|
||||
loginAddress,
|
||||
logout,
|
||||
}),
|
||||
[
|
||||
address,
|
||||
completeLogin,
|
||||
loginModalOpen,
|
||||
loginAddress,
|
||||
logout,
|
||||
signInInjected,
|
||||
status,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { apiBase, getJSONAuth, postJSON } from "../api";
|
||||
|
||||
export type WalletNonceResponse = {
|
||||
nonce: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type WalletVerifyResponse = {
|
||||
export type WalletLoginResponse = {
|
||||
token: string;
|
||||
wallet: string;
|
||||
};
|
||||
@@ -36,18 +31,8 @@ export type TokenPocketLoginResult =
|
||||
signature: string;
|
||||
};
|
||||
|
||||
export function requestWalletNonce(
|
||||
address: string,
|
||||
): Promise<WalletNonceResponse> {
|
||||
return postJSON<WalletNonceResponse>("/api/auth/wallet/nonce", { address });
|
||||
}
|
||||
|
||||
export function verifyWalletSignature(params: {
|
||||
address: string;
|
||||
message: string;
|
||||
signature: string;
|
||||
}): Promise<WalletVerifyResponse> {
|
||||
return postJSON<WalletVerifyResponse>("/api/auth/wallet/verify", params);
|
||||
export function loginWithWallet(address: string): Promise<WalletLoginResponse> {
|
||||
return postJSON<WalletLoginResponse>("/api/auth/wallet/login", { address });
|
||||
}
|
||||
|
||||
export function fetchWalletMe(token: string): Promise<WalletMeResponse> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { requestWalletNonce, verifyWalletSignature } from "./api";
|
||||
import { loginWithWallet } from "./api";
|
||||
|
||||
export type WalletKind = "tokenPocket" | "metaMask" | "imToken";
|
||||
|
||||
@@ -26,12 +26,6 @@ function isAddress(value: unknown): value is string {
|
||||
return typeof value === "string" && /^0x[a-fA-F0-9]{40}$/.test(value);
|
||||
}
|
||||
|
||||
function utf8ToHex(value: string): string {
|
||||
return `0x${Array.from(new TextEncoder().encode(value), (byte) =>
|
||||
byte.toString(16).padStart(2, "0"),
|
||||
).join("")}`;
|
||||
}
|
||||
|
||||
function errorText(error: unknown): string {
|
||||
if (!error || typeof error !== "object") return String(error ?? "");
|
||||
const parts: string[] = [];
|
||||
@@ -57,13 +51,6 @@ function normalizeWalletError(error: unknown): Error {
|
||||
return new Error(message || "Wallet login failed");
|
||||
}
|
||||
|
||||
function shouldRetryPersonalSign(error: unknown): boolean {
|
||||
const text = errorText(error);
|
||||
return /wallet must has at least one account|wallet must has one account|must have at least one account|invalid params|invalid account|account not found/i.test(
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureBnbChain(ethereum: EthereumProvider): Promise<void> {
|
||||
const chainId = await ethereum
|
||||
.request<string>({ method: "eth_chainId" })
|
||||
@@ -106,31 +93,6 @@ async function requestInjectedAddress(
|
||||
return requestedAddress;
|
||||
}
|
||||
|
||||
async function personalSign(params: {
|
||||
ethereum: EthereumProvider;
|
||||
message: string;
|
||||
address: string;
|
||||
}): Promise<string> {
|
||||
const { ethereum, message, address } = params;
|
||||
const hexMessage = utf8ToHex(message);
|
||||
try {
|
||||
return await ethereum.request<string>({
|
||||
method: "personal_sign",
|
||||
params: [hexMessage, address],
|
||||
});
|
||||
} catch (error) {
|
||||
if (!shouldRetryPersonalSign(error)) throw error;
|
||||
return ethereum
|
||||
.request<string>({
|
||||
method: "personal_sign",
|
||||
params: [address, hexMessage],
|
||||
})
|
||||
.catch((retryError: unknown): never => {
|
||||
throw normalizeWalletError(retryError);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getInjectedEthereum(): EthereumProvider | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
const maybeWindow = window as typeof window & { ethereum?: EthereumProvider };
|
||||
@@ -214,35 +176,9 @@ export async function signInWithInjectedWallet(kind?: WalletKind): Promise<{
|
||||
token: string;
|
||||
wallet: string;
|
||||
}> {
|
||||
console.info("[wallet-login] start injected", { kind });
|
||||
logWalletProviders();
|
||||
const ethereum = getInjectedWallet(kind);
|
||||
if (!ethereum) {
|
||||
console.warn("[wallet-login] no injected provider found");
|
||||
throw new Error("No injected wallet found");
|
||||
}
|
||||
|
||||
console.info("[wallet-login] requesting BNB wallet account…");
|
||||
const address = await requestInjectedAddress(ethereum);
|
||||
console.info("[wallet-login] account", address);
|
||||
|
||||
console.info("[wallet-login] ensuring BNB Chain (0x38)…");
|
||||
await ensureBnbChain(ethereum);
|
||||
|
||||
console.info("[wallet-login] requesting nonce for", address);
|
||||
const nonce = await requestWalletNonce(address);
|
||||
console.info("[wallet-login] got nonce, requesting personal_sign…");
|
||||
const signature = await personalSign({
|
||||
ethereum,
|
||||
message: nonce.message,
|
||||
address,
|
||||
});
|
||||
console.info("[wallet-login] signed, verifying with backend…");
|
||||
const result = await verifyWalletSignature({
|
||||
address,
|
||||
message: nonce.message,
|
||||
signature,
|
||||
});
|
||||
console.info("[wallet-login] verified, wallet =", result.wallet);
|
||||
const address = await connectInjectedWallet(kind);
|
||||
console.info("[wallet-login] requesting backend login for", address);
|
||||
const result = await loginWithWallet(address);
|
||||
console.info("[wallet-login] logged in, wallet =", result.wallet);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getInjectedWallet,
|
||||
type WalletKind,
|
||||
} from "./injected";
|
||||
import { localWalletToken, useWallet } from "./WalletProvider";
|
||||
import { useWallet } from "./WalletProvider";
|
||||
|
||||
export type WalletConnectLoginState = "idle" | "connecting" | "signing";
|
||||
export type WalletConnectLoginMode = "deeplink" | "qr";
|
||||
@@ -96,7 +96,7 @@ function connectorMatchesWallet(
|
||||
|
||||
export function useWalletConnectLogin() {
|
||||
const available = hasWalletConnectProjectId();
|
||||
const { address: localAddress, completeLogin } = useWallet();
|
||||
const { address: localAddress, loginAddress } = useWallet();
|
||||
const { address: wagmiAddress, isConnected: wagmiConnected } = useAccount();
|
||||
const { connectAsync, connectors } = useConnect();
|
||||
const { disconnectAsync } = useDisconnect();
|
||||
@@ -140,12 +140,17 @@ export function useWalletConnectLogin() {
|
||||
chain: "BNB Chain",
|
||||
chainId: bsc.id,
|
||||
});
|
||||
completeLogin(localWalletToken(wagmiAddress), wagmiAddress);
|
||||
console.info("[wallet-login] local wallet session completed", {
|
||||
address: wagmiAddress,
|
||||
});
|
||||
void loginAddress(wagmiAddress)
|
||||
.then(() => {
|
||||
console.info("[wallet-login] wallet session completed", {
|
||||
address: wagmiAddress,
|
||||
});
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setError(err instanceof Error ? err.message : "Wallet login failed");
|
||||
});
|
||||
}
|
||||
}, [completeLogin, localAddress, wagmiAddress, wagmiConnected]);
|
||||
}, [localAddress, loginAddress, wagmiAddress, wagmiConnected]);
|
||||
|
||||
const start = useCallback(
|
||||
async (
|
||||
@@ -173,7 +178,7 @@ export function useWalletConnectLogin() {
|
||||
chain: "BNB Chain",
|
||||
chainId: bsc.id,
|
||||
});
|
||||
completeLogin(localWalletToken(injectedAddress), injectedAddress);
|
||||
await loginAddress(injectedAddress);
|
||||
setState("idle");
|
||||
return;
|
||||
} catch (err) {
|
||||
@@ -256,10 +261,17 @@ export function useWalletConnectLogin() {
|
||||
chain: "BNB Chain",
|
||||
chainId: bsc.id,
|
||||
});
|
||||
completeLogin(localWalletToken(address), address);
|
||||
console.info("[wallet-login] local wallet session completed", {
|
||||
address,
|
||||
});
|
||||
void loginAddress(address)
|
||||
.then(() => {
|
||||
console.info("[wallet-login] wallet session completed", {
|
||||
address,
|
||||
});
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Wallet login failed",
|
||||
);
|
||||
});
|
||||
};
|
||||
const pollId = window.setInterval(() => {
|
||||
void connector
|
||||
@@ -293,7 +305,7 @@ export function useWalletConnectLogin() {
|
||||
cleanupPollingRef.current = null;
|
||||
}
|
||||
},
|
||||
[available, completeLogin, connectAsync, connectors, disconnectAsync],
|
||||
[available, connectAsync, connectors, disconnectAsync, loginAddress],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user