// mock.ts — MockClient:演示数据,供阶段 A 的 UI 验收使用,零网络依赖。 import { ApiClient, ApiError, Device, LoginResult, Me, RedeemResult, Session, SubscriptionInfo, TotpSetup, } from './types'; import { setSession, clearSession, setEmail } from './session'; const delay = (ms = 420) => new Promise((r) => setTimeout(r, ms)); function makeSession(): Session { return { accessToken: 'mock-access-' + Math.random().toString(36).slice(2), refreshToken: 'mock-refresh-' + Math.random().toString(36).slice(2), accessExpiresAt: Date.now() + 10 * 60_000, }; } export class MockClient implements ApiClient { // 演示态:PRO 会员,已开启 2FA(故登录走二段式) private totpEnabled = true; private pending: string | null = null; private subUrl = 'https://sub.pangolin.vpn/s/8f3kx92m'; private devices: Device[] = [ { id: 'd1', name: 'iPhone 15 Pro', platform: 'iOS 18', lastActive: '刚刚', current: true }, { id: 'd2', name: 'MacBook Air', platform: 'macOS 15', lastActive: '2 小时前', current: false }, ]; async login(email: string, password: string): Promise { await delay(); // 演示锁定/失败:用特定凭证触发,便于验收双语文案 if (password === 'locked') { throw new ApiError( { code: 'account_locked', message_zh: '失败次数过多,账户已临时锁定', message_en: 'Too many attempts — account temporarily locked' }, 30, ); } if (password === 'wrong' || !/\S+@\S+\.\S+/.test(email)) { throw new ApiError({ code: 'invalid_credentials', message_zh: '邮箱或密码错误', message_en: 'Wrong email or password' }); } if (this.totpEnabled) { this.pending = 'mock-pending-' + Math.random().toString(36).slice(2); return { kind: 'totp_required', pendingToken: this.pending }; } const s = makeSession(); setSession(s); return { kind: 'session', session: s }; } async loginTotp(_pendingToken: string, code: string): Promise { await delay(); if (code === '000000') { throw new ApiError({ code: 'totp_invalid', message_zh: '动态码不正确,请重试', message_en: 'Invalid code, try again' }); } const s = makeSession(); setSession(s); this.pending = null; return s; } // 演示态:任意非 'invalid' 票据都兑换成功;'invalid' 用于验收失败态文案。 async exchangeWebTicket(ticket: string): Promise { await delay(300); if (ticket === 'invalid') { throw new ApiError({ code: 'auth.ticket_invalid', message_zh: '登录票据无效或已过期,请重新从 App 打开', message_en: 'Login ticket is invalid or expired, please reopen from the app', }); } const s = makeSession(); setSession(s); return s; } async refresh(): Promise { await delay(150); const s = makeSession(); setSession(s); return s; } async getMe(): Promise { await delay(260); setEmail('me@pangolin.vpn'); // 同源官网读取显示用户名;clearSession/logout 时删除 return { email: 'me@pangolin.vpn', plan: 'pro', expiresAt: '2026-12-31', devicesUsed: 2, devicesMax: 5, quotaTodayMin: null, dataTodayGB: 0.8, weeklyGB: [1.8, 2.4, 1.2, 3.1, 4.0, 5.2, 2.6], totpEnabled: this.totpEnabled, }; } async getSubscription(): Promise { await delay(160); return { url: this.subUrl }; } async resetSubscription(): Promise { await delay(500); this.subUrl = 'https://sub.pangolin.vpn/s/' + Math.random().toString(36).slice(2, 10); return { url: this.subUrl }; } async listDevices(): Promise { await delay(220); return [...this.devices]; } async removeDevice(id: string): Promise { await delay(360); this.devices = this.devices.filter((d) => d.id !== id); } async redeem(code: string): Promise { await delay(520); const c = code.trim().toUpperCase(); if (c === 'LOCKED') { throw new ApiError( { code: 'rate_limited', message_zh: '操作过于频繁,请稍后再试', message_en: 'Too many requests, slow down' }, 20, ); } if (c === 'USED') { throw new ApiError({ code: 'code_used', message_zh: '激活码已被使用', message_en: 'Activation code already used' }); } if (c.length < 4 || c === 'INVALID') { throw new ApiError({ code: 'code_invalid', message_zh: '激活码无效', message_en: 'Invalid activation code' }); } return { plan: 'pro', expiresAt: '2027-12-31' }; } async totpSetup(): Promise { await delay(260); const secret = 'JBSWY3DPEHPK3PXP'; return { secret, otpauthUri: `otpauth://totp/Pangolin:me@pangolin.vpn?secret=${secret}&issuer=Pangolin`, }; } async totpVerify(code: string): Promise { await delay(300); if (code === '000000' || code.length !== 6) { throw new ApiError({ code: 'totp_invalid', message_zh: '动态码不正确,请重试', message_en: 'Invalid code, try again' }); } this.totpEnabled = true; } async totpDisable(code: string): Promise { await delay(300); if (code === '000000' || code.length !== 6) { throw new ApiError({ code: 'totp_invalid', message_zh: '动态码不正确,请重试', message_en: 'Invalid code, try again' }); } this.totpEnabled = false; } async logout(): Promise { await delay(120); this.pending = null; clearSession(); } }