diff --git a/.unipi/docs/specs/2026-06-01-china-friendly-wallet-login-design.md b/.unipi/docs/specs/2026-06-01-china-friendly-wallet-login-design.md new file mode 100644 index 0000000..12dc992 --- /dev/null +++ b/.unipi/docs/specs/2026-06-01-china-friendly-wallet-login-design.md @@ -0,0 +1,367 @@ +--- +title: "China-Friendly Wallet Login" +type: brainstorm +date: 2026-06-01 +--- + +# China-Friendly Wallet Login + +## Problem Statement + +ARK Library needs wallet-based login so users can later access account-bound features such as favorites. The login must work for China-based users without requiring VPN access where possible. The goal is not to perform on-chain reads or transactions; it is only to verify wallet address ownership through message signing and bind that address to a backend session/JWT. + +The practical problem is that mobile users may open the DApp in different environments: desktop browser with extension, wallet DApp browser, or a normal mobile browser. A normal mobile browser cannot directly talk to a wallet app unless there is a bridge such as WalletConnect/Reown, a wallet-specific SDK, or a wallet-specific callback/deep-link flow. + +## Context + +Existing backend wallet authentication is partially available: + +- `POST /api/auth/wallet/nonce` +- `POST /api/auth/wallet/verify` +- `GET /api/auth/wallet/me` + +Backend findings: + +- Wallet nonce currently expires after 15 minutes. +- Wallet JWT currently lasts 30 days. +- There is no refresh-token mechanism. +- There is no user-bound favorites API yet. +- Existing `/api/resources/{id}/favorite` only changes global favorite count and does not bind favorites to a wallet address. + +Frontend findings: + +- There is currently no wallet login implementation. +- Favorites page is currently a placeholder. +- Wallet login should be designed separately from the full favorites feature. + +Research findings: + +- RainbowKit is a wallet UI layer. Its generic scan/login flow usually depends on WalletConnect/Reown. +- Reown/WalletConnect relay was previously found unstable for China access. +- RainbowKit can appear stable when users are actually connecting through injected providers (`window.ethereum`) in extensions or wallet DApp browsers; that stability does not prove WalletConnect/Reown scan stability. +- TokenPocket supports stable external-browser flows through `tpoutside://pull.activity` deep links and callback URLs. +- TokenPocket `tp-js-sdk` only works inside TokenPocket's DApp browser, so it is not the external-browser QR bridge by itself. +- imToken QR login generally uses WalletConnect-style bridging. +- MetaMask QR login can use WalletConnect/RainbowKit-style bridging or MetaMask SDK. For this project, MetaMask QR is handled through RainbowKit as a fallback rather than a China-stable primary path. +- OKX Connect SDK was considered but rejected because OKX Wallet is not a target wallet for this product. + +## Chosen Approach + +Use a hybrid wallet login approach: + +1. **Stable primary path:** custom injected-provider login plus TokenPocket QR/callback login. +2. **Compatibility fallback:** RainbowKit/Reown QR login for MetaMask and imToken users who want scan-login from a separate device. + +Supported wallets for this design: + +1. TokenPocket +2. MetaMask +3. imToken + +Supported login paths: + +| Wallet | Injected / DApp browser login | Click / deep-link login | QR login back to current browser | +|---|---:|---:|---:| +| TokenPocket | Yes | Yes | Yes, via TokenPocket callback | +| MetaMask | Yes | Yes | Yes, via RainbowKit/Reown fallback | +| imToken | Yes | Yes | Yes, via RainbowKit/Reown fallback | + +The UI must not imply that all QR methods are equally stable in China. It should distinguish: + +- **TokenPocket QR login** — recommended China-stable QR path. +- **MetaMask / imToken QR login** — compatibility fallback powered by RainbowKit/Reown; may fail or be slow depending on network environment. + +## Why This Approach + +This approach balances China stability with the user's requirement that MetaMask and imToken also have QR login options. + +Accepted trade-offs: + +- TokenPocket gets the primary QR login because it provides a direct callback mechanism that avoids Reown/WalletConnect relay instability. +- MetaMask and imToken get QR login through RainbowKit/Reown, but this is explicitly treated as a fallback and not the recommended China-stable path. +- The frontend will include heavier wallet dependencies for the fallback path: RainbowKit, wagmi, viem, and Reown/WalletConnect configuration. +- A WalletConnect/Reown project ID is required through environment configuration. +- Full favorites behavior is out of scope for this spec and should be designed separately. + +Rejected alternatives: + +1. **RainbowKit/Reown for all wallets including TokenPocket** + - Rejected because it would make the China-stable TokenPocket flow depend on the relay that was already found unstable. + +2. **TokenPocket-only QR login** + - Rejected because the desired product behavior now includes MetaMask and imToken QR login, even if those QR paths are less reliable in China. + +3. **OKX Connect SDK** + - Rejected because OKX Wallet is not a target wallet for the current product requirement. + +4. **MetaMask SDK separate integration** + - Rejected for now because RainbowKit/Reown gives a broader compatibility fallback for both MetaMask and imToken with one integration. + +5. **Favorites in the same plan** + - Rejected because wallet login and user-bound favorites are separate subsystems. Favorites needs its own backend endpoints and product decisions. + +## Design + +### Architecture + +The login feature should be split into small units: + +1. **Wallet auth API client** + - Requests nonce. + - Verifies signed messages. + - Fetches current wallet session. + - Stores and clears JWT. + +2. **Wallet session provider** + - Owns login state: loading, logged out, logged in, error. + - Exposes wallet address, shortened address, token status, login actions, logout action. + - Restores session through `/api/auth/wallet/me` when a token exists. + +3. **Injected provider adapter** + - Uses `window.ethereum` when available. + - Requests accounts. + - Signs nonce via `personal_sign`. + - Sends address/signature to backend verify endpoint. + - Covers desktop extensions and wallet DApp browsers. + +4. **TokenPocket QR adapter** + - Creates a server-recognized `actionId` / login request. + - Gets or constructs a TokenPocket `tpoutside://pull.activity` login/sign deep link. + - Displays it as QR code. + - Frontend polls backend for callback result. + - Once backend has the address/signature result, frontend finalizes login through normal wallet verification. + - This is the recommended QR path for China users. + +5. **RainbowKit QR fallback adapter** + - Configures RainbowKit/wagmi/WalletConnect for MetaMask and imToken QR login. + - Uses `VITE_WALLETCONNECT_PROJECT_ID` for the Reown project ID. + - After wallet connection, requests a backend nonce and asks the connected wallet to sign the nonce. + - Sends address/signature to `/api/auth/wallet/verify` and stores the returned JWT. + - UI copy must label this as a fallback that may be unstable on some China networks. + +6. **Wallet deep-link helper** + - Provides buttons for TokenPocket, MetaMask, and imToken. + - Opens the current URL in the selected wallet's DApp browser when no injected provider is available or when the user chooses the DApp-browser path. + +7. **Wallet login modal** + - Shows wallet options. + - Shows TokenPocket QR as the recommended scan option. + - Shows MetaMask/imToken QR via RainbowKit as an alternate scan option. + - Shows device-specific copy: + - Desktop TokenPocket QR: "Use TokenPocket on your phone to scan this QR code." + - Mobile TokenPocket QR: "Use TokenPocket on another device to scan this QR code." + - RainbowKit fallback: "MetaMask / imToken QR uses WalletConnect/Reown and may be unstable on some networks. If it fails, open this site inside your wallet app." + +### User Flow: Injected Login + +1. User clicks Connect Wallet. +2. Frontend detects `window.ethereum`. +3. Frontend requests wallet accounts. +4. Frontend requests nonce from backend. +5. User signs nonce through wallet. +6. Frontend sends address and signature to backend verify endpoint. +7. Backend verifies signature and returns JWT. +8. Frontend stores JWT and updates UI to shortened address. + +### User Flow: TokenPocket QR Login + +1. User opens login modal and chooses TokenPocket QR. +2. Frontend creates a TokenPocket QR login request with a unique `actionId`. +3. Frontend displays a QR code for TokenPocket. +4. User scans QR with TokenPocket on another device. +5. TokenPocket asks user to sign the login message. +6. TokenPocket sends result to backend callback URL. +7. Frontend polls backend for `actionId` result. +8. Frontend receives address/signature result and completes verify flow. +9. Frontend stores JWT and updates UI. + +### User Flow: MetaMask / imToken QR Fallback + +1. User chooses MetaMask/imToken QR login. +2. Frontend opens RainbowKit's connection flow with WalletConnect/Reown configured. +3. User scans the QR using MetaMask or imToken. +4. WalletConnect/Reown establishes the session. +5. Frontend requests a backend nonce. +6. User signs the nonce through the connected wallet. +7. Frontend sends address/signature to `/api/auth/wallet/verify`. +8. Frontend stores JWT and updates UI. +9. If connection fails or times out, UI recommends TokenPocket QR or opening the site inside the wallet DApp browser. + +### User Flow: MetaMask / imToken Deep Link + +1. User clicks MetaMask or imToken button. +2. If injected provider exists, use injected login. +3. If no injected provider exists, open current site URL in the selected wallet's DApp browser using that wallet's deep-link/universal-link format. +4. The login completes inside the wallet DApp browser using injected login. + +### Logged-In UI + +- Desktop/header should show shortened address such as `0x12...ab34`. +- Clicking the address opens a small menu with Disconnect. +- Disconnect clears the local JWT/session and returns UI to logged-out state. +- No ENS lookup is required. +- No remote avatar lookup is required. + +### Backend API Contract + +Existing wallet auth endpoints should remain the canonical verification path: + +```http +POST /api/auth/wallet/nonce +POST /api/auth/wallet/verify +GET /api/auth/wallet/me +``` + +The frontend needs exact request/response contracts confirmed before implementation. Expected shape: + +```http +POST /api/auth/wallet/nonce +Content-Type: application/json + +{ "address": "0x..." } +``` + +```json +{ "nonce": "message to sign" } +``` + +```http +POST /api/auth/wallet/verify +Content-Type: application/json + +{ "address": "0x...", "signature": "0x..." } +``` + +```json +{ "token": "jwt", "address": "0x..." } +``` + +For TokenPocket QR login, backend needs additional endpoints or equivalent behavior: + +```http +POST /api/auth/wallet/tp-login-request +``` + +Creates a short-lived login request and returns data needed to render the QR. + +Expected output: + +```json +{ + "actionId": "unique-id", + "message": "message to sign", + "qrUrl": "tpoutside://pull.activity?param=...", + "expiresAt": "ISO timestamp" +} +``` + +```http +POST /api/auth/wallet/tp-callback +``` + +Called by TokenPocket after user signs. Backend validates the callback payload shape, stores the result by `actionId`, and expires it quickly. + +```http +GET /api/auth/wallet/tp-result?actionId=... +``` + +Frontend polls this endpoint until result is pending, completed, expired, or failed. + +Expected states: + +```json +{ "status": "pending" } +``` + +```json +{ + "status": "completed", + "address": "0x...", + "signature": "0x..." +} +``` + +```json +{ "status": "expired" } +``` + +The final JWT should still come from `/api/auth/wallet/verify` so all wallet-login paths share one verification endpoint. + +RainbowKit/Reown QR fallback does not require new backend endpoints beyond the canonical nonce/verify/me endpoints, but it does require frontend environment configuration: + +```env +VITE_WALLETCONNECT_PROJECT_ID=... +``` + +### Error Handling + +- No wallet detected: show wallet choices, TokenPocket QR login, and RainbowKit QR fallback. +- User rejects signature: show a clear retryable error. +- Nonce expired: request a fresh nonce and retry. +- TokenPocket QR expired: generate a new QR. +- TokenPocket callback never arrives: show timeout and retry option. +- RainbowKit/Reown connection fails: explain that scan login may be blocked or slow on this network; recommend TokenPocket QR or wallet DApp browser. +- Invalid signature: show login failed and do not store token. +- `/me` fails with expired/invalid token: clear token and return to logged-out state. + +### Testing + +Frontend testing should cover: + +- Session provider restores logged-in state when `/me` succeeds. +- Session provider clears state when `/me` fails. +- Injected adapter signs and verifies through mocked provider/API. +- TokenPocket QR polling handles pending, completed, expired, failed, and timeout states. +- RainbowKit fallback handles connected, rejected, timeout/failure, and signed-message states. +- Login modal copy correctly distinguishes TokenPocket QR from RainbowKit/Reown fallback QR. +- Logout clears token and resets UI. + +Backend testing should cover: + +- TokenPocket login request creates short-lived action IDs. +- Callback stores exactly one completed result per action ID. +- Expired action IDs cannot be completed. +- Polling endpoint returns correct states. +- Verify endpoint still validates signatures and returns JWT. + +## Implementation Checklist + +- [ ] Confirm exact existing wallet auth request/response shapes with backend. +- [ ] Confirm TokenPocket callback payload fields from official docs or a sandbox callback test. +- [ ] Confirm WalletConnect/Reown project ID ownership and add `VITE_WALLETCONNECT_PROJECT_ID` to env docs. +- [ ] Add backend TokenPocket login request endpoint. +- [ ] Add backend TokenPocket callback endpoint. +- [ ] Add backend TokenPocket polling/result endpoint. +- [ ] Add frontend install plan for RainbowKit, wagmi, viem, and required query provider dependency. +- [ ] Add frontend wallet auth API client and token storage helpers. +- [ ] Add frontend wallet session provider/hook. +- [ ] Add injected provider login adapter. +- [ ] Add TokenPocket QR login adapter and polling flow. +- [ ] Add RainbowKit/Reown QR fallback for MetaMask and imToken. +- [ ] Add wallet deep-link helpers for TokenPocket, MetaMask, and imToken. +- [ ] Add wallet login modal and header logged-in state UI. +- [ ] Wire logout to clear token and session state. +- [ ] Add frontend tests for session, injected login, TP QR polling, RainbowKit fallback, and logout. +- [ ] Add backend tests for TokenPocket request/callback/result behavior. +- [ ] Document the backend API contract for Louis/backend implementation. + +## Open Questions + +1. What exact message format should users sign? It should include domain, wallet address, nonce, issued-at time, and purpose such as "Sign in to ARK Library". +2. Should JWT remain 30 days, or should backend add refresh tokens later? +3. What exact TokenPocket callback payload will be received for EVM personal-sign login? +4. Which public icon URL should TokenPocket QR metadata use for ARK Library? +5. Should login UI appear only in the header and favorites flow, or also in mobile menu? +6. Should favorites trigger wallet login immediately when clicked, or should that be decided in the separate favorites design? +7. Which Reown/WalletConnect project ID should production use, and who owns that Reown project? +8. Should RainbowKit fallback be hidden or visually de-emphasized for China users, or simply shown with warning copy? + +## Out of Scope + +- Full user-bound favorites implementation. +- Favorites page real list UI. +- Favorites database schema. +- OKX Connect SDK. +- MetaMask SDK separate integration. +- ENS names, ENS avatars, or chain data reads. +- On-chain transactions.