import { useCallback, useState } from 'react'; import { fetchWechatLoginResult } from '../api'; import type { AppRole, WechatPendingTicket, WechatUserProfile } from '../types'; import { isWechatAvailable, requestWechatAuthCode } from '../lib/wechat-sdk'; import { useAuth } from '../context/AuthContext'; /* ===================== 微信登录状态机 ===================== * 编排三种账号场景的流转,页面只按 state.kind 渲染,不写判断逻辑。 * * idle ──start()──> 本地 token 有效?──是──> alreadyLoggedIn(免授权直接落地) * │ * 否 * ↓ * authenticating(调起微信) * ↓ * verifying(后端校验 code) * ↓ * ┌───────────────┼───────────────┐ * new 分支 active 分支 deleted 分支 * ↓ ↓ ↓ * needBindPhone success deleted(提示无法登录) * (跳手机号+验证码) │ * ↓ cancelLogin() → idle * 由 P2 页完成绑定后落地 * * 注意:注销审核期内的账号不可登录、不可复活,因此 deleted 分支没有「重新注册」出口。 */ /** 待绑定手机号时持有的临时凭证(原 code 已失效,只能用它标识身份) */ export type PendingBindPhone = WechatPendingTicket; /** 微信登录状态(判别联合,页面通过 kind 收窄后读取对应字段) */ export type WechatLoginState = | { kind: 'idle' } /** 正在调起微信 */ | { kind: 'authenticating' } /** 已拿到 code,正在请求后端校验 */ | { kind: 'verifying' } /** 本地 token 未过期,无需再走微信授权 */ | { kind: 'alreadyLoggedIn' } /** 第一次登录:openid 未绑定账号,需跳手机号 + 验证码完成注册 */ | { kind: 'needBindPhone'; ticket: PendingBindPhone } /** 账号处于注销审核期,不可登录 */ | { kind: 'deleted' } /** 登录成功;role 为 undefined 表示尚未选择身份,需引导去注册流程 */ | { kind: 'success'; profile: WechatUserProfile; role?: AppRole } /** 失败(未安装微信 / 用户取消 / 网络异常) */ | { kind: 'error'; message: string }; export interface UseWechatLoginReturn { state: WechatLoginState; /** 是否处于请求中(用于按钮 loading 与防重复点击) */ loading: boolean; /** 入口:本地 token 有效则直接登录,否则走微信授权 */ start: () => Promise; /** 关闭注销提示,回到登录页 */ cancelLogin: () => void; /** 重置为初始态 */ reset: () => void; } /** 初始态 */ const IDLE: WechatLoginState = { kind: 'idle' }; export function useWechatLogin(): UseWechatLoginReturn { const { login, isTokenValid } = useAuth(); const [state, setState] = useState(IDLE); const start = useCallback(async () => { // 第二次及以后登录:本地令牌仍有效时免微信授权 if (isTokenValid) { setState({ kind: 'alreadyLoggedIn' }); return; } setState({ kind: 'authenticating' }); try { if (!isWechatAvailable()) { throw new Error('未检测到微信客户端,请先安装微信'); } const code = await requestWechatAuthCode(); setState({ kind: 'verifying' }); const result = await fetchWechatLoginResult(code); switch (result.status) { case 'active': { if (!result.profile) { throw new Error('登录信息不完整,请重试'); } login(result.token, result.expiresAt); setState({ kind: 'success', profile: result.profile, role: result.role }); break; } case 'new': { setState({ kind: 'needBindPhone', ticket: { token: result.token, openid: result.openid }, }); break; } case 'deleted': { // 注销审核期:不写登录态,仅提示不可登录 setState({ kind: 'deleted' }); break; } } } catch (e) { setState({ kind: 'error', message: e instanceof Error ? e.message : '微信登录失败,请稍后重试', }); } }, [isTokenValid, login]); const cancelLogin = useCallback(() => { setState(IDLE); }, []); const reset = useCallback(() => { setState(IDLE); }, []); return { state, loading: state.kind === 'authenticating' || state.kind === 'verifying', start, cancelLogin, reset, }; }