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:
TerryM
2026-06-02 04:00:30 +08:00
parent 7abe4a868c
commit ed04e1fb7e
10 changed files with 204 additions and 83 deletions

View File

@@ -5,11 +5,39 @@
> >
> **核心结论:后端目前几乎已经满足前端全部需求。本次重设计与 bug 修复基本是纯前端工作。后端真正需要新增的只有少量「可选项」,外加几处需要确认的契约。请不要把已完成的功能再派一遍。** > **核心结论:后端目前几乎已经满足前端全部需求。本次重设计与 bug 修复基本是纯前端工作。后端真正需要新增的只有少量「可选项」,外加几处需要确认的契约。请不要把已完成的功能再派一遍。**
日期2026-06-02 日期2026-06-022026-06-02 更新:新增 §0.5 部署阻塞)
相关分支:`terry-wallet-login` 相关分支:`terry-wallet-login`
--- ---
## 0.5 🔴 关键阻塞:线上后端是旧版本,收藏与 TokenPocket 端点未部署
> 源码(`Arkie-Library-Backend`)里这些端点都有;但**当前线上 / dev 代理指向的后端**`https://ark-library.com/apnew`)是**旧构建**,下列路由全部 404。
> **前端已就绪,但收藏与 TokenPocket 扫码登录在生产上无法工作,直到后端把含这些路由的版本部署上线。** 这正是用户实测「收藏用不了、扫码登录用不了」的根因。
curl 实证(经 vite dev 代理打到线上后端2026-06-02
| 端点 | 方法 | 实测状态 | 结论 |
|---|---|---|---|
| `/api/auth/wallet/nonce` | POST | **200** | ✅ 已部署 |
| `/api/auth/wallet/verify` | POST | **400**(入参错误)| ✅ 已部署 |
| `/api/auth/wallet/me` | GET | **401**(需鉴权)| ✅ 已部署 |
| `/api/auth/wallet/tp-login-request` | POST | **404** | ❌ 未部署 |
| `/api/auth/wallet/tp-result` | GET | **404** | ❌ 未部署 |
| `/api/auth/wallet/tp-callback` | POST | **404** | ❌ 未部署 |
| `/api/me/favorites` | GET | **404**(应为 401| ❌ 未部署 |
| `/api/me/favorites/ids` | GET | **404** | ❌ 未部署 |
| `/api/me/favorites/{id}` | POST | **404** | ❌ 未部署 |
**后端动作(必做,按优先级最高):**
1. 部署包含 favorites`internal/handlers/favorites.go`+ TokenPocket`internal/handlers/wallet_tp.go`)路由的后端版本(`cmd/server/main.go` 已注册这些路由)。
2. 为 TokenPocket 登录设置 `PUBLIC_BASE_URL``buildTokenPocketSignURL` 需要它生成回调 URL否则 tp-login-request 会 500
3. 确保 `wallet_tp_login_requests` / `user_favorites` 等表已迁移(`EnsureWalletAuthSchema` 会建表)。
> 复测命令:`curl -s -o /dev/null -w "%{http_code}" -X POST https://ark-library.com/apnew/api/auth/wallet/tp-login-request -d '{}'` 应返回 200且 body 的 `qrUrl` 形如 `tpoutside://pull.activity?param=...`)。
---
## 0. 一句话给后端 ## 0. 一句话给后端
> 钱包认证、TokenPocket 扫码、收藏列表/筛选/分页/可用性,**都已实现且符合前端契约**。 > 钱包认证、TokenPocket 扫码、收藏列表/筛选/分页/可用性,**都已实现且符合前端契约**。

View File

@@ -61,7 +61,7 @@ export function ToastProvider({ children }: { children: ReactNode }) {
<ToastContext.Provider value={value}> <ToastContext.Provider value={value}>
{children} {children}
<div <div
className="pointer-events-none fixed inset-x-0 bottom-[92px] z-[100] flex flex-col items-center gap-2 px-4 md:bottom-6" className="pointer-events-none fixed inset-x-0 bottom-[92px] z-[200] flex flex-col items-center gap-2 px-4 md:bottom-6"
aria-live="polite" aria-live="polite"
aria-atomic="true" aria-atomic="true"
> >

View File

@@ -183,8 +183,15 @@ export const enDict: Dict = {
walletTokenPocket: "TokenPocket", walletTokenPocket: "TokenPocket",
walletMetaMask: "MetaMask", walletMetaMask: "MetaMask",
walletImToken: "imToken", walletImToken: "imToken",
walletTokenPocketLogin: "TokenPocket login",
walletTpMobileDesc:
"Open TokenPocket to sign, then come back here to finish. You stay in this browser instead of the wallet's in-app browser.",
walletTpLoginBtn: "Log in with TokenPocket",
walletTpWaiting: "Waiting for your signature in TokenPocket…",
walletTpReopen: "Reopen TokenPocket",
favoritesFilters: "Filters", favoritesFilters: "Filters",
favoriteSessionExpired: "Your session expired. Please sign in again.", favoriteSessionExpired: "Your session expired. Please sign in again.",
loadFailed: "Could not load your favorites.",
walletChooseDesktop: walletChooseDesktop:
"Choose the wallet you want to use. On desktop, install the matching browser extension.", "Choose the wallet you want to use. On desktop, install the matching browser extension.",
walletChooseMobile: "Choose a wallet app to open this site.", walletChooseMobile: "Choose a wallet app to open this site.",

View File

@@ -183,8 +183,15 @@ export const idDict: Dict = {
walletTokenPocket: "TokenPocket", walletTokenPocket: "TokenPocket",
walletMetaMask: "MetaMask", walletMetaMask: "MetaMask",
walletImToken: "imToken", walletImToken: "imToken",
walletTokenPocketLogin: "Masuk TokenPocket",
walletTpMobileDesc:
"Buka TokenPocket untuk menandatangani, lalu kembali ke sini untuk menyelesaikan. Anda tetap di browser ini, bukan browser dalam aplikasi dompet.",
walletTpLoginBtn: "Masuk dengan TokenPocket",
walletTpWaiting: "Menunggu tanda tangan Anda di TokenPocket…",
walletTpReopen: "Buka kembali TokenPocket",
favoritesFilters: "Filter", favoritesFilters: "Filter",
favoriteSessionExpired: "Sesi Anda telah berakhir. Silakan masuk lagi.", favoriteSessionExpired: "Sesi Anda telah berakhir. Silakan masuk lagi.",
loadFailed: "Gagal memuat favorit Anda.",
walletChooseDesktop: walletChooseDesktop:
"Pilih dompet yang ingin digunakan. Di desktop, pasang ekstensi browser yang sesuai.", "Pilih dompet yang ingin digunakan. Di desktop, pasang ekstensi browser yang sesuai.",
walletChooseMobile: "Pilih aplikasi dompet untuk membuka situs ini.", walletChooseMobile: "Pilih aplikasi dompet untuk membuka situs ini.",

View File

@@ -203,9 +203,16 @@ export const jaDict: Dict = {
walletTokenPocket: "TokenPocket", walletTokenPocket: "TokenPocket",
walletMetaMask: "MetaMask", walletMetaMask: "MetaMask",
walletImToken: "imToken", walletImToken: "imToken",
walletTokenPocketLogin: "TokenPocket ログイン",
walletTpMobileDesc:
"TokenPocket で署名するとこのページに戻ってログインが完了します。ウォレット内ブラウザには移動せず、現在のブラウザのままです。",
walletTpLoginBtn: "TokenPocket でログイン",
walletTpWaiting: "TokenPocket での署名を待っています…",
walletTpReopen: "TokenPocket を再度開く",
favoritesFilters: "フィルター", favoritesFilters: "フィルター",
favoriteSessionExpired: favoriteSessionExpired:
"セッションの有効期限が切れました。もう一度ログインしてください。", "セッションの有効期限が切れました。もう一度ログインしてください。",
loadFailed: "お気に入りを読み込めませんでした。",
walletChooseDesktop: walletChooseDesktop:
"使用するウォレットを選択してください。デスクトップの場合は対応するブラウザ拡張機能をインストールしてください。", "使用するウォレットを選択してください。デスクトップの場合は対応するブラウザ拡張機能をインストールしてください。",
walletChooseMobile: "このサイトを開くウォレットアプリを選択してください。", walletChooseMobile: "このサイトを開くウォレットアプリを選択してください。",

View File

@@ -181,8 +181,15 @@ export const koDict: Dict = {
walletTokenPocket: "TokenPocket", walletTokenPocket: "TokenPocket",
walletMetaMask: "MetaMask", walletMetaMask: "MetaMask",
walletImToken: "imToken", walletImToken: "imToken",
walletTokenPocketLogin: "TokenPocket 로그인",
walletTpMobileDesc:
"TokenPocket에서 서명하면 이 페이지로 돌아와 로그인이 완료됩니다. 지갑 내장 브라우저로 이동하지 않고 현재 브라우저에 머무릅니다.",
walletTpLoginBtn: "TokenPocket으로 로그인",
walletTpWaiting: "TokenPocket에서 서명을 기다리는 중…",
walletTpReopen: "TokenPocket 다시 열기",
favoritesFilters: "필터", favoritesFilters: "필터",
favoriteSessionExpired: "세션이 만료되었습니다. 다시 로그인해 주세요.", favoriteSessionExpired: "세션이 만료되었습니다. 다시 로그인해 주세요.",
loadFailed: "즐겨찾기를 불러오지 못했습니다.",
walletChooseDesktop: walletChooseDesktop:
"사용할 지갑을 선택하세요. 데스크톱에서는 해당 브라우저 확장 프로그램을 설치하세요.", "사용할 지갑을 선택하세요. 데스크톱에서는 해당 브라우저 확장 프로그램을 설치하세요.",
walletChooseMobile: "이 사이트를 열 지갑 앱을 선택하세요.", walletChooseMobile: "이 사이트를 열 지갑 앱을 선택하세요.",

View File

@@ -183,8 +183,15 @@ export const msDict: Dict = {
walletTokenPocket: "TokenPocket", walletTokenPocket: "TokenPocket",
walletMetaMask: "MetaMask", walletMetaMask: "MetaMask",
walletImToken: "imToken", walletImToken: "imToken",
walletTokenPocketLogin: "Log masuk TokenPocket",
walletTpMobileDesc:
"Buka TokenPocket untuk menandatangani, kemudian kembali ke sini untuk selesai. Anda kekal dalam pelayar ini, bukan pelayar dalam aplikasi dompet.",
walletTpLoginBtn: "Log masuk dengan TokenPocket",
walletTpWaiting: "Menunggu tandatangan anda dalam TokenPocket…",
walletTpReopen: "Buka semula TokenPocket",
favoritesFilters: "Penapis", favoritesFilters: "Penapis",
favoriteSessionExpired: "Sesi anda telah tamat. Sila log masuk semula.", favoriteSessionExpired: "Sesi anda telah tamat. Sila log masuk semula.",
loadFailed: "Gagal memuatkan kegemaran anda.",
walletChooseDesktop: walletChooseDesktop:
"Pilih dompet yang ingin anda gunakan. Pada desktop, pasang sambungan pelayar yang sepadan.", "Pilih dompet yang ingin anda gunakan. Pada desktop, pasang sambungan pelayar yang sepadan.",
walletChooseMobile: "Pilih aplikasi dompet untuk membuka laman ini.", walletChooseMobile: "Pilih aplikasi dompet untuk membuka laman ini.",

View File

@@ -181,8 +181,15 @@ export const viDict: Dict = {
walletTokenPocket: "TokenPocket", walletTokenPocket: "TokenPocket",
walletMetaMask: "MetaMask", walletMetaMask: "MetaMask",
walletImToken: "imToken", walletImToken: "imToken",
walletTokenPocketLogin: "Đăng nhập TokenPocket",
walletTpMobileDesc:
"Mở TokenPocket để ký, rồi quay lại đây để hoàn tất. Bạn vẫn ở trong trình duyệt này thay vì trình duyệt trong ví.",
walletTpLoginBtn: "Đăng nhập bằng TokenPocket",
walletTpWaiting: "Đang chờ bạn ký trong TokenPocket…",
walletTpReopen: "Mở lại TokenPocket",
favoritesFilters: "Bộ lọc", favoritesFilters: "Bộ lọc",
favoriteSessionExpired: "Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.", favoriteSessionExpired: "Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.",
loadFailed: "Không thể tải mục yêu thích của bạn.",
walletChooseDesktop: walletChooseDesktop:
"Chọn ví bạn muốn dùng. Trên máy tính, hãy cài tiện ích mở rộng trình duyệt tương ứng.", "Chọn ví bạn muốn dùng. Trên máy tính, hãy cài tiện ích mở rộng trình duyệt tương ứng.",
walletChooseMobile: "Chọn ứng dụng ví để mở trang này.", walletChooseMobile: "Chọn ứng dụng ví để mở trang này.",

View File

@@ -174,8 +174,15 @@ export const zhDict: Dict = {
walletTokenPocket: "TokenPocket", walletTokenPocket: "TokenPocket",
walletMetaMask: "MetaMask", walletMetaMask: "MetaMask",
walletImToken: "imToken", walletImToken: "imToken",
walletTokenPocketLogin: "TokenPocket 登录",
walletTpMobileDesc:
"在 TokenPocket 中签名后会自动返回本页面完成登录,留在当前浏览器,不会跳进钱包内置浏览器。",
walletTpLoginBtn: "使用 TokenPocket 登录",
walletTpWaiting: "等待你在 TokenPocket 中完成签名…",
walletTpReopen: "重新打开 TokenPocket",
favoritesFilters: "筛选", favoritesFilters: "筛选",
favoriteSessionExpired: "登录已过期,请重新登录。", favoriteSessionExpired: "登录已过期,请重新登录。",
loadFailed: "无法加载收藏,请稍后重试。",
walletChooseDesktop: "选择你要使用的钱包。电脑端需要先安装对应浏览器插件。", walletChooseDesktop: "选择你要使用的钱包。电脑端需要先安装对应浏览器插件。",
walletChooseMobile: "选择钱包 App 打开本站。", walletChooseMobile: "选择钱包 App 打开本站。",
walletDesktopHint: walletDesktopHint:

View File

@@ -1,4 +1,5 @@
import { QRCodeSVG } from "qrcode.react"; import { QRCodeSVG } from "qrcode.react";
import { LoaderCircle } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useI18n } from "../i18n"; import { useI18n } from "../i18n";
import { import {
@@ -132,6 +133,10 @@ export function WalletLoginModal() {
const signInjected = async () => { const signInjected = async () => {
setError(""); setError("");
if (!getInjectedEthereum()) {
setError(t("walletNoBrowserWalletDesc"));
return;
}
setState("signing"); setState("signing");
await signInInjected() await signInInjected()
.catch((err) => { .catch((err) => {
@@ -146,13 +151,19 @@ export function WalletLoginModal() {
openWalletDeepLink(kind); 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(""); setError("");
setState("tpLoading"); setState("tpLoading");
try { try {
const req = await createTokenPocketLoginRequest(); const req = await createTokenPocketLoginRequest();
setTpRequest(req); setTpRequest(req);
setState("tpPolling"); setState("tpPolling");
if (mobileDevice) window.location.href = req.qrUrl;
} catch { } catch {
setState("idle"); setState("idle");
setError(t("walletTpQrFailed")); setError(t("walletTpQrFailed"));
@@ -189,68 +200,81 @@ export function WalletLoginModal() {
</div> </div>
<div className="mt-5 grid gap-3"> <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 <button
type="button" type="button"
onClick={() => void signInjected()} onClick={() => void startTokenPocketLogin()}
disabled={busy} disabled={state === "tpLoading"}
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" 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" <WalletBrandIcon kind="tokenPocket" size={20} />
? t("walletSigning") {state === "tpLoading"
? t("loading")
: mobileDevice : mobileDevice
? t("walletUseCurrent") ? t("walletTpLoginBtn")
: t("walletInjected")} : t("walletGenerateQr")}
</button> </button>
{!mobileDevice ? ( ) : mobileDevice ? (
<p className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-xs leading-5 text-neutral-400"> <div className="mt-1 grid gap-2 rounded-2xl border border-dashed border-white/15 bg-white/[0.03] px-4 py-3">
{t("walletDesktopHint")} <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> </p>
) : null}
</>
) : (
<div className="grid gap-2">
<p className="text-sm font-medium text-neutral-300">
{t("walletOpenWalletApp")}
</p>
{appWallets.map((option) => (
<button <button
key={option.kind}
type="button" type="button"
onClick={() => openApp(option.kind)} onClick={() => {
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" 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} /> {t("walletTpReopen")}
<span>{t(option.labelKey)}</span>
</button> </button>
))} </div>
{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"> <div className="mt-1 grid place-items-center gap-2 rounded-2xl bg-white p-4 text-center">
<p>{withWallet("walletOpening", openingWallet)}</p> <QRCodeSVG value={tpRequest.qrUrl} size={180} level="M" />
<p>{t("walletAppNotInstalled")}</p> <p className="text-xs font-medium text-neutral-700">
<div className="flex flex-wrap gap-2 pt-1"> {t("walletQrUseAnotherDevice")}
<a </p>
href={walletDownloadUrl(openingWallet)} </div>
target="_blank" )}
rel="noopener noreferrer" </div>
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>
)}
{/* Other methods: open a wallet app (mobile) and WalletConnect QR. */}
<div className="rounded-2xl border border-dashed border-white/15"> <div className="rounded-2xl border border-dashed border-white/15">
<button <button
type="button" type="button"
@@ -263,36 +287,56 @@ export function WalletLoginModal() {
{showOther ? ( {showOther ? (
<div className="grid gap-4 px-4 pb-4"> <div className="grid gap-4 px-4 pb-4">
{/* TokenPocket QR — stable path for China users (works on desktop too). */} {mobileDevice && !hasInjected ? (
<div className="grid gap-2"> <div className="grid gap-2">
<p className="text-sm font-semibold text-neutral-200"> <p className="text-sm font-semibold text-neutral-200">
{t("walletTokenPocketQr")} {t("walletOpenWalletApp")}
</p> </p>
<p className="text-xs leading-5 text-neutral-400"> {appWallets.map((option) => (
{t("walletTokenPocketQrDesc")} <button
</p> key={option.kind}
<button type="button"
type="button" onClick={() => openApp(option.kind)}
onClick={() => void startTokenPocketQr()} 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"
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" <WalletBrandIcon kind={option.kind} />
> <span>{t(option.labelKey)}</span>
{state === "tpLoading" </button>
? t("loading") ))}
: t("walletGenerateQr")} {openingWallet ? (
</button> <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">
{tpRequest ? ( <p>{withWallet("walletOpening", openingWallet)}</p>
<div className="mt-1 grid place-items-center gap-2 rounded-2xl bg-white p-4 text-center"> <p>{t("walletAppNotInstalled")}</p>
<QRCodeSVG value={tpRequest.qrUrl} size={180} level="M" /> <div className="flex flex-wrap gap-2 pt-1">
<p className="text-xs font-medium text-neutral-700"> <a
{t("walletQrUseAnotherDevice")} href={walletDownloadUrl(openingWallet)}
</p> target="_blank"
</div> rel="noopener noreferrer"
) : null} className="rounded-full border border-ark-gold/40 px-3 py-1.5 font-semibold text-ark-gold transition hover:bg-ark-gold/10"
</div> >
{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. */} {/* MetaMask / imToken QR via WalletConnect — needs a real id. */}
<div className="grid gap-2 border-t border-white/10 pt-4"> <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"> <p className="text-sm font-semibold text-neutral-200">
{t("walletRainbowFallback")} {t("walletRainbowFallback")}
</p> </p>