Files
Arkie-Library-Frontend/src/wallet/WalletLoginModal.tsx

209 lines
7.6 KiB
TypeScript

import { QRCodeSVG } from "qrcode.react";
import { LoaderCircle, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useI18n } from "../i18n";
import type { WalletKind } from "./injected";
import { useWallet } from "./WalletProvider";
import { useWalletConnectLogin } from "./useWalletConnectLogin";
import { WalletBrandIcon } from "./WalletBrandIcon";
const wallets: WalletKind[] = ["tokenPocket", "metaMask", "imToken"];
function isMobileDevice(): boolean {
if (typeof navigator === "undefined") return false;
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(
navigator.userAgent || "",
);
}
export function WalletLoginModal() {
const { t } = useI18n();
const { closeLoginModal, loginModalOpen } = useWallet();
const wc = useWalletConnectLogin();
const [selected, setSelected] = useState<WalletKind | null>(null);
const [mobileDevice, setMobileDevice] = useState(false);
const resetWalletConnect = wc.reset;
useEffect(() => {
if (!loginModalOpen) return;
setMobileDevice(isMobileDevice());
setSelected(null);
resetWalletConnect();
}, [loginModalOpen, resetWalletConnect]);
if (!loginModalOpen) return null;
const walletName = (kind: WalletKind) => t(walletNameKey(kind));
const walletHint = (kind: WalletKind) => {
if (kind === "tokenPocket") {
return mobileDevice
? t("walletTpMobileDesc")
: t("walletTokenPocketQrDesc");
}
return t("walletRainbowFallbackDesc");
};
const busy = wc.state !== "idle";
const close = () => {
closeLoginModal();
setSelected(null);
wc.reset();
};
const selectWallet = (kind: WalletKind) => {
setSelected(kind);
wc.reset();
};
const startWalletLogin = (kind: WalletKind, mode: "deeplink" | "qr") => {
setSelected(kind);
void wc.start(kind, mode);
};
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-[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">
<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>
</div>
<button
type="button"
onClick={close}
aria-label={t("close")}
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"
>
<X size={18} />
</button>
</div>
<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 (
<div
key={kind}
className={`rounded-2xl border p-3 transition ${
active
? "border-ark-gold/60 bg-ark-gold/10"
: "border-white/10 bg-[#20202a]"
}`}
>
<button
type="button"
onClick={() =>
mobileDevice
? selectWallet(kind)
: startWalletLogin(kind, "qr")
}
disabled={!wc.available || busy}
className="flex w-full items-center gap-3 text-left disabled:cursor-wait disabled:opacity-70"
>
<WalletBrandIcon kind={kind} size={32} />
<span className="min-w-0 flex-1">
<span className="block text-base font-semibold text-neutral-100">
{walletName(kind)}
</span>
<span className="mt-1 block text-xs leading-5 text-neutral-400">
{connecting
? t("walletConnecting")
: signing
? t("walletSigning")
: walletHint(kind)}
</span>
</span>
{connecting || signing ? (
<LoaderCircle className="h-4 w-4 animate-spin text-ark-gold" />
) : null}
</button>
{mobileDevice && active ? (
<div className="mt-3 grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => startWalletLogin(kind, "deeplink")}
disabled={!wc.available || busy}
className="rounded-full bg-ark-gold px-3 py-2 text-sm font-bold text-black transition hover:bg-ark-gold2 disabled:cursor-wait disabled:opacity-70"
>
{t("walletOpenWalletApp")}
</button>
<button
type="button"
onClick={() => startWalletLogin(kind, "qr")}
disabled={!wc.available || busy}
className="rounded-full border border-ark-gold/50 px-3 py-2 text-sm font-semibold text-ark-gold transition hover:bg-ark-gold/10 disabled:cursor-wait disabled:opacity-70"
>
{t("walletQrLogin")}
</button>
</div>
) : null}
</div>
);
})}
</div>
{!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}
{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>
</div>
) : null}
{wc.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">
{wc.error}
</p>
) : null}
{selected ? (
<div className="mt-4 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-xs leading-5 text-neutral-400">
<p className="font-semibold text-neutral-200">Wallet debug</p>
<p>state: {wc.state}</p>
<p>connected: {wc.isConnected ? "yes" : "no"}</p>
<p className="break-all">address: {wc.address || "-"}</p>
<p>qr: {wc.qrUri ? "received" : "-"}</p>
</div>
) : null}
{selected ? (
<p className="mt-4 text-xs leading-5 text-amber-300/80">
{t("walletNetworkWarning")}
</p>
) : null}
</div>
</div>
);
}
function walletNameKey(kind: WalletKind): string {
if (kind === "tokenPocket") return "walletTokenPocket";
if (kind === "metaMask") return "walletMetaMask";
return "walletImToken";
}