fix: TokenPocket mobile deep-link login, desktop empty-state, toast above modal
- Mobile TokenPocket now opens the tpoutside:// sign deep link and returns to the original browser to finish login (no wallet in-app browser); desktop keeps the QR. Fixes mobile login + logout being trapped in TP's browser. - Desktop without an injected wallet shows a clear message instead of a dead button; TokenPocket login card is always available as a working path. - Raise toast z-index above the login modal so feedback is visible. - Add native TokenPocket-login strings across 7 locales. - Document that the live backend lacks favorites + TokenPocket routes (404), the real blocker for those features in production. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useI18n } from "../i18n";
|
||||
import {
|
||||
@@ -132,6 +133,10 @@ export function WalletLoginModal() {
|
||||
|
||||
const signInjected = async () => {
|
||||
setError("");
|
||||
if (!getInjectedEthereum()) {
|
||||
setError(t("walletNoBrowserWalletDesc"));
|
||||
return;
|
||||
}
|
||||
setState("signing");
|
||||
await signInInjected()
|
||||
.catch((err) => {
|
||||
@@ -146,13 +151,19 @@ export function WalletLoginModal() {
|
||||
openWalletDeepLink(kind);
|
||||
};
|
||||
|
||||
const startTokenPocketQr = async () => {
|
||||
// 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 () => {
|
||||
setError("");
|
||||
setState("tpLoading");
|
||||
try {
|
||||
const req = await createTokenPocketLoginRequest();
|
||||
setTpRequest(req);
|
||||
setState("tpPolling");
|
||||
if (mobileDevice) window.location.href = req.qrUrl;
|
||||
} catch {
|
||||
setState("idle");
|
||||
setError(t("walletTpQrFailed"));
|
||||
@@ -189,68 +200,81 @@ export function WalletLoginModal() {
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-3">
|
||||
{!mobileDevice || hasInjected ? (
|
||||
<>
|
||||
{/* 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 ? (
|
||||
<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"
|
||||
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"
|
||||
>
|
||||
{state === "signing"
|
||||
? t("walletSigning")
|
||||
<WalletBrandIcon kind="tokenPocket" size={20} />
|
||||
{state === "tpLoading"
|
||||
? t("loading")
|
||||
: mobileDevice
|
||||
? t("walletUseCurrent")
|
||||
: t("walletInjected")}
|
||||
? t("walletTpLoginBtn")
|
||||
: t("walletGenerateQr")}
|
||||
</button>
|
||||
{!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")}
|
||||
) : 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")}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm font-medium text-neutral-300">
|
||||
{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-4 text-left text-base font-semibold text-neutral-100 transition hover:border-ark-gold/50 hover:bg-ark-gold/10"
|
||||
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"
|
||||
>
|
||||
<WalletBrandIcon kind={option.kind} />
|
||||
<span>{t(option.labelKey)}</span>
|
||||
{t("walletTpReopen")}
|
||||
</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>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* Other methods: open a wallet app (mobile) and WalletConnect QR. */}
|
||||
<div className="rounded-2xl border border-dashed border-white/15">
|
||||
<button
|
||||
type="button"
|
||||
@@ -263,36 +287,56 @@ export function WalletLoginModal() {
|
||||
|
||||
{showOther ? (
|
||||
<div className="grid gap-4 px-4 pb-4">
|
||||
{/* TokenPocket QR — stable path for China users (works on desktop too). */}
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm font-semibold text-neutral-200">
|
||||
{t("walletTokenPocketQr")}
|
||||
</p>
|
||||
<p className="text-xs leading-5 text-neutral-400">
|
||||
{t("walletTokenPocketQrDesc")}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void startTokenPocketQr()}
|
||||
disabled={state === "tpLoading"}
|
||||
className="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"
|
||||
>
|
||||
{state === "tpLoading"
|
||||
? t("loading")
|
||||
: t("walletGenerateQr")}
|
||||
</button>
|
||||
{tpRequest ? (
|
||||
<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>
|
||||
) : null}
|
||||
</div>
|
||||
{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}
|
||||
|
||||
{/* MetaMask / imToken QR via WalletConnect — gated on a real project id. */}
|
||||
<div className="grid gap-2 border-t border-white/10 pt-4">
|
||||
{/* 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"
|
||||
}
|
||||
>
|
||||
<p className="text-sm font-semibold text-neutral-200">
|
||||
{t("walletRainbowFallback")}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user