// QR code REST endpoints (docs/IM-前后端对接文档.md §5.15.1). Confirmed LIVE against the real // deployment (2026-09-08, two fresh test accounts) -- `resolveQRCode`'s real response shape nests // `relationship`/`can_add`/`action`/`public_account` all under `user`, unlike the doc's own worked // example which shows them as siblings of `user` at the top level. Also confirmed live: resolving a // code against the SAME account that minted it is rejected (`400 MALFORMED_REQUEST`, `field:"token"`, // message "malformed request" -- distinct from a genuinely garbage token, which instead gets message // "qr code invalid" with no `details.field` at all) -- cross-account resolution works correctly. // // The security rationale for why this exists at all instead of just encoding a raw user id (doc line // 1299): "二维码只含不透明、签名、带租户和用途约束的短期 Token,不直接放手机号、用户 ID 或登录 // Token" -- the QR image payload is `qr_payload` (an `im://qr?token=...&purpose=...` URI), not // `qr_token` alone, matching the old app's own `im://qr` scheme (im_webapp/src/views/tinode-web.jsx // confirmed this exact prefix check before ever sending a scanned string to `resolve`). import { imPost } from './client'; interface QRCodeDisplayProfile { user_id: string; display_name: string; public_account?: string; } export interface MyQRCode { qr_token: string; /** What actually gets encoded into the QR image -- `im://qr?token=...&purpose=...`. */ qr_payload: string; expires_at: string; display_profile: QRCodeDisplayProfile; } export function createMyQRCode(expiresInSeconds = 86400) { return imPost('/me/qrcodes', { purpose: 'add_friend', expires_in_seconds: expiresInSeconds }); } /** `identifier`: user_id/username/public_account -- viewing a specific friend's own QR card. */ export function createUserQRCode(identifier: string, expiresInSeconds = 86400) { return imPost(`/users/${encodeURIComponent(identifier)}/qrcodes`, { purpose: 'add_friend', expires_in_seconds: expiresInSeconds }); } export interface QRResolveUser { user_id: string; display_name: string; public_account?: string; // docs/IM-前后端对接文档.md:1289 -- same real direction-specific enum as friends.ts's // UserSearchResult/UserProfile, never a bare "pending". relationship: 'none' | 'friend' | 'pending_inbound' | 'pending_outbound' | string; can_add: boolean; action: string; } /** Confirmed LIVE (2026-09-08, same account that minted the code -- a real cross-account "scan a * stranger's group code" resolve hasn't been exercised, this session only had one account free): * a `purpose:"join_group"` resolve nests the group under `group` (NOT `group_snapshot`, despite * the mint response using that key -- resolve and mint use different field names for the same * shape), and carries a `status` this specific case's doc example (line 1737, mint-only) never * showed: resolving your own group's code (i.e. already a member) comes back `status:"joined"`. * What a genuinely-not-yet-a-member resolve returns for `status` is unconfirmed -- treated as * "anything other than 'joined'" in QRScannerModal/AddFriendModal, i.e. the general case that still * needs a real join-request submission. */ export interface QRResolveGroup { topic: string; name: string; member_count: number; } export interface QRResolveResult { purpose: 'add_friend' | 'join_group' | string; source: string; source_topic: string; expires_at: string; user?: QRResolveUser; group?: QRResolveGroup; group_snapshot?: QRResolveGroup; /** join_group only -- `"joined"` means the scanning account is already a member (confirmed live); * any other value (including absent) is treated as "not a member yet, submit a join request". */ status?: string; } export function resolveQRCode(token: string, purpose: 'add_friend' | 'join_group' | string = 'add_friend') { return imPost('/qrcodes/resolve', { token, purpose }); } /** docs/IM-前后端对接文档.md:1590 -- 群管理员/群主展示的入群二维码 (mirrors createMyQRCode/ * createUserQRCode's shape, just scoped to a group and carrying `group_snapshot` instead of a * personal `display_profile`). */ export interface GroupQRCode { qr_token: string; qr_payload: string; expires_at: string; group_snapshot: QRResolveGroup; } export function createGroupQRCode(topic: string, expiresInSeconds = 86400) { return imPost(`/groups/${encodeURIComponent(topic)}/qrcodes`, { expires_in_seconds: expiresInSeconds }); }