diff --git a/src/pages/login/index.module.scss b/src/pages/login/index.module.scss index b6650e3..535d798 100644 --- a/src/pages/login/index.module.scss +++ b/src/pages/login/index.module.scss @@ -63,27 +63,6 @@ margin-top: 32px; } -// 手机号授权用原生 Button(open-type=getPhoneNumber),抹平默认样式与 .submit 对齐 -.submitBtn { - height: 96px; - margin-top: 32px; - background: $color-primary-grad; - border-radius: 48px; - display: flex; - align-items: center; - justify-content: center; - color: #ffffff; - font-size: 32px; - font-weight: 600; - line-height: 1; - padding: 0; - border: none; - - &::after { - border: none; - } -} - .submitText { color: #ffffff; font-size: 32px; diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 3060efd..5added1 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -1,15 +1,9 @@ import { useState } from 'react' -import { View, Text, Button } from '@tarojs/components' +import { View, Text } from '@tarojs/components' import { useDidShow } from '@tarojs/taro' import { useAppDispatch, useAppSelector } from '@/store/hooks' import { setAuth } from '@/store/authSlice' -import { - performWxLogin, - completeUserInfoLogin, - completeMobileLogin, - getLoginSubState, - type LoginSubState -} from './wxLoginFlow' +import { performWxLogin, completeUserInfoLogin, getLoginSubState } from './wxLoginFlow' import { useToast } from '@/hooks/useToast' import { ROUTES } from '@/constants/routes' import { reLaunch } from '@/utils/navigation' @@ -22,8 +16,8 @@ export default function Login() { const toast = useToast() const token = useAppSelector((s) => s.auth.token) const [loading, setLoading] = useState(false) - // 注册中间态:needUserInfo(待头像昵称授权)→ noMobile(待手机号授权)→ null(初始/完成) - const [subState, setSubState] = useState(null) + // 注册中间态:needUserInfo(待用户资料授权)→ null(初始/完成) + const [needAuth, setNeedAuth] = useState(false) const [openId, setOpenId] = useState('') // 已有 token 直接进首页(本期不做 checkLogin 校验,spec D5) @@ -31,25 +25,27 @@ export default function Login() { if (token && !loading) reLaunch(ROUTES.HOME) }) - // 完整登录态校验与落地;未完成时推进 subState + // 完整登录态校验与落地;未完成时推进授权态 const applyResult = async (result: Awaited>) => { const state = getLoginSubState(result) if (state !== 'complete') { console.log('[login-page] 注册未完成,推进到', state) - // openId 兜底链:noMobile 阶段后端把 openId 放在 bindKey 字段返回(联调实测); - // 其余阶段从本地缓存(needUserInfo 时已写入)兜底 + if (state === 'noMobile') { + // 手机号授权暂缓(后端字段未定):提示并保持当前态,等后端确认后接续 + throw new Error('后端要求绑定手机号,该流程暂未开通') + } + // openId 兜底链:响应 openId → 本地缓存(needUserInfo 首次已写入)→ 当前 state const openIdNext = - result.openId ?? (state === 'noMobile' ? result.bindKey : undefined) ?? - storage.get(WX_OPENID_STORAGE_KEY) ?? openId + result.openId ?? storage.get(WX_OPENID_STORAGE_KEY) ?? openId if (!openIdNext) throw new Error('响应缺少 openId,无法继续授权') setOpenId(openIdNext) - setSubState(state) + setNeedAuth(true) setLoading(false) return } if (!result.code || !result.userInfo) throw new Error('登录响应缺少凭证') console.log('[login-page] 登录完成,token 已落地') - setSubState(null) + setNeedAuth(false) dispatch(setAuth({ token: result.code, userInfo: result.userInfo })) await reLaunch(ROUTES.HOME) } @@ -67,50 +63,20 @@ export default function Login() { } } - // needUserInfo:getUserProfile 授权头像昵称 + // needUserInfo:getUserInfo 授权用户资料(后端文档约定的授权接口) const handleAuthUserInfo = async () => { if (loading || !openId) return setLoading(true) try { applyResult(await completeUserInfoLogin(openId)) } catch (e) { - // 授权失败/取消:保留待授权态,用户可重试(getUserProfile 每次都需用户确认) + // 授权失败/取消:保留待授权态,用户可重试 console.error('[login-page] completeUserInfoLogin 失败', e) toast.error(e instanceof Error && e.message ? e.message : '授权失败') setLoading(false) } } - // noMobile:手机号授权按钮回调(Button open-type=getPhoneNumber) - const handleGetPhoneNumber = async (e: { - detail: { iv: string; encryptedData: string; code?: string; errMsg?: string } - }) => { - console.log('[login-page] 手机号授权回调', { - errMsg: e.detail.errMsg, - // 两种手机号回传形态:新版 code(动态令牌)或旧版 iv/encryptedData(加密数据) - hasCode: Boolean(e.detail.code), - hasEncrypted: Boolean(e.detail.encryptedData) - }) - if (loading || !openId) return - setLoading(true) - try { - applyResult(await completeMobileLogin(openId, e.detail)) - } catch (err) { - console.error('[login-page] completeMobileLogin 失败', err) - toast.error(err instanceof Error && err.message ? err.message : '授权失败') - setLoading(false) - } - } - - const submitText = () => { - if (loading) { - return subState === 'noMobile' ? '绑定中...' : subState === 'needUserInfo' ? '授权中...' : '登录中...' - } - if (subState === 'needUserInfo') return '授权头像昵称,继续' - if (subState === 'noMobile') return '授权手机号,完成注册' - return '微信一键登录' - } - return ( @@ -121,23 +87,14 @@ export default function Login() { 一起守护家人 - {subState === 'noMobile' ? ( - // 手机号授权必须用原生 Button open-type,微信弹官方授权框 - - ) : ( - - {submitText()} - - )} + + + {loading ? '登录中...' : needAuth ? '授权用户信息,继续' : '微信一键登录'} + + ) diff --git a/src/pages/login/wxLoginFlow.ts b/src/pages/login/wxLoginFlow.ts index 9bb5458..b6bfa84 100644 --- a/src/pages/login/wxLoginFlow.ts +++ b/src/pages/login/wxLoginFlow.ts @@ -62,11 +62,12 @@ async function reloginWith( return result } -// needUserInfo 二次授权:getUserProfile 拿 iv/encryptedData → 重调登录接口补用户资料 +// needUserInfo 二次授权:getUserInfo 拿 iv/encryptedData → 重调登录接口补用户资料 +// 后端文档约定:wx.login 返回 needUserInfo 时须用 wx.getUserInfo 授权(getUserProfile 拿的资料后端不认) export async function completeUserInfoLogin(openId: string): Promise { let profile: { iv: string; encryptedData: string } try { - const r = await Taro.getUserProfile({ desc: '用于完善会员资料' }) + const r = await Taro.getUserInfo({ withCredentials: true }) profile = { iv: r.iv, encryptedData: r.encryptedData } } catch (e) { throw new Error(e instanceof Error ? e.message : (e as { errMsg?: string })?.errMsg || '授权失败') @@ -75,8 +76,7 @@ export async function completeUserInfoLogin(openId: string): Promise vi.fn()) const mockGetAccountInfo = vi.hoisted(() => vi.fn()) -const mockGetUserProfile = vi.hoisted(() => vi.fn()) +const mockGetUserInfo = vi.hoisted(() => vi.fn()) vi.mock('@tarojs/taro', () => ({ default: { login: mockLogin, getAccountInfoSync: mockGetAccountInfo, - getUserProfile: mockGetUserProfile + getUserInfo: mockGetUserInfo } })) @@ -51,7 +51,7 @@ beforeEach(() => { mem.clear() mockLogin.mockReset() mockGetAccountInfo.mockReset() - mockGetUserProfile.mockReset() + mockGetUserInfo.mockReset() mockWxMiniappLogin.mockReset() mockLogin.mockResolvedValue({ code: 'wx-code-1' }) mockGetAccountInfo.mockReturnValue({ miniProgram: { appId: 'wx-appid-live' } }) @@ -136,11 +136,12 @@ describe('needUserInfo 首次登录分支', () => { }) describe('completeUserInfoLogin 二次授权', () => { - it('getUserProfile → 换新 code → 带 iv/encryptedData/openId 重调登录', async () => { - mockGetUserProfile.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' }) + it('getUserInfo(withCredentials) → 换新 code → 带 iv/encryptedData/openId 重调登录', async () => { + mockGetUserInfo.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' }) mockWxMiniappLogin.mockResolvedValue(result({ openId: 'o-need' })) const res = await completeUserInfoLogin('o-need') expect(res.code).toBe('tk-abc') + expect(mockGetUserInfo).toHaveBeenCalledWith({ withCredentials: true }) expect(mockWxMiniappLogin).toHaveBeenCalledWith({ appId: 'wx-appid-live', code: 'wx-code-1', @@ -151,13 +152,13 @@ describe('completeUserInfoLogin 二次授权', () => { }) it('用户拒绝授权(errMsg 形状 reject)抛带 message 的 Error', async () => { - mockGetUserProfile.mockRejectedValue({ errMsg: 'getUserProfile:fail auth deny' }) - await expect(completeUserInfoLogin('o-need')).rejects.toThrow('getUserProfile:fail auth deny') + mockGetUserInfo.mockRejectedValue({ errMsg: 'getUserInfo:fail auth deny' }) + await expect(completeUserInfoLogin('o-need')).rejects.toThrow('getUserInfo:fail auth deny') expect(mockWxMiniappLogin).not.toHaveBeenCalled() }) it('授权成功但换 code 失败:抛错且不发起登录请求', async () => { - mockGetUserProfile.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' }) + mockGetUserInfo.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' }) mockLogin.mockRejectedValue({ errMsg: 'login:fail' }) await expect(completeUserInfoLogin('o-need')).rejects.toThrow('获取微信登录凭证失败,请重试') expect(mockWxMiniappLogin).not.toHaveBeenCalled()