From ed04e1fb7e8b3301b5b82ec9eade68d6fb017377 Mon Sep 17 00:00:00 2001 From: TerryM Date: Tue, 2 Jun 2026 04:00:30 +0800 Subject: [PATCH] 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) --- docs/backend-requirements-wallet-favorites.md | 30 ++- src/components/Toast.tsx | 2 +- src/locales/en.ts | 7 + src/locales/id.ts | 7 + src/locales/ja.ts | 7 + src/locales/ko.ts | 7 + src/locales/ms.ts | 7 + src/locales/vi.ts | 7 + src/locales/zh-CN.ts | 7 + src/wallet/WalletLoginModal.tsx | 206 +++++++++++------- 10 files changed, 204 insertions(+), 83 deletions(-) diff --git a/docs/backend-requirements-wallet-favorites.md b/docs/backend-requirements-wallet-favorites.md index c65bf13..a8eb317 100644 --- a/docs/backend-requirements-wallet-favorites.md +++ b/docs/backend-requirements-wallet-favorites.md @@ -5,11 +5,39 @@ > > **核心结论:后端目前几乎已经满足前端全部需求。本次重设计与 bug 修复基本是纯前端工作。后端真正需要新增的只有少量「可选项」,外加几处需要确认的契约。请不要把已完成的功能再派一遍。** -日期:2026-06-02 +日期:2026-06-02(2026-06-02 更新:新增 §0.5 部署阻塞) 相关分支:`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. 一句话给后端 > 钱包认证、TokenPocket 扫码、收藏列表/筛选/分页/可用性,**都已实现且符合前端契约**。 diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index d03bfe3..71f79b7 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -61,7 +61,7 @@ export function ToastProvider({ children }: { children: ReactNode }) { {children}
diff --git a/src/locales/en.ts b/src/locales/en.ts index 8592c5b..48d990f 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -183,8 +183,15 @@ export const enDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", 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", favoriteSessionExpired: "Your session expired. Please sign in again.", + loadFailed: "Could not load your favorites.", walletChooseDesktop: "Choose the wallet you want to use. On desktop, install the matching browser extension.", walletChooseMobile: "Choose a wallet app to open this site.", diff --git a/src/locales/id.ts b/src/locales/id.ts index 12d34ce..d49f526 100644 --- a/src/locales/id.ts +++ b/src/locales/id.ts @@ -183,8 +183,15 @@ export const idDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", 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", favoriteSessionExpired: "Sesi Anda telah berakhir. Silakan masuk lagi.", + loadFailed: "Gagal memuat favorit Anda.", walletChooseDesktop: "Pilih dompet yang ingin digunakan. Di desktop, pasang ekstensi browser yang sesuai.", walletChooseMobile: "Pilih aplikasi dompet untuk membuka situs ini.", diff --git a/src/locales/ja.ts b/src/locales/ja.ts index d51c6c1..fba8bdf 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -203,9 +203,16 @@ export const jaDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", walletImToken: "imToken", + walletTokenPocketLogin: "TokenPocket ログイン", + walletTpMobileDesc: + "TokenPocket で署名するとこのページに戻ってログインが完了します。ウォレット内ブラウザには移動せず、現在のブラウザのままです。", + walletTpLoginBtn: "TokenPocket でログイン", + walletTpWaiting: "TokenPocket での署名を待っています…", + walletTpReopen: "TokenPocket を再度開く", favoritesFilters: "フィルター", favoriteSessionExpired: "セッションの有効期限が切れました。もう一度ログインしてください。", + loadFailed: "お気に入りを読み込めませんでした。", walletChooseDesktop: "使用するウォレットを選択してください。デスクトップの場合は対応するブラウザ拡張機能をインストールしてください。", walletChooseMobile: "このサイトを開くウォレットアプリを選択してください。", diff --git a/src/locales/ko.ts b/src/locales/ko.ts index c935aa2..b89fc14 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -181,8 +181,15 @@ export const koDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", walletImToken: "imToken", + walletTokenPocketLogin: "TokenPocket 로그인", + walletTpMobileDesc: + "TokenPocket에서 서명하면 이 페이지로 돌아와 로그인이 완료됩니다. 지갑 내장 브라우저로 이동하지 않고 현재 브라우저에 머무릅니다.", + walletTpLoginBtn: "TokenPocket으로 로그인", + walletTpWaiting: "TokenPocket에서 서명을 기다리는 중…", + walletTpReopen: "TokenPocket 다시 열기", favoritesFilters: "필터", favoriteSessionExpired: "세션이 만료되었습니다. 다시 로그인해 주세요.", + loadFailed: "즐겨찾기를 불러오지 못했습니다.", walletChooseDesktop: "사용할 지갑을 선택하세요. 데스크톱에서는 해당 브라우저 확장 프로그램을 설치하세요.", walletChooseMobile: "이 사이트를 열 지갑 앱을 선택하세요.", diff --git a/src/locales/ms.ts b/src/locales/ms.ts index 04702d4..f993c9c 100644 --- a/src/locales/ms.ts +++ b/src/locales/ms.ts @@ -183,8 +183,15 @@ export const msDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", 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", favoriteSessionExpired: "Sesi anda telah tamat. Sila log masuk semula.", + loadFailed: "Gagal memuatkan kegemaran anda.", walletChooseDesktop: "Pilih dompet yang ingin anda gunakan. Pada desktop, pasang sambungan pelayar yang sepadan.", walletChooseMobile: "Pilih aplikasi dompet untuk membuka laman ini.", diff --git a/src/locales/vi.ts b/src/locales/vi.ts index 792108f..fcb5be7 100644 --- a/src/locales/vi.ts +++ b/src/locales/vi.ts @@ -181,8 +181,15 @@ export const viDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", 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", 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: "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.", diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 69b418b..a638318 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -174,8 +174,15 @@ export const zhDict: Dict = { walletTokenPocket: "TokenPocket", walletMetaMask: "MetaMask", walletImToken: "imToken", + walletTokenPocketLogin: "TokenPocket 登录", + walletTpMobileDesc: + "在 TokenPocket 中签名后会自动返回本页面完成登录,留在当前浏览器,不会跳进钱包内置浏览器。", + walletTpLoginBtn: "使用 TokenPocket 登录", + walletTpWaiting: "等待你在 TokenPocket 中完成签名…", + walletTpReopen: "重新打开 TokenPocket", favoritesFilters: "筛选", favoriteSessionExpired: "登录已过期,请重新登录。", + loadFailed: "无法加载收藏,请稍后重试。", walletChooseDesktop: "选择你要使用的钱包。电脑端需要先安装对应浏览器插件。", walletChooseMobile: "选择钱包 App 打开本站。", walletDesktopHint: diff --git a/src/wallet/WalletLoginModal.tsx b/src/wallet/WalletLoginModal.tsx index 47db105..0ea3266 100644 --- a/src/wallet/WalletLoginModal.tsx +++ b/src/wallet/WalletLoginModal.tsx @@ -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() {
- {!mobileDevice || hasInjected ? ( - <> + {/* Injected wallet: browser extension (desktop) or in-wallet browser. */} + {hasInjected ? ( + + ) : !mobileDevice ? ( + + ) : null} + + {/* TokenPocket login — universal path that returns to this browser. */} +
+

+ {t("walletTokenPocketLogin")} +

+

+ {mobileDevice + ? t("walletTpMobileDesc") + : t("walletTokenPocketQrDesc")} +

+ {!tpRequest ? ( - {!mobileDevice ? ( -

- {t("walletDesktopHint")} + ) : mobileDevice ? ( +

+

+ + {t("walletTpWaiting")}

- ) : null} - - ) : ( -
-

- {t("walletOpenWalletApp")} -

- {appWallets.map((option) => ( - ))} - {openingWallet ? ( -
-

{withWallet("walletOpening", openingWallet)}

-

{t("walletAppNotInstalled")}

-
- - {withWallet("walletDownloadApp", openingWallet)} - - -
-
- ) : null} -
- )} +
+ ) : ( +
+ +

+ {t("walletQrUseAnotherDevice")} +

+
+ )} +
+ {/* Other methods: open a wallet app (mobile) and WalletConnect QR. */}
- {tpRequest ? ( -
- -

- {t("walletQrUseAnotherDevice")} -

-
- ) : null} -
+ {mobileDevice && !hasInjected ? ( +
+

+ {t("walletOpenWalletApp")} +

+ {appWallets.map((option) => ( + + ))} + {openingWallet ? ( +
+

{withWallet("walletOpening", openingWallet)}

+

{t("walletAppNotInstalled")}

+
+ + {withWallet("walletDownloadApp", openingWallet)} + + +
+
+ ) : null} +
+ ) : null} - {/* MetaMask / imToken QR via WalletConnect — gated on a real project id. */} -
+ {/* MetaMask / imToken QR via WalletConnect — needs a real id. */} +

{t("walletRainbowFallback")}