terry-staging #16

Merged
terry merged 96 commits from terry-staging into main 2026-06-05 16:33:12 +00:00
10 changed files with 204 additions and 83 deletions
Showing only changes of commit ed04e1fb7e - Show all commits

View File

@@ -5,11 +5,39 @@
>
> **核心结论:后端目前几乎已经满足前端全部需求。本次重设计与 bug 修复基本是纯前端工作。后端真正需要新增的只有少量「可选项」,外加几处需要确认的契约。请不要把已完成的功能再派一遍。**
日期2026-06-02
日期2026-06-022026-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 扫码、收藏列表/筛选/分页/可用性,**都已实现且符合前端契约**。

View File

@@ -61,7 +61,7 @@ export function ToastProvider({ children }: { children: ReactNode }) {
<ToastContext.Provider value={value}>
{children}
<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-atomic="true"
>

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -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: "このサイトを開くウォレットアプリを選択してください。",

View File

@@ -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: "이 사이트를 열 지갑 앱을 선택하세요.",

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -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:

View File

@@ -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,8 +200,8 @@ 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()}
@@ -203,15 +214,82 @@ export function WalletLoginModal() {
? t("walletUseCurrent")
: t("walletInjected")}
</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")}
</p>
) : !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 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"
>
<WalletBrandIcon kind="tokenPocket" size={20} />
{state === "tpLoading"
? t("loading")
: mobileDevice
? t("walletTpLoginBtn")
: t("walletGenerateQr")}
</button>
) : 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>
<button
type="button"
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"
>
{t("walletTpReopen")}
</button>
</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"
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">
{mobileDevice && !hasInjected ? (
<div className="grid gap-2">
<p className="text-sm font-medium text-neutral-300">
<p className="text-sm font-semibold text-neutral-200">
{t("walletOpenWalletApp")}
</p>
{appWallets.map((option) => (
@@ -219,7 +297,7 @@ export function WalletLoginModal() {
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"
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>
@@ -249,50 +327,16 @@ export function WalletLoginModal() {
</div>
) : null}
</div>
)}
<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">
{/* 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>
{/* 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>