2026-06-02 02:58:01 +08:00
|
|
|
import { useEffect, useState } from "react";
|
2026-06-02 00:32:46 +08:00
|
|
|
import { useI18n } from "../i18n";
|
|
|
|
|
import { openWalletDeepLink } from "./deepLinks";
|
2026-06-02 02:58:01 +08:00
|
|
|
import { getInjectedWallet, type WalletKind } from "./injected";
|
2026-06-02 00:32:46 +08:00
|
|
|
import { useWallet } from "./WalletProvider";
|
|
|
|
|
|
2026-06-02 02:58:01 +08:00
|
|
|
type ModalState = "idle" | "signing";
|
2026-06-02 00:32:46 +08:00
|
|
|
|
2026-06-02 02:58:01 +08:00
|
|
|
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)
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-06-02 00:32:46 +08:00
|
|
|
|
|
|
|
|
export function WalletLoginModal() {
|
|
|
|
|
const { t } = useI18n();
|
2026-06-02 02:58:01 +08:00
|
|
|
const { closeLoginModal, loginModalOpen, signInInjected } = useWallet();
|
2026-06-02 00:32:46 +08:00
|
|
|
const [state, setState] = useState<ModalState>("idle");
|
|
|
|
|
const [error, setError] = useState("");
|
2026-06-02 02:58:01 +08:00
|
|
|
const [mobileDevice, setMobileDevice] = useState(false);
|
|
|
|
|
const [selectedWallet, setSelectedWallet] = useState<WalletKind | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!loginModalOpen) return;
|
|
|
|
|
setMobileDevice(isMobileDevice());
|
|
|
|
|
setSelectedWallet(null);
|
|
|
|
|
setError("");
|
|
|
|
|
}, [loginModalOpen]);
|
2026-06-02 00:32:46 +08:00
|
|
|
|
|
|
|
|
const close = () => {
|
2026-06-02 02:58:01 +08:00
|
|
|
if (state === "signing") return;
|
2026-06-02 00:57:37 +08:00
|
|
|
closeLoginModal();
|
2026-06-02 00:32:46 +08:00
|
|
|
setError("");
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-02 00:57:37 +08:00
|
|
|
if (!loginModalOpen) return null;
|
2026-06-02 00:32:46 +08:00
|
|
|
|
2026-06-02 02:58:01 +08:00
|
|
|
const walletName = (kind: WalletKind) => {
|
|
|
|
|
if (kind === "tokenPocket") return t("walletTokenPocket");
|
|
|
|
|
if (kind === "metaMask") return t("walletMetaMask");
|
|
|
|
|
return t("walletImToken");
|
2026-06-02 00:32:46 +08:00
|
|
|
};
|
|
|
|
|
|
2026-06-02 02:58:01 +08:00
|
|
|
const chooseWallet = async (kind: WalletKind) => {
|
2026-06-02 00:32:46 +08:00
|
|
|
setError("");
|
2026-06-02 02:58:01 +08:00
|
|
|
setSelectedWallet(kind);
|
|
|
|
|
|
|
|
|
|
const injectedWallet = getInjectedWallet(kind);
|
|
|
|
|
if (!injectedWallet) {
|
|
|
|
|
if (mobileDevice) {
|
|
|
|
|
openWalletDeepLink(kind);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setError(
|
|
|
|
|
t("walletInstallSelected").replace("{wallet}", walletName(kind)),
|
|
|
|
|
);
|
|
|
|
|
return;
|
2026-06-02 00:32:46 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 02:58:01 +08:00
|
|
|
setState("signing");
|
|
|
|
|
await signInInjected(kind)
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
setError(err instanceof Error ? err.message : t("walletLoginFailed"));
|
|
|
|
|
})
|
|
|
|
|
.finally(() => setState("idle"));
|
2026-06-02 00:32:46 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 z-[120] flex items-end justify-center bg-black/70 px-3 pb-3 pt-10 backdrop-blur-sm md:items-center md:p-6"
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-labelledby="wallet-login-title"
|
|
|
|
|
>
|
|
|
|
|
<div className="max-h-[92dvh] w-full max-w-[520px] overflow-y-auto rounded-3xl border border-white/10 bg-[#17171d] p-5 shadow-2xl shadow-black/70 md:p-6">
|
|
|
|
|
<div className="flex items-start justify-between gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h2
|
|
|
|
|
id="wallet-login-title"
|
|
|
|
|
className="text-xl font-semibold text-white"
|
|
|
|
|
>
|
|
|
|
|
{t("walletLoginTitle")}
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="mt-2 text-sm leading-6 text-neutral-400">
|
|
|
|
|
{t("walletLoginDesc")}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={close}
|
|
|
|
|
className="rounded-full border border-white/10 px-3 py-1.5 text-sm text-neutral-300 transition hover:border-ark-gold/50 hover:text-ark-gold"
|
|
|
|
|
>
|
|
|
|
|
{t("close")}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-5 grid gap-3">
|
2026-06-02 02:58:01 +08:00
|
|
|
<p className="text-sm font-medium text-neutral-300">
|
|
|
|
|
{mobileDevice ? t("walletChooseMobile") : t("walletChooseDesktop")}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
{walletOptions.map((option) => {
|
|
|
|
|
const signingThis =
|
|
|
|
|
state === "signing" && selectedWallet === option.kind;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={option.kind}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => void chooseWallet(option.kind)}
|
|
|
|
|
disabled={state === "signing"}
|
|
|
|
|
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"
|
|
|
|
|
>
|
|
|
|
|
<span>{t(option.labelKey)}</span>
|
|
|
|
|
<span className="text-sm text-ark-gold">
|
|
|
|
|
{signingThis
|
|
|
|
|
? t("walletSigning")
|
|
|
|
|
: mobileDevice
|
|
|
|
|
? t("walletOpen")
|
|
|
|
|
: t("walletConnect")}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-06-02 00:32:46 +08:00
|
|
|
</div>
|
2026-06-02 02:58:01 +08:00
|
|
|
{!mobileDevice ? (
|
|
|
|
|
<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")}
|
2026-06-02 00:32:46 +08:00
|
|
|
</p>
|
2026-06-02 02:58:01 +08:00
|
|
|
) : null}
|
2026-06-02 00:32:46 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{error ? (
|
|
|
|
|
<p className="mt-4 rounded-2xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-200">
|
|
|
|
|
{error}
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|