---
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.
