diff --git a/.env.example b/.env.example index 3b42c74..c8439ac 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,7 @@ VITE_ADMIN_UI_PREFIX= # Use mock Post data (Telegram-style resource stream) only when explicitly enabled. # Default production/staging behavior should hit the real /api/posts API. VITE_USE_MOCK_POSTS=false + +# Reown/WalletConnect project ID used by RainbowKit fallback QR login +# for MetaMask/imToken. TokenPocket QR does not depend on this. +VITE_WALLETCONNECT_PROJECT_ID= diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml new file mode 100644 index 0000000..9362900 --- /dev/null +++ b/.gitea/workflows/deploy-staging.yml @@ -0,0 +1,125 @@ +name: Deploy Staging (terry-wallet-login) + +on: + push: + branches: + - terry-wallet-login + +jobs: + deploy: + runs-on: self-hosted + + steps: + - name: Free disk space + run: | + set +e + echo "=== Disk before cleanup ===" + df -h + du -sh "$HOME/.cache/act" "$HOME/.npm" "$HOME/actions-runner/_work" 2>/dev/null + + # DO NOT touch ~/.cache/act for the current job — only sweep dirs older than 60 min. + if [ -d "$HOME/.cache/act" ]; then + find "$HOME/.cache/act" -mindepth 1 -maxdepth 1 -type d -mmin +60 -exec rm -rf {} + 2>/dev/null + fi + + # Wipe npm and setup-node caches (cache: npm will repopulate from registry). + rm -rf "$HOME/.npm/_cacache" "$HOME/.npm/_logs" 2>/dev/null + rm -rf "$HOME/.cache/setup-node" 2>/dev/null + + # Old actions-runner workspaces (>6h) + if [ -d "$HOME/actions-runner/_work" ]; then + find "$HOME/actions-runner/_work" -mindepth 1 -maxdepth 2 -mmin +360 -exec rm -rf {} + 2>/dev/null + fi + + # Docker aggressive prune (all dangling + unused, including volumes) + if command -v docker >/dev/null 2>&1; then + docker system prune -af --volumes 2>/dev/null + fi + + # apt/yum cache + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get clean 2>/dev/null + fi + if command -v yum >/dev/null 2>&1; then + sudo yum clean all 2>/dev/null + fi + + # /tmp leftovers (>30 min) other than active runner state + find /tmp -mindepth 1 -maxdepth 1 -mmin +30 \ + -not -name 'runner*' -not -name 'act*' -not -name 'tmp.*' \ + -exec rm -rf {} + 2>/dev/null + + # journald logs vacuum to 100M + if command -v journalctl >/dev/null 2>&1; then + sudo journalctl --vacuum-size=100M 2>/dev/null + fi + + echo "=== Disk after cleanup ===" + df -h + exit 0 + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Type check + run: npx tsc --noEmit + + - name: Format check + run: npm run format:check + + - name: Test + run: npm test + + - name: Build + run: npm run build + env: + VITE_API_URL: "" + VITE_API_PREFIX: "/apnew" + VITE_DISABLE_ADMIN: "true" + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.STAGING_SSH_KEY }}" > ~/.ssh/staging_key + chmod 600 ~/.ssh/staging_key + ssh-keyscan -H ${{ secrets.STAGING_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + + - name: Deploy to staging server + run: | + set -euo pipefail + HOST="${{ secrets.STAGING_HOST }}" + USER="${{ secrets.STAGING_USER }}" + echo ">>> 部署到 staging $USER@$HOST" + rsync -avz --delete \ + -e "ssh -i ~/.ssh/staging_key -o StrictHostKeyChecking=no" \ + dist/ \ + "${USER}@${HOST}:/var/www/ark-library-staging/" + echo ">>> staging 部署完成" + + - name: Verify staging server matches local build + run: | + set -euo pipefail + LOCAL=$(sha256sum dist/index.html | awk '{print $1}') + REMOTE=$(ssh -i ~/.ssh/staging_key -o StrictHostKeyChecking=no \ + ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }} \ + "sha256sum /var/www/ark-library-staging/index.html | awk '{print \$1}'") + echo "local: $LOCAL" + echo "staging: $REMOTE" + if [ "$REMOTE" != "$LOCAL" ]; then + echo "ERROR: staging 不是本次构建的版本" + exit 1 + fi + echo "✓ staging 已经更新到本次构建的版本。" + + - name: Cleanup SSH key + if: always() + run: rm -f ~/.ssh/staging_key diff --git a/.unipi/docs/debug/2026-06-02-metamask-wallet-login-debug.md b/.unipi/docs/debug/2026-06-02-metamask-wallet-login-debug.md new file mode 100644 index 0000000..a9ef5cd --- /dev/null +++ b/.unipi/docs/debug/2026-06-02-metamask-wallet-login-debug.md @@ -0,0 +1,103 @@ +--- +title: "MetaMask Wallet Login Does Not Surface Address — Debug Report" +type: debug +date: 2026-06-02 +severity: high +status: root-caused +--- + +# MetaMask Wallet Login Does Not Surface Address — Debug Report + +## Summary +MetaMask QR login and mobile deeplink login can be approved in MetaMask, but the ARK frontend does not write the approved wallet address into the local wallet session; the mobile QR waiting text is also incorrectly TokenPocket-specific for all wallets. + +## Expected Behavior +- Selecting MetaMask and approving the connection should result in the page showing the connected wallet address. +- Mobile MetaMask deeplink login should return/reconnect to the page and complete local no-signature login with `local-wallet:
`. +- QR login copy should be generic or absent; it should not say “waiting in TokenPocket” when the selected wallet is MetaMask or imToken. + +## Actual Behavior +- Terry can approve MetaMask QR/deeplink login, but the web page does not show the authorized address. +- The QR panel uses TokenPocket-specific copy for any wallet on mobile, e.g. imToken QR login still shows a TokenPocket waiting message. + +## Reproduction Steps +1. Open the public frontend and open the wallet login modal. +2. Select MetaMask. +3. Use either: + - QR login: scan the QR using MetaMask and approve, or + - Mobile app login: tap “Open wallet app”, approve in MetaMask, then return to the web page. +4. Observe that the page does not show the wallet address. +5. Select imToken QR login on mobile and observe that the QR panel displays TokenPocket-specific waiting text. + +## Environment +- Project: Arkie Library Frontend (`ark-database-web`) +- Branch context: `terry-wallet-login` +- Stack: React 18, Vite, TypeScript, RainbowKit, Wagmi, WalletConnect/Reown +- Wallets involved: MetaMask Mobile, TokenPocket, imToken +- Backend auth endpoints intentionally not required for this flow; login is local no-signature wallet session. + +## Root Cause Analysis + +### Failure Chain +1. `WalletLoginModal` calls `wc.start(kind, mode)` for all wallet app/QR flows. +2. `useWalletConnectLogin.start()` currently chooses the first generic WalletConnect connector for non-injected flows: + - `connectors.find((item) => item.type === "walletConnect") ?? connectors.find((item) => item.id === "walletConnect")` +3. On MetaMask mobile, RainbowKit’s own MetaMask wallet definition intentionally uses Wagmi’s `metaMask()` / MetaMask SDK connector, not the generic WalletConnect connector. +4. The custom hook bypasses that MetaMask-specific connector on mobile, so MetaMask SDK deeplink/reconnect handling is not used. +5. The hook only calls `completeLogin(localWalletToken(address), address)` inside the awaited `connectAsync(...)` result path. +6. If MetaMask approval completes while the browser is backgrounded, after a page reload, or through a restored Wagmi connection, there is no `useAccount`/reconnect bridge that converts the Wagmi connected address into the app’s local wallet session. +7. `RainbowWalletProvider` calls `useReconnect()`, but this only restores Wagmi connection state; it does not update `WalletProvider` unless `useWalletConnectLogin` observes the restored account and calls `completeLogin`. +8. Therefore MetaMask may be approved/connected at the wallet/Wagmi layer but the ARK UI still has no `local-wallet:
` token and shows logged-out/no address. + +### Root Cause +The MetaMask flow is treated as a generic WalletConnect flow, but RainbowKit/Wagmi have MetaMask-specific mobile behavior. Additionally, local ARK wallet login is tied only to the synchronous `connectAsync` return path instead of being derived from Wagmi account state/reconnect events. This misses MetaMask connections that resolve via mobile app backgrounding, deep link return, QR approval, or page reload. + +### Evidence +- File: `src/wallet/useWalletConnectLogin.ts` — connector selection prefers `item.type === "walletConnect"` for all wallets, so mobile MetaMask does not use the RainbowKit/Wagmi MetaMask connector. +- File: `src/wallet/useWalletConnectLogin.ts` — `completeLogin(localWalletToken(...), ...)` only runs after `await connectAsync(...)`; there is no `useAccount` effect to complete local login when Wagmi is already/reconnected. +- File: `src/wallet/RainbowWalletProvider.tsx` — `WalletReconnectOnMount` calls `useReconnect()`, but no downstream code maps the restored Wagmi account into `WalletProvider`. +- File: `node_modules/@rainbow-me/rainbowkit/dist/wallets/walletConnectors/chunk-BQHQU37S.js` — RainbowKit MetaMask wallet uses `metaMask()` connector on mobile and comments that “MetaMask mobile deep linking [is] handled by wagmi”. The custom hook bypasses this by selecting a generic WalletConnect connector. +- File: `src/wallet/WalletLoginModal.tsx` — QR text uses `mobileDevice ? t("walletTpWaiting") : t("walletQrUseAnotherDevice")` for every selected wallet, causing TokenPocket-specific copy for MetaMask/imToken. +- File: `src/locales/en.ts` and `src/locales/zh-CN.ts` — `walletQrUseAnotherDevice` also explicitly mentions TokenPocket, so even desktop/generic QR copy is wallet-specific. + +## Affected Files +- `src/wallet/useWalletConnectLogin.ts` — connector choice, deeplink generation, QR URI generation, local-login completion. +- `src/wallet/WalletLoginModal.tsx` — misleading QR panel copy. +- `src/wallet/RainbowWalletProvider.tsx` — currently reconnects Wagmi but does not by itself complete local login. +- `src/locales/*.ts` — QR copy currently contains TokenPocket-specific text. + +## Suggested Fix +Use the wallet-specific connector when a wallet is selected, especially MetaMask, and add a Wagmi account/reconnect bridge that completes the local wallet session whenever Wagmi has an address. Remove or generalize TokenPocket-specific QR waiting copy. + +### Fix Strategy +1. In `useWalletConnectLogin.ts`, prefer a connector matching `preferredWallet` before falling back to generic WalletConnect: + - MetaMask: `id === "metaMask"` or `type === "metaMask"` + - imToken/TokenPocket: matching wallet id when present, otherwise generic WalletConnect +2. Add `useAccount()` inside `useWalletConnectLogin` and a `useEffect` that calls `completeLogin(localWalletToken(address), address)` when Wagmi reports `isConnected && address`. +3. For MetaMask QR, transform the displayed QR value with the same wallet-specific URI RainbowKit uses: `https://metamask.app.link/wc?uri=` instead of always rendering raw `wc:`. +4. Remove the QR panel paragraph entirely, or replace it with generic copy such as “Please approve the connection in your wallet app.” +5. Re-test MetaMask separately for desktop QR scan, mobile Chrome deeplink return, and MetaMask in-app browser injected login. + +### Risk Assessment +- Risk: Selecting MetaMask’s SDK connector may behave differently from WalletConnect for QR mode. Mitigate by falling back to WalletConnect if the MetaMask connector does not emit a `display_uri` in QR mode. +- Risk: Auto-completing local login from any Wagmi connected account may log in a stale account after reconnect. Mitigate by only completing when modal/pending/login-in-progress context exists or by clearing stale flags. +- Risk: Changing QR value for MetaMask may affect wallets that scan raw WalletConnect URIs. Mitigate by only applying MetaMask app-link QR transformation for MetaMask. + +## Verification Plan +1. Run `npx tsc --noEmit`. +2. Run `npm run format:check`. +3. Run `npm test`. +4. Desktop Chrome + MetaMask Mobile QR: Select MetaMask → QR login → scan/approve → confirm address and `local-wallet:
`. +5. Mobile Chrome + MetaMask app login: Select MetaMask → Open wallet app → approve → return/refresh browser → confirm address appears. +6. Regression test TokenPocket and imToken app login. +7. Confirm imToken/MetaMask QR login no longer displays TokenPocket-specific text. + +## Related Issues +- TokenPocket was previously fixed by handling mobile return/reload behavior. +- imToken was previously fixed by adding in-app browser fallback and injected no-signature login. +- Existing local-memory context indicates MetaMask QR approval has been observed to not update frontend state. + +## Notes +- This report is diagnosis only; no source fix was applied during `/unipi:debug`. +- The local no-signature session model means the frontend does not need backend wallet nonce/verify endpoints for this fix. +- The current debug UI showing `Wallet debug` may be useful during verification but should be removed or hidden before final production cleanup if no longer needed. diff --git a/.unipi/docs/debug/2026-06-04-imtoken-no-address-debug.md b/.unipi/docs/debug/2026-06-04-imtoken-no-address-debug.md new file mode 100644 index 0000000..911b893 --- /dev/null +++ b/.unipi/docs/debug/2026-06-04-imtoken-no-address-debug.md @@ -0,0 +1,137 @@ +--- +title: "imToken opens in-app browser but cannot get wallet address — Debug Report" +type: debug +date: 2026-06-04 +severity: high +status: needs-investigation +--- + +# imToken opens in-app browser but cannot get wallet address — Debug Report + +## Summary + +imToken can open the site in its in-app browser, but the frontend does not obtain a wallet address and the login flow stays at `connected: no`. + +## Expected Behavior + +1. User taps imToken from Chrome or from imToken's in-app browser. +2. The site detects the imToken injected wallet provider. +3. The frontend requests accounts via the injected provider. +4. The frontend receives a wallet address and completes login. + +## Actual Behavior + +The page opens inside imToken, but the wallet modal remains stuck with: + +- `state: connecting` +- `connected: no` +- `address: -` +- `qr: -` + +The supplied screenshot shows the page loaded at `192.168.1.187` and the imToken card selected. + +## Reproduction Steps + +1. Open the site in Chrome on mobile. +2. Tap wallet login. +3. Select imToken. +4. The app opens imToken's in-app browser. +5. Login does not complete and no wallet address appears. +6. Open the same page inside imToken's in-app browser and tap login again. +7. The wallet debug panel still shows `connected: no` and `address: -`. + +## Environment + +- Branch: `terry-wallet-login` +- Frontend URL shown in screenshot: `192.168.1.187` +- Wallet: imToken mobile in-app browser +- Network path: local LAN IP, likely non-HTTPS + +## Root Cause Analysis + +### Failure Chain + +1. `WalletLoginModal` selects imToken and exposes the mobile actions. +2. `openWalletAppDirect(kind)` first checks `getInjectedWallet(kind)`. +3. If no injected provider is detected, the flow deep-links to imToken or falls through to `useWalletConnectLogin.start()` depending on which action is tapped. +4. `useWalletConnectLogin.start()` sets `state` to `connecting` before trying direct injected login. +5. The direct injected branch only runs when `getInjectedWallet(preferredWallet)` returns a provider. +6. The screenshot shows `state: connecting`, `connected: no`, `address: -`, and `qr: -`, which means no address was completed through either direct injected login or WalletConnect. +7. `AutoInjectedLogin` can auto-start imToken login without `?autoLogin=imToken` only if `isImTokenBrowser()` detects the imToken user agent; it still depends on `waitForInjected("imToken")`, which depends on `getInjectedWallet("imToken")`. +8. Therefore the app is not obtaining a usable imToken injected provider, or the provider is present but `eth_accounts` / `eth_requestAccounts` returns no valid address. + +### Root Cause + +The immediate root cause is failure to obtain a usable injected imToken provider/account. The most likely reason from the screenshot is that the page is running on raw LAN IP `192.168.1.187`, likely over HTTP. imToken may not inject its EIP-1193 provider into non-HTTPS/raw-IP pages, or its iOS in-app browser may not expose a user agent/provider shape that matches the current checks. + +The code currently assumes at least one of these is true: + +- URL contains `?autoLogin=imToken`, or +- `navigator.userAgent` matches `/imtoken/i`, and +- `window.ethereum` is present and accepted by `getInjectedWallet("imToken")`, and +- `eth_accounts` or `eth_requestAccounts` returns a valid `0x...` address. + +The observed behavior shows that this assumption chain is breaking before a valid address is produced. + +### Evidence + +- File: `src/wallet/WalletLoginModal.tsx` — `openWalletAppDirect()` uses `getInjectedWallet(kind)` as the gate for direct injected login; if it is false, the flow navigates/deep-links instead of reading an address. +- File: `src/wallet/useWalletConnectLogin.ts` — `start()` sets `state` to `connecting`; the imToken direct local-session path only executes inside `if (mode === "deeplink" && preferredWallet && getInjectedWallet(preferredWallet))`. +- File: `src/wallet/AutoInjectedLogin.tsx` — auto-login picks imToken from the query parameter or `isImTokenBrowser()`, then waits for `getInjectedWallet("imToken")` before calling `connectInjectedWallet("imToken")`. +- File: `src/wallet/injected.ts` — `getInjectedWallet("imToken")` falls back to generic `window.ethereum` only when `isImTokenBrowser()` is true. +- Screenshot evidence: modal shows `state: connecting`, `connected: no`, `address: -`, `qr: -`, and URL `192.168.1.187`. + +## Affected Files + +- `src/wallet/WalletLoginModal.tsx` — user entry point for mobile imToken login. +- `src/wallet/AutoInjectedLogin.tsx` — auto-login effect for wallet in-app browsers. +- `src/wallet/useWalletConnectLogin.ts` — direct injected login vs WalletConnect fallback selection. +- `src/wallet/injected.ts` — provider/account detection and account request logic. +- `src/wallet/deepLinks.ts` — imToken in-app browser deeplink target. + +## Suggested Fix + +Do not guess blindly from `connected: no`; first make the runtime state visible. Add a temporary imToken diagnostic surface or alert that reports: + +- `navigator.userAgent` +- `location.href` +- whether `window.ethereum` exists +- whether `window.ethereum.providers` exists and its length +- provider flags: `isImToken`, `isMetaMask`, `isTokenPocket` +- result/error of `eth_accounts` +- result/error of `eth_requestAccounts` +- current `isSecureContext` + +Then apply one of these fixes based on the diagnostic result: + +### Fix Strategy + +1. If `window.ethereum` is missing on `192.168.1.187`, test and deploy through an HTTPS domain/tunnel because imToken is likely not injecting on the LAN IP origin. +2. If `window.ethereum` exists but `isImTokenBrowser()` is false, broaden imToken detection or allow a user-selected imToken flow to try generic `window.ethereum` before WalletConnect. +3. If `eth_accounts` is empty and `eth_requestAccounts` errors, surface that wallet error in the modal instead of leaving `state: connecting`. +4. If a valid address is returned, complete imToken login with the local-session path already used by `connectInjectedWallet()` + `localWalletToken()`. +5. Ensure the imToken mobile button never silently falls into WalletConnect when the user is already inside imToken and selected imToken; it should either get an address or show a clear injected-provider/account error. + +### Risk Assessment + +- Broadly accepting generic `window.ethereum` could pick the wrong provider in multi-wallet browsers. Mitigation: only do this for explicit user selection of imToken or when already inside imToken. +- Testing on LAN IP can produce false negatives. Mitigation: verify on the actual HTTPS domain or an HTTPS tunnel before judging imToken support. +- More diagnostics can expose wallet details on screen. Mitigation: keep diagnostics temporary or behind a debug flag. + +## Verification Plan + +1. Test in imToken in-app browser on the production HTTPS domain. +2. Test in imToken in-app browser on the current LAN IP to confirm whether `window.ethereum` is missing there. +3. Record diagnostic output for `userAgent`, `hasEthereum`, provider flags, `eth_accounts`, and `eth_requestAccounts`. +4. From Chrome, tap imToken and confirm the in-app browser receives either `?autoLogin=imToken` or the imToken browser fallback runs. +5. Inside imToken, tap wallet login and confirm it does not remain at `state: connecting` without an address. + +## Related Issues + +- `.unipi/docs/fix/2026-06-04-imtoken-injected-provider-detection-fix.md` +- `.unipi/docs/fix/2026-06-04-imtoken-restore-local-session-login-fix.md` +- `.unipi/docs/fix/2026-06-04-imtoken-auto-login-without-query-fix.md` + +## Notes + +The current screenshot strongly suggests that the problem is not backend verification or local token writing. The flow never reaches an address. The next useful step is to confirm whether imToken is injecting `window.ethereum` on the tested origin. diff --git a/.unipi/docs/fix/2026-06-03-mobile-menu-drawer-stacking-fix.md b/.unipi/docs/fix/2026-06-03-mobile-menu-drawer-stacking-fix.md new file mode 100644 index 0000000..1d852ec --- /dev/null +++ b/.unipi/docs/fix/2026-06-03-mobile-menu-drawer-stacking-fix.md @@ -0,0 +1,29 @@ +--- +title: "Mobile menu drawer invisible — Quick Fix" +type: quick-fix +date: 2026-06-03 +--- + +# Mobile menu drawer invisible — Quick Fix + +## Bug +After redesigning the mobile menu to the full-screen Figma drawer (`4164-5336` → `ARK V2 - 導航菜單`), tapping the hamburger toggled the icon to `X` but the drawer overlay never appeared on screen. Page content stayed fully visible and the bottom nav stayed on top. + +## Root Cause +The drawer was rendered as a child of `
`. A `position: sticky` element with a `z-index` creates its own stacking context, which traps the drawer's `position: fixed; z-50` inside that context. Globally, the drawer ends up bound to the header's `z-40` layer, while the unrelated bottom navigation (`