fix(login): needUserInfo 授权改用 wx.getUserInfo(后端文档约定),暂缓手机号授权流程
This commit is contained in:
parent
026578dfbd
commit
87e4031fff
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<LoginSubState | null>(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<ReturnType<typeof performWxLogin>>) => {
|
||||
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<string>(WX_OPENID_STORAGE_KEY) ?? openId
|
||||
result.openId ?? storage.get<string>(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 (
|
||||
<View className={styles.page}>
|
||||
<View className={styles.hero}>
|
||||
|
|
@ -121,23 +87,14 @@ export default function Login() {
|
|||
<Text className={styles.slogan}>一起守护家人</Text>
|
||||
</View>
|
||||
<View className={styles.form}>
|
||||
{subState === 'noMobile' ? (
|
||||
// 手机号授权必须用原生 Button open-type,微信弹官方授权框
|
||||
<Button
|
||||
className={styles.submitBtn}
|
||||
openType="getPhoneNumber"
|
||||
onGetPhoneNumber={handleGetPhoneNumber}
|
||||
>
|
||||
{submitText()}
|
||||
</Button>
|
||||
) : (
|
||||
<View
|
||||
className={styles.submit}
|
||||
onClick={subState === 'needUserInfo' ? handleAuthUserInfo : handleWxLogin}
|
||||
>
|
||||
<Text className={styles.submitText}>{submitText()}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
className={styles.submit}
|
||||
onClick={needAuth ? handleAuthUserInfo : handleWxLogin}
|
||||
>
|
||||
<Text className={styles.submitText}>
|
||||
{loading ? '登录中...' : needAuth ? '授权用户信息,继续' : '微信一键登录'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<WxMiniappLoginResponse> {
|
||||
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<WxMiniappLo
|
|||
}
|
||||
|
||||
// noMobile 三次授权:手机号授权按钮回调(open-type=getPhoneNumber)→ 重调登录接口
|
||||
// detail 两种形态:新版含 code(动态令牌,后端可直连微信换取手机号);
|
||||
// 旧版含 iv/encryptedData(需与 session_key 解密,依赖同一 code 会话)
|
||||
// 暂缓接入(后端读取字段未定,等联调确认);保留实现以便接续
|
||||
export async function completeMobileLogin(
|
||||
openId: string,
|
||||
detail: { iv: string; encryptedData: string; code?: string; errMsg?: string }
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|||
|
||||
const mockLogin = vi.hoisted(() => 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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue