2026-06-02 03:43:13 +08:00
|
|
|
|
import { QRCodeSVG } from "qrcode.react";
|
2026-06-02 04:00:30 +08:00
|
|
|
|
import { LoaderCircle } 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 03:43:13 +08:00
|
|
|
|
import {
|
|
|
|
|
|
createTokenPocketLoginRequest,
|
|
|
|
|
|
fetchTokenPocketLoginResult,
|
|
|
|
|
|
verifyWalletSignature,
|
|
|
|
|
|
type TokenPocketLoginRequest,
|
|
|
|
|
|
} from "./api";
|
|
|
|
|
|
import { openWalletDeepLink, walletDownloadUrl } from "./deepLinks";
|
|
|
|
|
|
import { getInjectedEthereum, 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 03:43:13 +08:00
|
|
|
|
const pollIntervalMs = 1800;
|
2026-06-02 00:32:46 +08:00
|
|
|
|
|
2026-06-02 03:43:13 +08:00
|
|
|
|
type ModalState = "idle" | "signing" | "tpLoading" | "tpPolling";
|
2026-06-02 02:58:01 +08:00
|
|
|
|
|
2026-06-02 03:43:13 +08:00
|
|
|
|
const appWallets: { kind: WalletKind; labelKey: string }[] = [
|
|
|
|
|
|
{ kind: "tokenPocket", labelKey: "walletOpenTokenPocket" },
|
|
|
|
|
|
{ kind: "metaMask", labelKey: "walletOpenMetaMask" },
|
|
|
|
|
|
{ kind: "imToken", labelKey: "walletOpenImToken" },
|
2026-06-02 02:58:01 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function isMobileDevice(): boolean {
|
|
|
|
|
|
if (typeof navigator === "undefined") return false;
|
|
|
|
|
|
const ua = navigator.userAgent || "";
|
2026-06-02 03:43:13 +08:00
|
|
|
|
if (
|
2026-06-02 02:58:01 +08:00
|
|
|
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(
|
|
|
|
|
|
ua,
|
2026-06-02 03:43:13 +08:00
|
|
|
|
)
|
|
|
|
|
|
) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// iPadOS 13+ reports a desktop "Macintosh" UA. A genuine touch-primary iPad
|
|
|
|
|
|
// exposes a coarse pointer; a Mac (even with a touch peripheral) keeps a fine
|
|
|
|
|
|
// pointer, so it stays on the desktop flow instead of the wallet-app jump.
|
|
|
|
|
|
const coarsePointer =
|
|
|
|
|
|
typeof window !== "undefined" &&
|
|
|
|
|
|
typeof window.matchMedia === "function" &&
|
|
|
|
|
|
window.matchMedia("(pointer: coarse)").matches;
|
|
|
|
|
|
return /Macintosh/i.test(ua) && navigator.maxTouchPoints > 1 && coarsePointer;
|
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 03:43:13 +08:00
|
|
|
|
const { closeLoginModal, completeLogin, loginModalOpen, signInInjected } =
|
|
|
|
|
|
useWallet();
|
|
|
|
|
|
const wc = useWalletConnectLogin();
|
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);
|
2026-06-02 03:43:13 +08:00
|
|
|
|
const [hasInjected, setHasInjected] = useState(false);
|
|
|
|
|
|
const [showOther, setShowOther] = useState(false);
|
|
|
|
|
|
const [openingWallet, setOpeningWallet] = useState<WalletKind | null>(null);
|
|
|
|
|
|
const [tpRequest, setTpRequest] = useState<TokenPocketLoginRequest | null>(
|
|
|
|
|
|
null,
|
|
|
|
|
|
);
|
2026-06-02 02:58:01 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!loginModalOpen) return;
|
|
|
|
|
|
setMobileDevice(isMobileDevice());
|
2026-06-02 03:43:13 +08:00
|
|
|
|
setHasInjected(Boolean(getInjectedEthereum()));
|
|
|
|
|
|
setShowOther(false);
|
|
|
|
|
|
setOpeningWallet(null);
|
|
|
|
|
|
setTpRequest(null);
|
|
|
|
|
|
setState("idle");
|
2026-06-02 02:58:01 +08:00
|
|
|
|
setError("");
|
2026-06-02 03:43:13 +08:00
|
|
|
|
wc.reset();
|
|
|
|
|
|
}, [loginModalOpen, wc]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!loginModalOpen || !tpRequest || 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);
|
|
|
|
|
|
setState("idle");
|
|
|
|
|
|
setTpRequest(null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (result.status === "expired" || result.status === "failed") {
|
|
|
|
|
|
setState("idle");
|
2026-06-02 04:30:27 +08:00
|
|
|
|
setTpRequest(null);
|
2026-06-02 03:43:13 +08:00
|
|
|
|
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, t, tpRequest]);
|
|
|
|
|
|
|
|
|
|
|
|
const busy = state === "signing" || state === "tpLoading";
|
2026-06-02 00:32:46 +08:00
|
|
|
|
|
|
|
|
|
|
const close = () => {
|
2026-06-02 03:43:13 +08:00
|
|
|
|
if (busy) 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 03:43:13 +08:00
|
|
|
|
const withWallet = (key: string, kind: WalletKind) =>
|
|
|
|
|
|
t(key).replace("{wallet}", t(walletNameKey(kind)));
|
2026-06-02 00:32:46 +08:00
|
|
|
|
|
2026-06-02 03:43:13 +08:00
|
|
|
|
const signInjected = async () => {
|
2026-06-02 00:32:46 +08:00
|
|
|
|
setError("");
|
2026-06-02 04:00:30 +08:00
|
|
|
|
if (!getInjectedEthereum()) {
|
|
|
|
|
|
setError(t("walletNoBrowserWalletDesc"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-02 02:58:01 +08:00
|
|
|
|
setState("signing");
|
2026-06-02 03:43:13 +08:00
|
|
|
|
await signInInjected()
|
2026-06-02 02:58:01 +08:00
|
|
|
|
.catch((err) => {
|
|
|
|
|
|
setError(err instanceof Error ? err.message : t("walletLoginFailed"));
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => setState("idle"));
|
2026-06-02 00:32:46 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-02 03:43:13 +08:00
|
|
|
|
const openApp = (kind: WalletKind) => {
|
|
|
|
|
|
setError("");
|
|
|
|
|
|
setOpeningWallet(kind);
|
|
|
|
|
|
openWalletDeepLink(kind);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-02 04:00:30 +08:00
|
|
|
|
// TokenPocket login. The backend returns a `tpoutside://pull.activity` deep
|
|
|
|
|
|
// link (a one-off SIGN request, not a dApp-browser link). On mobile we open
|
|
|
|
|
|
// it directly so TokenPocket only asks for a signature and then returns to
|
|
|
|
|
|
// THIS browser — the poll below finishes login here, no in-app browser. On
|
|
|
|
|
|
// desktop we render it as a QR to scan from a phone.
|
|
|
|
|
|
const startTokenPocketLogin = async () => {
|
2026-06-02 03:43:13 +08:00
|
|
|
|
setError("");
|
|
|
|
|
|
setState("tpLoading");
|
|
|
|
|
|
try {
|
|
|
|
|
|
const req = await createTokenPocketLoginRequest();
|
|
|
|
|
|
setTpRequest(req);
|
|
|
|
|
|
setState("tpPolling");
|
2026-06-02 04:00:30 +08:00
|
|
|
|
if (mobileDevice) window.location.href = req.qrUrl;
|
2026-06-02 03:43:13 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
setState("idle");
|
|
|
|
|
|
setError(t("walletTpQrFailed"));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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 04:00:30 +08:00
|
|
|
|
{/* Injected wallet: browser extension (desktop) or in-wallet browser. */}
|
|
|
|
|
|
{hasInjected ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => void signInjected()}
|
|
|
|
|
|
disabled={busy}
|
|
|
|
|
|
className="flex items-center justify-center gap-2 rounded-2xl bg-ark-gold px-4 py-4 text-base font-bold text-black transition hover:bg-ark-gold2 disabled:cursor-wait disabled:opacity-70"
|
|
|
|
|
|
>
|
|
|
|
|
|
{state === "signing"
|
|
|
|
|
|
? t("walletSigning")
|
|
|
|
|
|
: mobileDevice
|
|
|
|
|
|
? t("walletUseCurrent")
|
|
|
|
|
|
: t("walletInjected")}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : !mobileDevice ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => void signInjected()}
|
|
|
|
|
|
className="flex items-center justify-center gap-2 rounded-2xl border border-ark-gold/50 bg-ark-gold/5 px-4 py-3 text-sm font-semibold text-ark-gold transition hover:bg-ark-gold/10"
|
|
|
|
|
|
>
|
|
|
|
|
|
{t("walletInjected")}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
{/* TokenPocket login — universal path that returns to this browser. */}
|
|
|
|
|
|
<div className="grid gap-2 rounded-2xl border border-white/10 bg-white/[0.02] p-4">
|
|
|
|
|
|
<p className="text-sm font-semibold text-neutral-100">
|
|
|
|
|
|
{t("walletTokenPocketLogin")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-xs leading-5 text-neutral-400">
|
|
|
|
|
|
{mobileDevice
|
|
|
|
|
|
? t("walletTpMobileDesc")
|
|
|
|
|
|
: t("walletTokenPocketQrDesc")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{!tpRequest ? (
|
2026-06-02 03:43:13 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-06-02 04:00:30 +08:00
|
|
|
|
onClick={() => void startTokenPocketLogin()}
|
|
|
|
|
|
disabled={state === "tpLoading"}
|
|
|
|
|
|
className="mt-1 inline-flex items-center justify-center gap-2 justify-self-start 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"
|
2026-06-02 03:43:13 +08:00
|
|
|
|
>
|
2026-06-02 04:00:30 +08:00
|
|
|
|
<WalletBrandIcon kind="tokenPocket" size={20} />
|
|
|
|
|
|
{state === "tpLoading"
|
|
|
|
|
|
? t("loading")
|
2026-06-02 03:43:13 +08:00
|
|
|
|
: mobileDevice
|
2026-06-02 04:00:30 +08:00
|
|
|
|
? t("walletTpLoginBtn")
|
|
|
|
|
|
: t("walletGenerateQr")}
|
2026-06-02 03:43:13 +08:00
|
|
|
|
</button>
|
2026-06-02 04:00:30 +08:00
|
|
|
|
) : mobileDevice ? (
|
|
|
|
|
|
<div className="mt-1 grid gap-2 rounded-2xl border border-dashed border-white/15 bg-white/[0.03] px-4 py-3">
|
|
|
|
|
|
<p className="flex items-center gap-2 text-xs leading-5 text-neutral-300">
|
|
|
|
|
|
<LoaderCircle className="h-4 w-4 animate-spin text-ark-gold" />
|
|
|
|
|
|
{t("walletTpWaiting")}
|
2026-06-02 03:43:13 +08:00
|
|
|
|
</p>
|
2026-06-02 02:58:01 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-06-02 04:00:30 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
window.location.href = tpRequest.qrUrl;
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="justify-self-start rounded-full border border-ark-gold/40 px-3 py-1.5 text-xs font-semibold text-ark-gold transition hover:bg-ark-gold/10"
|
2026-06-02 02:58:01 +08:00
|
|
|
|
>
|
2026-06-02 04:00:30 +08:00
|
|
|
|
{t("walletTpReopen")}
|
2026-06-02 02:58:01 +08:00
|
|
|
|
</button>
|
2026-06-02 04:00:30 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="mt-1 grid place-items-center gap-2 rounded-2xl bg-white p-4 text-center">
|
|
|
|
|
|
<QRCodeSVG value={tpRequest.qrUrl} size={180} level="M" />
|
|
|
|
|
|
<p className="text-xs font-medium text-neutral-700">
|
|
|
|
|
|
{t("walletQrUseAnotherDevice")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-06-02 03:43:13 +08:00
|
|
|
|
|
2026-06-02 04:00:30 +08:00
|
|
|
|
{/* Other methods: open a wallet app (mobile) and WalletConnect QR. */}
|
2026-06-02 03:43:13 +08:00
|
|
|
|
<div className="rounded-2xl border border-dashed border-white/15">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowOther((value) => !value)}
|
|
|
|
|
|
className="flex w-full items-center justify-between px-4 py-3 text-left text-sm font-semibold text-ark-gold transition hover:text-ark-gold2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>{t("walletOtherMethods")}</span>
|
|
|
|
|
|
<span>{showOther ? "−" : "+"}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
{showOther ? (
|
|
|
|
|
|
<div className="grid gap-4 px-4 pb-4">
|
2026-06-02 04:00:30 +08:00
|
|
|
|
{mobileDevice && !hasInjected ? (
|
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
|
<p className="text-sm font-semibold text-neutral-200">
|
|
|
|
|
|
{t("walletOpenWalletApp")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{appWallets.map((option) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={option.kind}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => openApp(option.kind)}
|
|
|
|
|
|
className="flex items-center gap-3 rounded-2xl border border-white/10 bg-[#20202a] px-4 py-3 text-left text-sm font-semibold text-neutral-100 transition hover:border-ark-gold/50 hover:bg-ark-gold/10"
|
|
|
|
|
|
>
|
|
|
|
|
|
<WalletBrandIcon kind={option.kind} />
|
|
|
|
|
|
<span>{t(option.labelKey)}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{openingWallet ? (
|
|
|
|
|
|
<div className="grid gap-2 rounded-2xl border border-dashed border-white/15 bg-white/[0.03] px-4 py-3 text-xs leading-5 text-neutral-400">
|
|
|
|
|
|
<p>{withWallet("walletOpening", openingWallet)}</p>
|
|
|
|
|
|
<p>{t("walletAppNotInstalled")}</p>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2 pt-1">
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={walletDownloadUrl(openingWallet)}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
|
className="rounded-full border border-ark-gold/40 px-3 py-1.5 font-semibold text-ark-gold transition hover:bg-ark-gold/10"
|
|
|
|
|
|
>
|
|
|
|
|
|
{withWallet("walletDownloadApp", openingWallet)}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => openApp(openingWallet)}
|
|
|
|
|
|
className="rounded-full border border-white/15 px-3 py-1.5 font-semibold text-neutral-200 transition hover:border-ark-gold/40 hover:text-ark-gold"
|
|
|
|
|
|
>
|
|
|
|
|
|
{t("walletRetry")}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
2026-06-02 03:43:13 +08:00
|
|
|
|
|
2026-06-02 04:00:30 +08:00
|
|
|
|
{/* MetaMask / imToken QR via WalletConnect — needs a real id. */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={
|
|
|
|
|
|
mobileDevice && !hasInjected
|
|
|
|
|
|
? "grid gap-2 border-t border-white/10 pt-4"
|
|
|
|
|
|
: "grid gap-2"
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-06-02 03:43:13 +08:00
|
|
|
|
<p className="text-sm font-semibold text-neutral-200">
|
|
|
|
|
|
{t("walletRainbowFallback")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-xs leading-5 text-neutral-400">
|
|
|
|
|
|
{t("walletRainbowFallbackDesc")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{wc.available ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => wc.start()}
|
|
|
|
|
|
disabled={wc.state !== "idle"}
|
|
|
|
|
|
className="justify-self-start 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"
|
|
|
|
|
|
>
|
|
|
|
|
|
{wc.state === "connecting"
|
|
|
|
|
|
? t("walletConnecting")
|
|
|
|
|
|
: wc.state === "signing"
|
|
|
|
|
|
? t("walletSigning")
|
|
|
|
|
|
: t("walletOpenRainbow")}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<p className="text-xs leading-5 text-amber-300/80">
|
|
|
|
|
|
{t("walletNetworkWarning")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p className="rounded-xl border border-white/10 bg-white/[0.03] px-3 py-2 text-xs leading-5 text-neutral-500">
|
|
|
|
|
|
{t("walletRainbowUnavailable")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
2026-06-02 00:32:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-02 03:43:13 +08:00
|
|
|
|
{error || 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 03:43:13 +08:00
|
|
|
|
{error || wc.error}
|
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";
|
|
|
|
|
|
}
|