2026-06-02 03:43:13 +08:00
|
|
|
import { QRCodeSVG } from "qrcode.react";
|
2026-06-02 21:25:05 +08:00
|
|
|
import { LoaderCircle, X } from "lucide-react";
|
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";
|
2026-06-02 21:25:05 +08:00
|
|
|
import type { WalletKind } from "./injected";
|
2026-06-02 00:32:46 +08:00
|
|
|
import { useWallet } from "./WalletProvider";
|
2026-06-02 03:43:13 +08:00
|
|
|
import { useWalletConnectLogin } from "./useWalletConnectLogin";
|
|
|
|
|
import { WalletBrandIcon } from "./WalletBrandIcon";
|
2026-06-02 00:32:46 +08:00
|
|
|
|
2026-06-02 21:05:01 +08:00
|
|
|
const wallets: WalletKind[] = ["tokenPocket", "metaMask", "imToken"];
|
2026-06-02 02:58:01 +08:00
|
|
|
|
|
|
|
|
function isMobileDevice(): boolean {
|
|
|
|
|
if (typeof navigator === "undefined") return false;
|
2026-06-02 21:25:05 +08:00
|
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(
|
|
|
|
|
navigator.userAgent || "",
|
|
|
|
|
);
|
2026-06-02 02:58:01 +08:00
|
|
|
}
|
2026-06-02 00:32:46 +08:00
|
|
|
|
|
|
|
|
export function WalletLoginModal() {
|
|
|
|
|
const { t } = useI18n();
|
2026-06-02 21:25:05 +08:00
|
|
|
const { closeLoginModal, loginModalOpen } = useWallet();
|
2026-06-02 03:43:13 +08:00
|
|
|
const wc = useWalletConnectLogin();
|
2026-06-02 21:05:01 +08:00
|
|
|
const [selected, setSelected] = useState<WalletKind | null>(null);
|
2026-06-02 02:58:01 +08:00
|
|
|
const [mobileDevice, setMobileDevice] = useState(false);
|
2026-06-02 21:05:01 +08:00
|
|
|
|
|
|
|
|
const resetWalletConnect = wc.reset;
|
2026-06-02 02:58:01 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!loginModalOpen) return;
|
|
|
|
|
setMobileDevice(isMobileDevice());
|
2026-06-02 21:05:01 +08:00
|
|
|
setSelected(null);
|
|
|
|
|
resetWalletConnect();
|
|
|
|
|
}, [loginModalOpen, resetWalletConnect]);
|
2026-06-02 03:43:13 +08:00
|
|
|
|
2026-06-02 00:57:37 +08:00
|
|
|
if (!loginModalOpen) return null;
|
2026-06-02 00:32:46 +08:00
|
|
|
|
2026-06-02 21:05:01 +08:00
|
|
|
const walletName = (kind: WalletKind) => t(walletNameKey(kind));
|
2026-06-02 21:25:05 +08:00
|
|
|
const walletHint = (kind: WalletKind) => {
|
|
|
|
|
if (kind === "tokenPocket") {
|
|
|
|
|
return mobileDevice
|
|
|
|
|
? t("walletTpMobileDesc")
|
|
|
|
|
: t("walletTokenPocketQrDesc");
|
|
|
|
|
}
|
|
|
|
|
return t("walletRainbowFallbackDesc");
|
2026-06-02 21:05:01 +08:00
|
|
|
};
|
2026-06-02 21:25:05 +08:00
|
|
|
const busy = wc.state !== "idle";
|
2026-06-02 21:05:01 +08:00
|
|
|
|
2026-06-02 21:25:05 +08:00
|
|
|
const close = () => {
|
|
|
|
|
closeLoginModal();
|
2026-06-02 21:05:01 +08:00
|
|
|
setSelected(null);
|
2026-06-02 21:25:05 +08:00
|
|
|
wc.reset();
|
2026-06-02 00:32:46 +08:00
|
|
|
};
|
|
|
|
|
|
2026-06-02 21:25:05 +08:00
|
|
|
const startWalletLogin = (kind: WalletKind) => {
|
|
|
|
|
setSelected(kind);
|
2026-06-02 21:05:01 +08:00
|
|
|
void wc.start(kind);
|
2026-06-02 03:43:13 +08:00
|
|
|
};
|
|
|
|
|
|
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"
|
|
|
|
|
>
|
2026-06-02 21:05:01 +08:00
|
|
|
<div className="max-h-[92dvh] w-full max-w-[460px] 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-3">
|
2026-06-02 21:25:05 +08:00
|
|
|
<div className="min-w-0">
|
|
|
|
|
<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>
|
2026-06-02 00:32:46 +08:00
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={close}
|
2026-06-02 21:05:01 +08:00
|
|
|
aria-label={t("close")}
|
2026-06-02 21:25:05 +08:00
|
|
|
className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full border border-white/10 text-neutral-300 transition hover:border-ark-gold/50 hover:text-ark-gold"
|
2026-06-02 00:32:46 +08:00
|
|
|
>
|
2026-06-02 21:05:01 +08:00
|
|
|
<X size={18} />
|
2026-06-02 00:32:46 +08:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-02 21:25:05 +08:00
|
|
|
<div className="mt-5 grid gap-2">
|
|
|
|
|
{wallets.map((kind) => {
|
|
|
|
|
const active = selected === kind;
|
|
|
|
|
const connecting = active && wc.state === "connecting";
|
|
|
|
|
const signing = active && wc.state === "signing";
|
|
|
|
|
return (
|
2026-06-02 03:43:13 +08:00
|
|
|
<button
|
2026-06-02 21:05:01 +08:00
|
|
|
key={kind}
|
2026-06-02 03:43:13 +08:00
|
|
|
type="button"
|
2026-06-02 21:25:05 +08:00
|
|
|
onClick={() => startWalletLogin(kind)}
|
|
|
|
|
disabled={!wc.available || busy}
|
|
|
|
|
className={`flex items-center gap-3 rounded-2xl border px-4 py-4 text-left transition ${
|
|
|
|
|
active
|
|
|
|
|
? "border-ark-gold/60 bg-ark-gold/10"
|
|
|
|
|
: "border-white/10 bg-[#20202a] hover:border-ark-gold/50 hover:bg-ark-gold/10"
|
|
|
|
|
} disabled:cursor-wait disabled:opacity-70`}
|
2026-06-02 03:43:13 +08:00
|
|
|
>
|
2026-06-02 21:05:01 +08:00
|
|
|
<WalletBrandIcon kind={kind} size={32} />
|
2026-06-02 21:25:05 +08:00
|
|
|
<span className="min-w-0 flex-1">
|
|
|
|
|
<span className="block text-base font-semibold text-neutral-100">
|
|
|
|
|
{walletName(kind)}
|
2026-06-02 21:05:01 +08:00
|
|
|
</span>
|
2026-06-02 21:25:05 +08:00
|
|
|
<span className="mt-1 block text-xs leading-5 text-neutral-400">
|
|
|
|
|
{connecting
|
|
|
|
|
? t("walletConnecting")
|
|
|
|
|
: signing
|
|
|
|
|
? t("walletSigning")
|
|
|
|
|
: walletHint(kind)}
|
2026-06-02 21:05:01 +08:00
|
|
|
</span>
|
2026-06-02 21:25:05 +08:00
|
|
|
</span>
|
|
|
|
|
{connecting || signing ? (
|
|
|
|
|
<LoaderCircle className="h-4 w-4 animate-spin text-ark-gold" />
|
|
|
|
|
) : null}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
2026-06-02 03:43:13 +08:00
|
|
|
|
2026-06-02 21:25:05 +08:00
|
|
|
{!wc.available ? (
|
|
|
|
|
<p className="mt-4 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-neutral-400">
|
|
|
|
|
{t("walletRainbowUnavailable")}
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
2026-06-02 03:43:13 +08:00
|
|
|
|
2026-06-02 21:25:05 +08:00
|
|
|
{selected && wc.qrUri ? (
|
|
|
|
|
<div className="mt-4 grid place-items-center gap-2 rounded-2xl bg-white p-4 text-center">
|
|
|
|
|
<QRCodeSVG value={wc.qrUri} size={180} level="M" />
|
|
|
|
|
<p className="text-xs font-medium text-neutral-700">
|
|
|
|
|
{mobileDevice
|
|
|
|
|
? t("walletTpWaiting")
|
|
|
|
|
: t("walletQrUseAnotherDevice")}
|
|
|
|
|
</p>
|
2026-06-02 00:32:46 +08:00
|
|
|
</div>
|
2026-06-02 21:05:01 +08:00
|
|
|
) : null}
|
2026-06-02 00:32:46 +08:00
|
|
|
|
2026-06-02 21:25:05 +08:00
|
|
|
{wc.error ? (
|
2026-06-02 00:32:46 +08:00
|
|
|
<p className="mt-4 rounded-2xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-200">
|
2026-06-02 21:25:05 +08:00
|
|
|
{wc.error}
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{selected ? (
|
|
|
|
|
<p className="mt-4 text-xs leading-5 text-amber-300/80">
|
|
|
|
|
{t("walletNetworkWarning")}
|
2026-06-02 00:32:46 +08:00
|
|
|
</p>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-06-02 03:43:13 +08:00
|
|
|
|
|
|
|
|
function walletNameKey(kind: WalletKind): string {
|
|
|
|
|
if (kind === "tokenPocket") return "walletTokenPocket";
|
|
|
|
|
if (kind === "metaMask") return "walletMetaMask";
|
|
|
|
|
return "walletImToken";
|
|
|
|
|
}
|