terry-wallet-login #15
@@ -171,9 +171,26 @@ export const enDict: Dict = {
|
|||||||
walletDisconnect: "Disconnect",
|
walletDisconnect: "Disconnect",
|
||||||
walletLoginTitle: "Connect wallet",
|
walletLoginTitle: "Connect wallet",
|
||||||
walletLoginDesc:
|
walletLoginDesc:
|
||||||
"Sign a message to verify your wallet address. No transaction or gas fee.",
|
"Sign a message to verify your BNB Chain wallet address. No transaction or gas fee.",
|
||||||
walletInjected: "Browser wallet / DApp browser",
|
walletInjected: "Use browser wallet",
|
||||||
walletInjectedDesc: "Use the wallet already available in this browser.",
|
walletInjectedDesc: "Sign with the wallet available in this browser.",
|
||||||
|
walletNoBrowserWallet: "No browser wallet detected",
|
||||||
|
walletNoBrowserWalletDesc:
|
||||||
|
"Install or enable a browser wallet extension, such as MetaMask.",
|
||||||
|
walletOpenWalletApp: "Open wallet app",
|
||||||
|
walletOpenWalletAppDesc:
|
||||||
|
"Open this site in your wallet app, then sign to log in.",
|
||||||
|
walletTokenPocket: "TokenPocket",
|
||||||
|
walletMetaMask: "MetaMask",
|
||||||
|
walletImToken: "imToken",
|
||||||
|
walletChooseDesktop:
|
||||||
|
"Choose the wallet you want to use. On desktop, install the matching browser extension and switch to BNB Chain.",
|
||||||
|
walletChooseMobile: "Choose a wallet app to open this site.",
|
||||||
|
walletDesktopHint:
|
||||||
|
"If no wallet opens after clicking, make sure the matching browser extension is installed and enabled.",
|
||||||
|
walletInstallSelected:
|
||||||
|
"No {wallet} browser extension detected. Install or enable it, then try again.",
|
||||||
|
walletOpen: "Open",
|
||||||
walletTokenPocketQr: "TokenPocket QR login",
|
walletTokenPocketQr: "TokenPocket QR login",
|
||||||
walletTokenPocketQrDesc:
|
walletTokenPocketQrDesc:
|
||||||
"Recommended for China users. Scan with TokenPocket and sign to return login to this browser.",
|
"Recommended for China users. Scan with TokenPocket and sign to return login to this browser.",
|
||||||
|
|||||||
@@ -164,9 +164,23 @@ export const zhDict: Dict = {
|
|||||||
walletConnectedAs: "已连接钱包",
|
walletConnectedAs: "已连接钱包",
|
||||||
walletDisconnect: "断开连接",
|
walletDisconnect: "断开连接",
|
||||||
walletLoginTitle: "连接钱包",
|
walletLoginTitle: "连接钱包",
|
||||||
walletLoginDesc: "签名验证钱包地址,不会发起交易,也不需要 Gas。",
|
walletLoginDesc: "签名验证 BNB Chain 钱包地址,不会发起交易,也不需要 Gas。",
|
||||||
walletInjected: "浏览器钱包 / 钱包内置浏览器",
|
walletInjected: "使用浏览器钱包登录",
|
||||||
walletInjectedDesc: "使用当前浏览器里已经注入的钱包。",
|
walletInjectedDesc: "签名验证当前浏览器里的钱包。",
|
||||||
|
walletNoBrowserWallet: "未检测到浏览器钱包",
|
||||||
|
walletNoBrowserWalletDesc: "请安装或启用浏览器钱包插件,例如 MetaMask。",
|
||||||
|
walletOpenWalletApp: "打开钱包 App",
|
||||||
|
walletOpenWalletAppDesc: "请在钱包 App 中打开本站后签名登录。",
|
||||||
|
walletTokenPocket: "TokenPocket",
|
||||||
|
walletMetaMask: "MetaMask",
|
||||||
|
walletImToken: "imToken",
|
||||||
|
walletChooseDesktop:
|
||||||
|
"选择你要使用的钱包。电脑端需要先安装对应浏览器插件,并切换到 BNB Chain。",
|
||||||
|
walletChooseMobile: "选择钱包 App 打开本站。",
|
||||||
|
walletDesktopHint:
|
||||||
|
"如果点击后没有弹出钱包,请确认已安装并启用对应的钱包浏览器插件。",
|
||||||
|
walletInstallSelected: "未检测到 {wallet} 浏览器插件,请先安装或启用后再试。",
|
||||||
|
walletOpen: "打开",
|
||||||
walletTokenPocketQr: "TokenPocket 扫码登录",
|
walletTokenPocketQr: "TokenPocket 扫码登录",
|
||||||
walletTokenPocketQrDesc:
|
walletTokenPocketQrDesc:
|
||||||
"推荐中国用户使用。用 TokenPocket 扫码签名后,会回到当前浏览器完成登录。",
|
"推荐中国用户使用。用 TokenPocket 扫码签名后,会回到当前浏览器完成登录。",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import { http, createConfig, WagmiProvider } from "wagmi";
|
import { http, createConfig, WagmiProvider } from "wagmi";
|
||||||
import { mainnet } from "wagmi/chains";
|
import { bsc } from "wagmi/chains";
|
||||||
|
|
||||||
const projectId =
|
const projectId =
|
||||||
import.meta.env.VITE_WALLETCONNECT_PROJECT_ID || "ark-library-dev-only";
|
import.meta.env.VITE_WALLETCONNECT_PROJECT_ID || "ark-library-dev-only";
|
||||||
@@ -32,11 +32,11 @@ const connectors = connectorsForWallets(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const wagmiConfig = createConfig({
|
export const wagmiConfig = createConfig({
|
||||||
chains: [mainnet],
|
chains: [bsc],
|
||||||
connectors,
|
connectors,
|
||||||
ssr: false,
|
ssr: false,
|
||||||
transports: {
|
transports: {
|
||||||
[mainnet.id]: http(),
|
[bsc.id]: http(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,159 +1,84 @@
|
|||||||
import { useConnectModal } from "@rainbow-me/rainbowkit";
|
import { useEffect, useState } from "react";
|
||||||
import { QRCodeSVG } from "qrcode.react";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useAccount, useSignMessage } from "wagmi";
|
|
||||||
import { useToast } from "../components/Toast";
|
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
import {
|
|
||||||
createTokenPocketLoginRequest,
|
|
||||||
fetchTokenPocketLoginResult,
|
|
||||||
requestWalletNonce,
|
|
||||||
verifyWalletSignature,
|
|
||||||
type TokenPocketLoginRequest,
|
|
||||||
} from "./api";
|
|
||||||
import { openWalletDeepLink } from "./deepLinks";
|
import { openWalletDeepLink } from "./deepLinks";
|
||||||
|
import { getInjectedWallet, type WalletKind } from "./injected";
|
||||||
import { useWallet } from "./WalletProvider";
|
import { useWallet } from "./WalletProvider";
|
||||||
|
|
||||||
const pollIntervalMs = 1800;
|
type ModalState = "idle" | "signing";
|
||||||
|
|
||||||
type ModalState = "idle" | "tpLoading" | "tpPolling" | "rainbowSigning";
|
type WalletOption = {
|
||||||
|
kind: WalletKind;
|
||||||
|
labelKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const walletOptions: WalletOption[] = [
|
||||||
|
{ kind: "tokenPocket", labelKey: "walletTokenPocket" },
|
||||||
|
{ kind: "metaMask", labelKey: "walletMetaMask" },
|
||||||
|
{ kind: "imToken", labelKey: "walletImToken" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function isMobileDevice(): boolean {
|
||||||
|
if (typeof navigator === "undefined") return false;
|
||||||
|
const ua = navigator.userAgent || "";
|
||||||
|
return (
|
||||||
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(
|
||||||
|
ua,
|
||||||
|
) ||
|
||||||
|
(/Macintosh/i.test(ua) && navigator.maxTouchPoints > 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function WalletLoginModal() {
|
export function WalletLoginModal() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { showToast } = useToast();
|
const { closeLoginModal, loginModalOpen, signInInjected } = useWallet();
|
||||||
const { closeLoginModal, completeLogin, loginModalOpen, signInInjected } =
|
|
||||||
useWallet();
|
|
||||||
const { openConnectModal } = useConnectModal();
|
|
||||||
const { address, isConnected } = useAccount();
|
|
||||||
const { signMessageAsync } = useSignMessage();
|
|
||||||
const [state, setState] = useState<ModalState>("idle");
|
const [state, setState] = useState<ModalState>("idle");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [tpRequest, setTpRequest] = useState<TokenPocketLoginRequest | null>(
|
const [mobileDevice, setMobileDevice] = useState(false);
|
||||||
null,
|
const [selectedWallet, setSelectedWallet] = useState<WalletKind | null>(null);
|
||||||
);
|
|
||||||
const [rainbowPending, setRainbowPending] = useState(false);
|
useEffect(() => {
|
||||||
const rainbowSigningRef = useRef(false);
|
if (!loginModalOpen) return;
|
||||||
|
setMobileDevice(isMobileDevice());
|
||||||
|
setSelectedWallet(null);
|
||||||
|
setError("");
|
||||||
|
}, [loginModalOpen]);
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
if (state === "tpLoading" || state === "rainbowSigning") return;
|
if (state === "signing") return;
|
||||||
closeLoginModal();
|
closeLoginModal();
|
||||||
setError("");
|
setError("");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loginModalOpen || !tpRequest) return;
|
|
||||||
if (state !== "tpPolling") return;
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
const abortController = new AbortController();
|
|
||||||
const poll = async () => {
|
|
||||||
try {
|
|
||||||
const result = await fetchTokenPocketLoginResult(
|
|
||||||
tpRequest.actionId,
|
|
||||||
abortController.signal,
|
|
||||||
);
|
|
||||||
if (cancelled) return;
|
|
||||||
if (result.status === "completed") {
|
|
||||||
const verified = await verifyWalletSignature({
|
|
||||||
address: result.address,
|
|
||||||
message: result.message,
|
|
||||||
signature: result.signature,
|
|
||||||
});
|
|
||||||
if (cancelled) return;
|
|
||||||
completeLogin(verified.token, verified.wallet);
|
|
||||||
showToast(t("walletLoginSuccess"));
|
|
||||||
setState("idle");
|
|
||||||
setTpRequest(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result.status === "expired" || result.status === "failed") {
|
|
||||||
setState("idle");
|
|
||||||
setError(result.error || t("walletTpExpired"));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (
|
|
||||||
!cancelled &&
|
|
||||||
!(err instanceof DOMException && err.name === "AbortError")
|
|
||||||
) {
|
|
||||||
setError(t("walletLoginFailed"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void poll();
|
|
||||||
const timer = window.setInterval(() => void poll(), pollIntervalMs);
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
abortController.abort();
|
|
||||||
window.clearInterval(timer);
|
|
||||||
};
|
|
||||||
}, [completeLogin, loginModalOpen, state, tpRequest, t, showToast]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!rainbowPending || !isConnected || !address) return;
|
|
||||||
if (rainbowSigningRef.current) return;
|
|
||||||
rainbowSigningRef.current = true;
|
|
||||||
setState("rainbowSigning");
|
|
||||||
setError("");
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const nonce = await requestWalletNonce(address);
|
|
||||||
const signature = await signMessageAsync({ message: nonce.message });
|
|
||||||
const verified = await verifyWalletSignature({
|
|
||||||
address,
|
|
||||||
message: nonce.message,
|
|
||||||
signature,
|
|
||||||
});
|
|
||||||
completeLogin(verified.token, verified.wallet);
|
|
||||||
showToast(t("walletLoginSuccess"));
|
|
||||||
} catch (err) {
|
|
||||||
const message =
|
|
||||||
err instanceof Error ? err.message : t("walletLoginFailed");
|
|
||||||
setError(message || t("walletLoginFailed"));
|
|
||||||
showToast(t("walletLoginFailed"), "error");
|
|
||||||
} finally {
|
|
||||||
setState("idle");
|
|
||||||
setRainbowPending(false);
|
|
||||||
rainbowSigningRef.current = false;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [
|
|
||||||
address,
|
|
||||||
isConnected,
|
|
||||||
rainbowPending,
|
|
||||||
showToast,
|
|
||||||
signMessageAsync,
|
|
||||||
t,
|
|
||||||
completeLogin,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!loginModalOpen) return null;
|
if (!loginModalOpen) return null;
|
||||||
|
|
||||||
const startInjected = async () => {
|
const walletName = (kind: WalletKind) => {
|
||||||
setError("");
|
if (kind === "tokenPocket") return t("walletTokenPocket");
|
||||||
setState("idle");
|
if (kind === "metaMask") return t("walletMetaMask");
|
||||||
await signInInjected().catch(() => undefined);
|
return t("walletImToken");
|
||||||
};
|
};
|
||||||
|
|
||||||
const startTokenPocketQr = async () => {
|
const chooseWallet = async (kind: WalletKind) => {
|
||||||
setError("");
|
setError("");
|
||||||
setState("tpLoading");
|
setSelectedWallet(kind);
|
||||||
try {
|
|
||||||
const req = await createTokenPocketLoginRequest();
|
const injectedWallet = getInjectedWallet(kind);
|
||||||
setTpRequest(req);
|
if (!injectedWallet) {
|
||||||
setState("tpPolling");
|
if (mobileDevice) {
|
||||||
} catch {
|
openWalletDeepLink(kind);
|
||||||
setState("idle");
|
return;
|
||||||
setError(t("walletTpQrFailed"));
|
}
|
||||||
|
setError(
|
||||||
|
t("walletInstallSelected").replace("{wallet}", walletName(kind)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const startRainbowFallback = () => {
|
setState("signing");
|
||||||
setError("");
|
await signInInjected(kind)
|
||||||
setRainbowPending(true);
|
.catch((err) => {
|
||||||
openConnectModal?.();
|
setError(err instanceof Error ? err.message : t("walletLoginFailed"));
|
||||||
if (!openConnectModal) setError(t("walletRainbowUnavailable"));
|
})
|
||||||
|
.finally(() => setState("idle"));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -186,97 +111,38 @@ export function WalletLoginModal() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 grid gap-3">
|
<div className="mt-5 grid gap-3">
|
||||||
<button
|
<p className="text-sm font-medium text-neutral-300">
|
||||||
type="button"
|
{mobileDevice ? t("walletChooseMobile") : t("walletChooseDesktop")}
|
||||||
onClick={() => void startInjected()}
|
|
||||||
className="rounded-2xl border border-ark-gold/40 bg-ark-gold/10 p-4 text-left transition hover:bg-ark-gold/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80"
|
|
||||||
>
|
|
||||||
<span className="block font-semibold text-ark-gold2">
|
|
||||||
{t("walletInjected")}
|
|
||||||
</span>
|
|
||||||
<span className="mt-1 block text-sm text-neutral-400">
|
|
||||||
{t("walletInjectedDesc")}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-white">
|
|
||||||
{t("walletTokenPocketQr")}
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-sm leading-6 text-neutral-400">
|
|
||||||
{t("walletTokenPocketQrDesc")}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<div className="grid gap-2">
|
||||||
|
{walletOptions.map((option) => {
|
||||||
|
const signingThis =
|
||||||
|
state === "signing" && selectedWallet === option.kind;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
|
key={option.kind}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void startTokenPocketQr()}
|
onClick={() => void chooseWallet(option.kind)}
|
||||||
disabled={state === "tpLoading"}
|
disabled={state === "signing"}
|
||||||
className="shrink-0 rounded-full bg-ark-gold px-4 py-2 text-sm font-semibold text-black transition hover:bg-ark-gold2 disabled:cursor-wait disabled:opacity-70"
|
className="flex items-center justify-between rounded-2xl border border-white/10 bg-[#20202a] px-4 py-4 text-left text-base font-semibold text-neutral-100 transition hover:border-ark-gold/50 hover:bg-ark-gold/10 disabled:cursor-wait disabled:opacity-70"
|
||||||
>
|
>
|
||||||
{state === "tpLoading" ? t("loading") : t("walletGenerateQr")}
|
<span>{t(option.labelKey)}</span>
|
||||||
</button>
|
<span className="text-sm text-ark-gold">
|
||||||
</div>
|
{signingThis
|
||||||
{tpRequest ? (
|
|
||||||
<div className="mt-4 grid place-items-center gap-3 rounded-2xl bg-white p-4 text-center">
|
|
||||||
<QRCodeSVG value={tpRequest.qrUrl} size={196} level="M" />
|
|
||||||
<p className="text-xs font-medium text-neutral-700">
|
|
||||||
{t("walletQrUseAnotherDevice")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2 sm:grid-cols-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => openWalletDeepLink("tokenPocket")}
|
|
||||||
className="rounded-2xl border border-white/10 bg-[#20202a] px-4 py-3 text-sm font-semibold text-neutral-100 transition hover:border-ark-gold/40"
|
|
||||||
>
|
|
||||||
{t("walletOpenTokenPocket")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => openWalletDeepLink("metaMask")}
|
|
||||||
className="rounded-2xl border border-white/10 bg-[#20202a] px-4 py-3 text-sm font-semibold text-neutral-100 transition hover:border-ark-gold/40"
|
|
||||||
>
|
|
||||||
{t("walletOpenMetaMask")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => openWalletDeepLink("imToken")}
|
|
||||||
className="rounded-2xl border border-white/10 bg-[#20202a] px-4 py-3 text-sm font-semibold text-neutral-100 transition hover:border-ark-gold/40"
|
|
||||||
>
|
|
||||||
{t("walletOpenImToken")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-dashed border-white/15 p-4">
|
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-white">
|
|
||||||
{t("walletRainbowFallback")}
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-sm leading-6 text-neutral-400">
|
|
||||||
{t("walletRainbowFallbackDesc")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={startRainbowFallback}
|
|
||||||
disabled={state === "rainbowSigning"}
|
|
||||||
className="shrink-0 rounded-full border border-ark-gold/50 px-4 py-2 text-sm font-semibold text-ark-gold transition hover:bg-ark-gold/10 disabled:cursor-wait disabled:opacity-70"
|
|
||||||
>
|
|
||||||
{state === "rainbowSigning"
|
|
||||||
? t("walletSigning")
|
? t("walletSigning")
|
||||||
: t("walletOpenRainbow")}
|
: mobileDevice
|
||||||
|
? t("walletOpen")
|
||||||
|
: t("walletConnect")}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3 text-xs leading-5 text-yellow-200/80">
|
{!mobileDevice ? (
|
||||||
{t("walletNetworkWarning")}
|
<p className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-xs leading-5 text-neutral-400">
|
||||||
|
{t("walletDesktopHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { useToast } from "../components/Toast";
|
import { useToast } from "../components/Toast";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
import { fetchWalletMe } from "./api";
|
import { fetchWalletMe } from "./api";
|
||||||
import { signInWithInjectedWallet } from "./injected";
|
import { signInWithInjectedWallet, type WalletKind } from "./injected";
|
||||||
import { clearWalletToken, readWalletToken, writeWalletToken } from "./token";
|
import { clearWalletToken, readWalletToken, writeWalletToken } from "./token";
|
||||||
|
|
||||||
type WalletStatus = "loading" | "loggedOut" | "loggedIn";
|
type WalletStatus = "loading" | "loggedOut" | "loggedIn";
|
||||||
@@ -22,7 +22,7 @@ type WalletContextValue = {
|
|||||||
loginModalOpen: boolean;
|
loginModalOpen: boolean;
|
||||||
openLoginModal: () => void;
|
openLoginModal: () => void;
|
||||||
closeLoginModal: () => void;
|
closeLoginModal: () => void;
|
||||||
signInInjected: () => Promise<void>;
|
signInInjected: (kind?: WalletKind) => Promise<void>;
|
||||||
completeLogin: (token: string, wallet: string) => void;
|
completeLogin: (token: string, wallet: string) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
};
|
};
|
||||||
@@ -80,9 +80,10 @@ export function WalletProvider({ children }: { children: ReactNode }) {
|
|||||||
setLoginModalOpen(false);
|
setLoginModalOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const signInInjected = useCallback(async () => {
|
const signInInjected = useCallback(
|
||||||
|
async (kind?: WalletKind) => {
|
||||||
try {
|
try {
|
||||||
const res = await signInWithInjectedWallet();
|
const res = await signInWithInjectedWallet(kind);
|
||||||
completeLogin(res.token, res.wallet);
|
completeLogin(res.token, res.wallet);
|
||||||
showToast(t("walletLoginSuccess"));
|
showToast(t("walletLoginSuccess"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -91,7 +92,9 @@ export function WalletProvider({ children }: { children: ReactNode }) {
|
|||||||
showToast(message || t("walletLoginFailed"), "error");
|
showToast(message || t("walletLoginFailed"), "error");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, [completeLogin, showToast, t]);
|
},
|
||||||
|
[completeLogin, showToast, t],
|
||||||
|
);
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
clearWalletToken();
|
clearWalletToken();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function walletDeepLink(
|
|||||||
switch (kind) {
|
switch (kind) {
|
||||||
case "tokenPocket":
|
case "tokenPocket":
|
||||||
return `tpdapp://open?params=${encodeURIComponent(
|
return `tpdapp://open?params=${encodeURIComponent(
|
||||||
JSON.stringify({ url: dappUrl, chain: "ETH" }),
|
JSON.stringify({ url: dappUrl, chain: "BSC" }),
|
||||||
)}`;
|
)}`;
|
||||||
case "metaMask":
|
case "metaMask":
|
||||||
return `https://metamask.app.link/dapp/${encodeURIComponent(
|
return `https://metamask.app.link/dapp/${encodeURIComponent(
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { requestWalletNonce, verifyWalletSignature } from "./api";
|
import { requestWalletNonce, verifyWalletSignature } from "./api";
|
||||||
|
|
||||||
|
export type WalletKind = "tokenPocket" | "metaMask" | "imToken";
|
||||||
|
|
||||||
|
const bnbChainIdHex = "0x38";
|
||||||
|
|
||||||
export type EthereumProvider = {
|
export type EthereumProvider = {
|
||||||
|
isMetaMask?: boolean;
|
||||||
|
isTokenPocket?: boolean;
|
||||||
|
isImToken?: boolean;
|
||||||
|
providers?: EthereumProvider[];
|
||||||
request: <T = unknown>(args: {
|
request: <T = unknown>(args: {
|
||||||
method: string;
|
method: string;
|
||||||
params?: unknown[];
|
params?: unknown[];
|
||||||
@@ -13,13 +21,54 @@ export function getInjectedEthereum(): EthereumProvider | null {
|
|||||||
return maybeWindow.ethereum ?? null;
|
return maybeWindow.ethereum ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function signInWithInjectedWallet(): Promise<{
|
export function getInjectedWallet(kind?: WalletKind): EthereumProvider | null {
|
||||||
|
const ethereum = getInjectedEthereum();
|
||||||
|
if (!ethereum || !kind) return ethereum;
|
||||||
|
const providers = ethereum.providers?.length
|
||||||
|
? ethereum.providers
|
||||||
|
: [ethereum];
|
||||||
|
const match = providers.find((provider) => {
|
||||||
|
if (kind === "metaMask") return provider.isMetaMask;
|
||||||
|
if (kind === "tokenPocket") return provider.isTokenPocket;
|
||||||
|
if (kind === "imToken") return provider.isImToken;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return match ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureBnbChain(ethereum: EthereumProvider): Promise<void> {
|
||||||
|
try {
|
||||||
|
await ethereum.request({
|
||||||
|
method: "wallet_switchEthereumChain",
|
||||||
|
params: [{ chainId: bnbChainIdHex }],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const code = (error as { code?: number | string }).code;
|
||||||
|
if (code !== 4902 && code !== "4902") throw error;
|
||||||
|
await ethereum.request({
|
||||||
|
method: "wallet_addEthereumChain",
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
blockExplorerUrls: ["https://bscscan.com"],
|
||||||
|
chainId: bnbChainIdHex,
|
||||||
|
chainName: "BNB Smart Chain",
|
||||||
|
nativeCurrency: { decimals: 18, name: "BNB", symbol: "BNB" },
|
||||||
|
rpcUrls: ["https://bsc-dataseed.binance.org"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signInWithInjectedWallet(kind?: WalletKind): Promise<{
|
||||||
token: string;
|
token: string;
|
||||||
wallet: string;
|
wallet: string;
|
||||||
}> {
|
}> {
|
||||||
const ethereum = getInjectedEthereum();
|
const ethereum = getInjectedWallet(kind);
|
||||||
if (!ethereum) throw new Error("No injected wallet found");
|
if (!ethereum) throw new Error("No injected wallet found");
|
||||||
|
|
||||||
|
await ensureBnbChain(ethereum);
|
||||||
|
|
||||||
const accounts = await ethereum.request<string[]>({
|
const accounts = await ethereum.request<string[]>({
|
||||||
method: "eth_requestAccounts",
|
method: "eth_requestAccounts",
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user