feat(login): 接续 noMobile 手机号授权注册步骤,登录链路全量调试日志
This commit is contained in:
parent
46c517ff42
commit
96c2255c10
|
|
@ -34,9 +34,10 @@ export interface WxLoginResult {
|
||||||
openId?: string // 首次登录返回,需本地缓存
|
openId?: string // 首次登录返回,需本地缓存
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录响应联合体:Message === 'needUserInfo' 时为绑定凭证形态(needUserInfo 由 http 层标记)
|
// 登录响应联合体:Message 子状态由 http 层附加到 message 字段
|
||||||
|
// ''/undefined = 完整登录;'needUserInfo' = 需头像昵称授权;'noMobile' = 需手机号授权
|
||||||
export type WxMiniappLoginResponse = WxLoginResult &
|
export type WxMiniappLoginResponse = WxLoginResult &
|
||||||
Partial<WxNeedUserInfoResult> & { needUserInfo?: boolean }
|
Partial<WxNeedUserInfoResult> & { message?: string }
|
||||||
|
|
||||||
export function wxMiniappLogin(params: WxMiniappLoginParams): Promise<WxMiniappLoginResponse> {
|
export function wxMiniappLogin(params: WxMiniappLoginParams): Promise<WxMiniappLoginResponse> {
|
||||||
return request.post<WxLoginResult>(
|
return request.post<WxLoginResult>(
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ export interface RequestConfig {
|
||||||
export interface ApiEnvelope<T> {
|
export interface ApiEnvelope<T> {
|
||||||
Code: string
|
Code: string
|
||||||
Message?: string
|
Message?: string
|
||||||
Content: T & { needUserInfo?: boolean }
|
// message 为 http 层附加的 200 内子状态(needUserInfo/noMobile 等)
|
||||||
|
Content: T & { message?: string }
|
||||||
RecordCount?: number
|
RecordCount?: number
|
||||||
PageIndex?: number
|
PageIndex?: number
|
||||||
PageSize?: number
|
PageSize?: number
|
||||||
|
|
@ -83,9 +84,12 @@ export async function request<T>(
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Taro 网络 API reject 的是 { errMsg } 形状对象而非 Error 实例,需兼容读取以便透出真实原因
|
// Taro 网络 API reject 的是 { errMsg } 形状对象而非 Error 实例,需兼容读取以便透出真实原因
|
||||||
const raw = e instanceof Error ? e.message : (e as { errMsg?: string })?.errMsg
|
const raw = e instanceof Error ? e.message : (e as { errMsg?: string })?.errMsg
|
||||||
|
console.error(`[http] ${method} ${url} 网络失败:`, raw)
|
||||||
throw new ApiError(raw || '网络异常')
|
throw new ApiError(raw || '网络异常')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[http] ${method} ${url} ←`, res.statusCode, res.data)
|
||||||
|
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
if (res.statusCode === 401) {
|
if (res.statusCode === 401) {
|
||||||
redirectToLogin(tokenAtStart)
|
redirectToLogin(tokenAtStart)
|
||||||
|
|
@ -108,8 +112,12 @@ export async function request<T>(
|
||||||
if (bizCode !== '200') {
|
if (bizCode !== '200') {
|
||||||
throw new BusinessError(body.Message || '业务错误', bizCode)
|
throw new BusinessError(body.Message || '业务错误', bizCode)
|
||||||
}
|
}
|
||||||
// needUserInfo 是 200 成功内的子状态(Message 携带),附到返回值供调用方分支处理
|
// 200 成功内的子状态(Message 携带,如 needUserInfo/noMobile),附到返回值供调用方分支处理
|
||||||
if (body.Message === 'needUserInfo') body.Content.needUserInfo = true
|
const subState = body.Message
|
||||||
|
if (subState && subState !== '') {
|
||||||
|
console.log(`[http] ${url} 响应 Message=${subState}`, body.Content)
|
||||||
|
;(body.Content as Record<string, unknown>).message = subState
|
||||||
|
}
|
||||||
return body.Content
|
return body.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,27 @@
|
||||||
margin-top: 32px;
|
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 {
|
.submitText {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text, Button } from '@tarojs/components'
|
||||||
import { useDidShow } from '@tarojs/taro'
|
import { useDidShow } from '@tarojs/taro'
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks'
|
import { useAppDispatch, useAppSelector } from '@/store/hooks'
|
||||||
import { setAuth } from '@/store/authSlice'
|
import { setAuth } from '@/store/authSlice'
|
||||||
import { performWxLogin, completeUserInfoLogin, isNeedUserInfo } from './wxLoginFlow'
|
import {
|
||||||
|
performWxLogin,
|
||||||
|
completeUserInfoLogin,
|
||||||
|
completeMobileLogin,
|
||||||
|
getLoginSubState,
|
||||||
|
type LoginSubState
|
||||||
|
} from './wxLoginFlow'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { ROUTES } from '@/constants/routes'
|
import { ROUTES } from '@/constants/routes'
|
||||||
import { reLaunch } from '@/utils/navigation'
|
import { reLaunch } from '@/utils/navigation'
|
||||||
|
|
@ -14,52 +20,84 @@ export default function Login() {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const token = useAppSelector((s) => s.auth.token)
|
const token = useAppSelector((s) => s.auth.token)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
// 首次登录 needUserInfo 中间态:持有 openId 等待用户授权补全
|
// 注册中间态:needUserInfo(待头像昵称授权)→ noMobile(待手机号授权)→ null(初始/完成)
|
||||||
const [pendingAuth, setPendingAuth] = useState<string | null>(null)
|
const [subState, setSubState] = useState<LoginSubState | null>(null)
|
||||||
|
const [openId, setOpenId] = useState('')
|
||||||
|
|
||||||
// 已有 token 直接进首页(本期不做 checkLogin 校验,spec D5)
|
// 已有 token 直接进首页(本期不做 checkLogin 校验,spec D5)
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
if (token && !loading) reLaunch(ROUTES.HOME)
|
if (token && !loading) reLaunch(ROUTES.HOME)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleWxLogin = async () => {
|
// 完整登录态校验与落地;未完成时推进 subState
|
||||||
if (loading) return
|
const applyResult = async (result: Awaited<ReturnType<typeof performWxLogin>>) => {
|
||||||
setLoading(true)
|
const state = getLoginSubState(result)
|
||||||
try {
|
if (state !== 'complete') {
|
||||||
const result = await performWxLogin()
|
console.log('[login-page] 注册未完成,推进到', state)
|
||||||
if (isNeedUserInfo(result)) {
|
if (result.openId) setOpenId(result.openId)
|
||||||
// 首次登录:后端要求补授权,弹授权按钮等用户点击
|
setSubState(state)
|
||||||
setPendingAuth(result.openId ?? '')
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!result.code || !result.userInfo) throw new Error('登录响应缺少凭证')
|
if (!result.code || !result.userInfo) throw new Error('登录响应缺少凭证')
|
||||||
|
console.log('[login-page] 登录完成,token 已落地')
|
||||||
|
setSubState(null)
|
||||||
dispatch(setAuth({ token: result.code, userInfo: result.userInfo }))
|
dispatch(setAuth({ token: result.code, userInfo: result.userInfo }))
|
||||||
await reLaunch(ROUTES.HOME)
|
await reLaunch(ROUTES.HOME)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWxLogin = async () => {
|
||||||
|
if (loading) return
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
applyResult(await performWxLogin())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// code 失效/已用等场景:toast 后端 Message,用户重按按钮重新 Taro.login(code 一次性,重试安全)
|
// code 失效/已用等场景:toast 后端 Message,用户重按按钮重新 Taro.login(code 一次性,重试安全)
|
||||||
|
console.error('[login-page] performWxLogin 失败', e)
|
||||||
toast.error(e instanceof Error && e.message ? e.message : '登录失败')
|
toast.error(e instanceof Error && e.message ? e.message : '登录失败')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// needUserInfo:getUserProfile 授权头像昵称
|
||||||
const handleAuthUserInfo = async () => {
|
const handleAuthUserInfo = async () => {
|
||||||
if (loading || !pendingAuth) return
|
if (loading || !openId) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const result = await completeUserInfoLogin(pendingAuth)
|
applyResult(await completeUserInfoLogin(openId))
|
||||||
if (isNeedUserInfo(result)) throw new Error('授权后仍未获取到用户信息')
|
|
||||||
if (!result.code || !result.userInfo) throw new Error('登录响应缺少凭证')
|
|
||||||
setPendingAuth(null)
|
|
||||||
dispatch(setAuth({ token: result.code, userInfo: result.userInfo }))
|
|
||||||
await reLaunch(ROUTES.HOME)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 授权失败/取消:保留待授权态,用户可重试(getUserProfile 每次都需用户确认)
|
// 授权失败/取消:保留待授权态,用户可重试(getUserProfile 每次都需用户确认)
|
||||||
|
console.error('[login-page] completeUserInfoLogin 失败', e)
|
||||||
toast.error(e instanceof Error && e.message ? e.message : '授权失败')
|
toast.error(e instanceof Error && e.message ? e.message : '授权失败')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noMobile:手机号授权按钮回调(Button open-type=getPhoneNumber)
|
||||||
|
const handleGetPhoneNumber = async (e: {
|
||||||
|
detail: { iv: string; encryptedData: string; errMsg?: string }
|
||||||
|
}) => {
|
||||||
|
console.log('[login-page] 手机号授权回调', e.detail.errMsg)
|
||||||
|
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 (
|
return (
|
||||||
<View className={styles.page}>
|
<View className={styles.page}>
|
||||||
<View className={styles.hero}>
|
<View className={styles.hero}>
|
||||||
|
|
@ -70,13 +108,21 @@ export default function Login() {
|
||||||
<Text className={styles.slogan}>一起守护家人</Text>
|
<Text className={styles.slogan}>一起守护家人</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className={styles.form}>
|
<View className={styles.form}>
|
||||||
{pendingAuth === null ? (
|
{subState === 'noMobile' ? (
|
||||||
<View className={styles.submit} onClick={handleWxLogin}>
|
// 手机号授权必须用原生 Button open-type,微信弹官方授权框
|
||||||
<Text className={styles.submitText}>{loading ? '登录中...' : '微信一键登录'}</Text>
|
<Button
|
||||||
</View>
|
className={styles.submitBtn}
|
||||||
|
openType="getPhoneNumber"
|
||||||
|
onGetPhoneNumber={handleGetPhoneNumber}
|
||||||
|
>
|
||||||
|
{submitText()}
|
||||||
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<View className={styles.submit} onClick={handleAuthUserInfo}>
|
<View
|
||||||
<Text className={styles.submitText}>{loading ? '授权中...' : '授权头像昵称,完成登录'}</Text>
|
className={styles.submit}
|
||||||
|
onClick={subState === 'needUserInfo' ? handleAuthUserInfo : handleWxLogin}
|
||||||
|
>
|
||||||
|
<Text className={styles.submitText}>{submitText()}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,18 @@ function resolveAppId(): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断响应是否为"首次登录需补授权"形态(Message=needUserInfo 时 http 层打的标记)
|
// 登录步骤子状态(http 层把 200 内 Message 附加到返回值 message 字段)
|
||||||
export function isNeedUserInfo(res: WxMiniappLoginResponse): boolean {
|
export type LoginSubState = 'needUserInfo' | 'noMobile' | 'complete'
|
||||||
return res.needUserInfo === true
|
|
||||||
|
export function getLoginSubState(res: WxMiniappLoginResponse): LoginSubState {
|
||||||
|
if (res.message === 'needUserInfo') return 'needUserInfo'
|
||||||
|
if (res.message === 'noMobile') return 'noMobile'
|
||||||
|
return 'complete'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 微信一键登录主流程:
|
// 微信一键登录主流程:
|
||||||
// Taro.login 换一次性 code → 组装 appId/openId → 调 miniapp/login → 缓存响应中的 openId
|
// Taro.login 换一次性 code → 组装 appId/openId → 调 miniapp/login → 缓存响应中的 openId
|
||||||
// 首次登录后端返回 needUserInfo(仅 openId/bindKey,无 token),需二次授权后重调接口补全
|
// 首次登录后端可能分步返回 needUserInfo / noMobile(仅绑定凭证,无 token),逐步授权后重调补全
|
||||||
export async function performWxLogin(): Promise<WxMiniappLoginResponse> {
|
export async function performWxLogin(): Promise<WxMiniappLoginResponse> {
|
||||||
// Taro.login reject 时返回 { errMsg } 形状对象而非 Error,此处归一化为带 message 的 Error
|
// Taro.login reject 时返回 { errMsg } 形状对象而非 Error,此处归一化为带 message 的 Error
|
||||||
let code: string
|
let code: string
|
||||||
|
|
@ -31,12 +35,34 @@ export async function performWxLogin(): Promise<WxMiniappLoginResponse> {
|
||||||
}
|
}
|
||||||
if (!code) throw new Error('获取微信登录凭证失败')
|
if (!code) throw new Error('获取微信登录凭证失败')
|
||||||
const openId = storage.get<string>(WX_OPENID_STORAGE_KEY) ?? undefined
|
const openId = storage.get<string>(WX_OPENID_STORAGE_KEY) ?? undefined
|
||||||
|
console.log('[login] performWxLogin 请求', { appId: resolveAppId(), code, openId })
|
||||||
const result = await wxMiniappLogin({ appId: resolveAppId(), code, openId })
|
const result = await wxMiniappLogin({ appId: resolveAppId(), code, openId })
|
||||||
if (result.openId) storage.set(WX_OPENID_STORAGE_KEY, result.openId)
|
if (result.openId) storage.set(WX_OPENID_STORAGE_KEY, result.openId)
|
||||||
|
console.log('[login] performWxLogin 结果', getLoginSubState(result), result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// needUserInfo 二次授权:getUserProfile 拿 iv/encryptedData → 换新 code 重调登录接口补全用户资料
|
// 重调登录接口的公共尾部:换新 code(一次性)→ 组装参数 → 调用 → 缓存 openId
|
||||||
|
async function reloginWith(
|
||||||
|
openId: string,
|
||||||
|
extra: { iv?: string; encryptedData?: string }
|
||||||
|
): Promise<WxMiniappLoginResponse> {
|
||||||
|
let code: string
|
||||||
|
try {
|
||||||
|
const r = await Taro.login()
|
||||||
|
code = r.code
|
||||||
|
} catch {
|
||||||
|
throw new Error('获取微信登录凭证失败,请重试')
|
||||||
|
}
|
||||||
|
const params = { appId: resolveAppId(), code, openId, ...extra }
|
||||||
|
console.log('[login] 重调登录接口', params)
|
||||||
|
const result = await wxMiniappLogin(params)
|
||||||
|
if (result.openId) storage.set(WX_OPENID_STORAGE_KEY, result.openId)
|
||||||
|
console.log('[login] 重调结果', getLoginSubState(result), result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// needUserInfo 二次授权:getUserProfile 拿 iv/encryptedData → 重调登录接口补用户资料
|
||||||
export async function completeUserInfoLogin(openId: string): Promise<WxMiniappLoginResponse> {
|
export async function completeUserInfoLogin(openId: string): Promise<WxMiniappLoginResponse> {
|
||||||
let profile: { iv: string; encryptedData: string }
|
let profile: { iv: string; encryptedData: string }
|
||||||
try {
|
try {
|
||||||
|
|
@ -45,22 +71,16 @@ export async function completeUserInfoLogin(openId: string): Promise<WxMiniappLo
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e instanceof Error ? e.message : (e as { errMsg?: string })?.errMsg || '授权失败')
|
throw new Error(e instanceof Error ? e.message : (e as { errMsg?: string })?.errMsg || '授权失败')
|
||||||
}
|
}
|
||||||
|
return reloginWith(openId, profile)
|
||||||
let code: string
|
}
|
||||||
try {
|
|
||||||
const r = await Taro.login()
|
// noMobile 三次授权:手机号授权按钮回调(open-type=getPhoneNumber)拿 iv/encryptedData → 重调登录接口
|
||||||
code = r.code
|
export async function completeMobileLogin(
|
||||||
} catch {
|
openId: string,
|
||||||
throw new Error('获取微信登录凭证失败,请重试')
|
detail: { iv: string; encryptedData: string; errMsg?: string }
|
||||||
}
|
): Promise<WxMiniappLoginResponse> {
|
||||||
|
if (!detail.iv || !detail.encryptedData) {
|
||||||
const result = await wxMiniappLogin({
|
throw new Error(detail.errMsg || '手机号授权失败')
|
||||||
appId: resolveAppId(),
|
}
|
||||||
code,
|
return reloginWith(openId, { iv: detail.iv, encryptedData: detail.encryptedData })
|
||||||
openId,
|
|
||||||
iv: profile.iv,
|
|
||||||
encryptedData: profile.encryptedData
|
|
||||||
})
|
|
||||||
if (result.openId) storage.set(WX_OPENID_STORAGE_KEY, result.openId)
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -198,12 +198,21 @@ describe('登录态失效踢回登录页', () => {
|
||||||
await expect(request.get('/demo')).rejects.toBeInstanceOf(ApiError)
|
await expect(request.get('/demo')).rejects.toBeInstanceOf(ApiError)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Message='needUserInfo' 的 200 响应在 Content 上打标记", async () => {
|
it("200 响应的 Message 子状态(needUserInfo 等)附加到返回值 message 字段", async () => {
|
||||||
mockRequest.mockResolvedValue({
|
mockRequest.mockResolvedValue({
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
data: { Code: '200', Message: 'needUserInfo', Content: { openId: 'o-1', bindKey: 'b-1' } }
|
data: { Code: '200', Message: 'needUserInfo', Content: { openId: 'o-1', bindKey: 'b-1' } }
|
||||||
})
|
})
|
||||||
const res = await request.get<{ openId: string }>('/demo')
|
const res = await request.get<{ openId: string }>('/demo')
|
||||||
expect(res).toMatchObject({ openId: 'o-1', bindKey: 'b-1', needUserInfo: true })
|
expect(res).toMatchObject({ openId: 'o-1', bindKey: 'b-1', message: 'needUserInfo' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Message='noMobile' 同样附加(手机号授权子状态)", async () => {
|
||||||
|
mockRequest.mockResolvedValue({
|
||||||
|
statusCode: 200,
|
||||||
|
data: { Code: '200', Message: 'noMobile', Content: { bindKey: 'b-2' } }
|
||||||
|
})
|
||||||
|
const res = await request.get('/demo')
|
||||||
|
expect(res).toMatchObject({ bindKey: 'b-2', message: 'noMobile' })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ vi.mock('@/utils/storage', () => ({
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import { performWxLogin, completeUserInfoLogin, isNeedUserInfo } from '@/pages/login/wxLoginFlow'
|
import { performWxLogin, completeUserInfoLogin, completeMobileLogin, getLoginSubState } from '@/pages/login/wxLoginFlow'
|
||||||
|
|
||||||
const result = (over: Record<string, unknown> = {}) => ({
|
const result = (over: Record<string, unknown> = {}) => ({
|
||||||
userInfo: {
|
userInfo: {
|
||||||
|
|
@ -34,11 +34,17 @@ const result = (over: Record<string, unknown> = {}) => ({
|
||||||
...over
|
...over
|
||||||
})
|
})
|
||||||
|
|
||||||
// 首次登录 needUserInfo 响应形态(http 层在 Content 上打的标记)
|
// 首次登录 needUserInfo 响应形态(http 层把 Message 附加到 message 字段)
|
||||||
const needInfoResult = () => ({
|
const needInfoResult = () => ({
|
||||||
openId: 'o-need',
|
openId: 'o-need',
|
||||||
bindKey: 'bind-1',
|
bindKey: 'bind-1',
|
||||||
needUserInfo: true as const
|
message: 'needUserInfo' as const
|
||||||
|
})
|
||||||
|
|
||||||
|
// noMobile 响应形态(授权头像后仍未绑定手机号)
|
||||||
|
const noMobileResult = () => ({
|
||||||
|
bindKey: 'bind-2',
|
||||||
|
message: 'noMobile' as const
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -115,13 +121,17 @@ describe('needUserInfo 首次登录分支', () => {
|
||||||
it('performWxLogin 返回 needUserInfo 形态时缓存 openId 并原样返回', async () => {
|
it('performWxLogin 返回 needUserInfo 形态时缓存 openId 并原样返回', async () => {
|
||||||
mockWxMiniappLogin.mockResolvedValue(needInfoResult())
|
mockWxMiniappLogin.mockResolvedValue(needInfoResult())
|
||||||
const res = await performWxLogin()
|
const res = await performWxLogin()
|
||||||
expect(isNeedUserInfo(res)).toBe(true)
|
expect(getLoginSubState(res)).toBe('needUserInfo')
|
||||||
expect(res.openId).toBe('o-need')
|
expect(res.openId).toBe('o-need')
|
||||||
expect(mem.get('wx_openid')).toBe('o-need')
|
expect(mem.get('wx_openid')).toBe('o-need')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('isNeedUserInfo 对完整登录响应返回 false', () => {
|
it('getLoginSubState 对完整登录响应返回 complete', () => {
|
||||||
expect(isNeedUserInfo(result())).toBe(false)
|
expect(getLoginSubState(result())).toBe('complete')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getLoginSubState 对 noMobile 响应返回 noMobile', () => {
|
||||||
|
expect(getLoginSubState(noMobileResult() as never)).toBe('noMobile')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -153,3 +163,25 @@ describe('completeUserInfoLogin 二次授权', () => {
|
||||||
expect(mockWxMiniappLogin).not.toHaveBeenCalled()
|
expect(mockWxMiniappLogin).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('completeMobileLogin 手机号授权', () => {
|
||||||
|
it('按钮回调 detail 带 iv/encryptedData → 重调登录接口补手机号', async () => {
|
||||||
|
mockWxMiniappLogin.mockResolvedValue(result())
|
||||||
|
const res = await completeMobileLogin('o-need', { iv: 'iv-ph', encryptedData: 'enc-ph' })
|
||||||
|
expect(res.code).toBe('tk-abc')
|
||||||
|
expect(mockWxMiniappLogin).toHaveBeenCalledWith({
|
||||||
|
appId: 'wx-appid-live',
|
||||||
|
code: 'wx-code-1',
|
||||||
|
openId: 'o-need',
|
||||||
|
iv: 'iv-ph',
|
||||||
|
encryptedData: 'enc-ph'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('用户拒绝手机号授权(detail 无加密数据):抛 errMsg 且不发起请求', async () => {
|
||||||
|
await expect(
|
||||||
|
completeMobileLogin('o-need', { iv: '', encryptedData: '', errMsg: 'getPhoneNumber:fail user deny' })
|
||||||
|
).rejects.toThrow('getPhoneNumber:fail user deny')
|
||||||
|
expect(mockWxMiniappLogin).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue