feat(privacy): 修复微信隐私协议授权流程
- 新增全局隐私协议授权管理器 privacyManager,统一管理弹窗显示和授权结果 - 用 PrivacyModal 组件替代原先 Taro.showModal 实现,使用 open-type="agreePrivacyAuthorization" 的 Button 触发微信认可的隐私授权 - 在 app.tsx 中注册 onNeedPrivacyAuthorization 回调,通过 privacyManager.show 弹出隐私弹窗 - 登录和注册页面 useDidShow 钩子调用 ensurePrivacyAuthorized 函数预检隐私授权状态,避免授权失败 - 修复登录/注册接口调用,统一使用 request.post + SERVER_API_URL,修正错误接口地址及导入问题 - 新增隐私弹窗样式和逻辑,实现用户同意或拒绝后的正确流程处理 - 解决微信基础库 3.16.1+ 强制隐私协议授权导致敏感 API 调用失败问题
This commit is contained in:
28
src/app.tsx
28
src/app.tsx
@@ -7,6 +7,8 @@ import AppContext from './contexts/AppContext'
|
||||
import { UserProvider } from './contexts/UserContext'
|
||||
import { CartProvider } from './contexts/CartContext'
|
||||
import ErrorBoundary from './components/ErrorBoundary'
|
||||
import PrivacyModal from './components/PrivacyModal'
|
||||
import { privacyManager } from './utils/privacy'
|
||||
import './app.scss'
|
||||
|
||||
type AppProps = {
|
||||
@@ -24,32 +26,15 @@ function App(props: AppProps) {
|
||||
}
|
||||
|
||||
// 微信隐私协议(基础库 3.16.1+ 强制)
|
||||
// 当调用 getPhoneNumber / chooseImage 等敏感 API 且用户未同意隐私协议时,
|
||||
// 微信会触发此回调。我们展示自定义弹窗,用户点击同意后再 resolve,
|
||||
// 否则微信会拒绝后续敏感 API 调用。
|
||||
// 必须使用 open-type="agreePrivacyAuthorization" 的 Button 让用户点击同意,
|
||||
// Taro.showModal 的按钮无法被微信识别为隐私授权,会导致后续敏感 API 仍被拒绝。
|
||||
const wxAny: any = Taro
|
||||
if (typeof wxAny.onNeedPrivacyAuthorization === 'function') {
|
||||
wxAny.onNeedPrivacyAuthorization((resolve: any) => {
|
||||
wxAny.getPrivacySetting({
|
||||
success: (setting: any) => {
|
||||
const privacyContractName = setting?.privacyContractName || '《用户隐私保护指引》'
|
||||
Taro.showModal({
|
||||
title: '隐私协议授权',
|
||||
content: `为提供完整服务,需您同意 ${privacyContractName}。点击同意后可继续使用手机号快捷登录、相册等功能。`,
|
||||
confirmText: '同意',
|
||||
cancelText: '拒绝',
|
||||
confirmColor: '#07c160',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
resolve({ event: 'agree', button: 'agree' })
|
||||
} else {
|
||||
resolve({ event: 'disagree', button: 'disagree' })
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
resolve({ event: 'disagree', button: 'disagree' })
|
||||
},
|
||||
})
|
||||
privacyManager.setPrivacyContractName(setting?.privacyContractName)
|
||||
privacyManager.show(resolve)
|
||||
},
|
||||
fail: () => {
|
||||
// 获取设置失败时,默认 resolve 同意,避免流程阻塞
|
||||
@@ -81,6 +66,7 @@ function App(props: AppProps) {
|
||||
<View className={theme === 'dark' ? 'dark' : ''}>
|
||||
{props.children}
|
||||
</View>
|
||||
<PrivacyModal />
|
||||
</ErrorBoundary>
|
||||
</ConfigProvider>
|
||||
</CartProvider>
|
||||
|
||||
79
src/components/PrivacyModal/index.scss
Normal file
79
src/components/PrivacyModal/index.scss
Normal file
@@ -0,0 +1,79 @@
|
||||
.privacy-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
width: 560px;
|
||||
background-color: #fff;
|
||||
border-radius: 24px;
|
||||
padding: 48px 40px 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 28px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
flex: 1;
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
color: #07c160;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/components/PrivacyModal/index.tsx
Normal file
56
src/components/PrivacyModal/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Button, Text } from '@tarojs/components'
|
||||
import { privacyManager } from '@/utils/privacy'
|
||||
import './index.scss'
|
||||
|
||||
/**
|
||||
* 微信隐私协议授权弹窗(基础库 3.16.1+ 强制)
|
||||
* 必须使用 open-type="agreePrivacyAuthorization" 的 Button 组件,
|
||||
* 用户点击后微信才会真正认为隐私协议已授权,后续敏感 API 才能调用。
|
||||
*/
|
||||
const PrivacyModal = () => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [contractName, setContractName] = useState(privacyManager.getPrivacyContractName())
|
||||
|
||||
useEffect(() => {
|
||||
privacyManager.setShowCallback((show) => {
|
||||
setVisible(show)
|
||||
if (show) {
|
||||
setContractName(privacyManager.getPrivacyContractName())
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<View className='privacy-modal'>
|
||||
<View className='privacy-modal__mask' />
|
||||
<View className='privacy-modal__content'>
|
||||
<Text className='privacy-modal__title'>隐私协议授权</Text>
|
||||
<Text className='privacy-modal__desc'>
|
||||
{`为提供完整服务,需您同意 ${contractName}。点击同意后可继续使用手机号快捷登录、相册等功能。`}
|
||||
</Text>
|
||||
<View className='privacy-modal__footer'>
|
||||
<Button
|
||||
className='privacy-modal__btn'
|
||||
onClick={() => privacyManager.disagree()}
|
||||
>
|
||||
拒绝
|
||||
</Button>
|
||||
<Button
|
||||
className='privacy-modal__btn privacy-modal__btn--primary'
|
||||
openType='agreePrivacyAuthorization'
|
||||
onAgreePrivacyAuthorization={() => privacyManager.agree()}
|
||||
onDisagreePrivacyAuthorization={() => privacyManager.disagree()}
|
||||
>
|
||||
同意
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default PrivacyModal
|
||||
@@ -1,79 +1,47 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
/**
|
||||
* 在调用隐私相关 API / 组件前,确保用户已完成微信隐私协议授权。
|
||||
* 基础库 3.16.1+ 强制要求,未授权会抛 errno:112 / buttonId is wrong 等错误。
|
||||
* 旧基础库或不支持隐私协议的版本直接放行。
|
||||
* 微信隐私协议授权全局管理器
|
||||
* 用于在基础库 3.16.1+ 下处理 onNeedPrivacyAuthorization 回调。
|
||||
* 必须在页面中渲染 <PrivacyModal /> 并绑定本管理器。
|
||||
*/
|
||||
export const ensurePrivacyAuthorized = (): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
const wxAny: any = Taro
|
||||
if (typeof wxAny.requirePrivacyAuthorize !== 'function') {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
if (typeof wxAny.getPrivacySetting === 'function') {
|
||||
wxAny.getPrivacySetting({
|
||||
success: (res: any) => {
|
||||
if (res && res.needAuthorization) {
|
||||
wxAny.requirePrivacyAuthorize({
|
||||
success: () => resolve(),
|
||||
fail: () => resolve(),
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
fail: () => resolve(),
|
||||
})
|
||||
} else {
|
||||
wxAny.requirePrivacyAuthorize({
|
||||
success: () => resolve(),
|
||||
fail: () => resolve(),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开官方隐私协议页面(半屏协议)。
|
||||
* 返回是否成功打开。
|
||||
*/
|
||||
export const openPrivacyContract = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const wxAny: any = Taro
|
||||
if (typeof wxAny.openPrivacyContract !== 'function') {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
wxAny.openPrivacyContract({
|
||||
success: () => resolve(true),
|
||||
fail: () => resolve(false),
|
||||
})
|
||||
})
|
||||
}
|
||||
type PrivacyResolve = (result: { event: 'agree' | 'disagree'; button: string }) => void
|
||||
|
||||
/**
|
||||
* 检查当前用户是否需要同意隐私协议。
|
||||
*/
|
||||
export const checkPrivacyAuthorization = (): Promise<{
|
||||
needAuthorization: boolean
|
||||
isAuthorization: boolean
|
||||
}> => {
|
||||
return new Promise((resolve) => {
|
||||
const wxAny: any = Taro
|
||||
if (typeof wxAny.getPrivacySetting !== 'function') {
|
||||
resolve({ needAuthorization: false, isAuthorization: true })
|
||||
return
|
||||
}
|
||||
wxAny.getPrivacySetting({
|
||||
success: (res: any) => {
|
||||
resolve({
|
||||
needAuthorization: !!res?.needAuthorization,
|
||||
isAuthorization: !!res?.isAuthorization,
|
||||
})
|
||||
},
|
||||
fail: () => resolve({ needAuthorization: false, isAuthorization: true }),
|
||||
})
|
||||
})
|
||||
let currentResolve: PrivacyResolve | null = null
|
||||
let showCallback: ((show: boolean) => void) | null = null
|
||||
let privacyContractName = '《用户隐私保护指引》'
|
||||
|
||||
export const privacyManager = {
|
||||
/** 设置弹窗显示/隐藏回调 */
|
||||
setShowCallback(cb: (show: boolean) => void) {
|
||||
showCallback = cb
|
||||
},
|
||||
|
||||
/** 设置隐私协议名称(从 getPrivacySetting 读取) */
|
||||
setPrivacyContractName(name: string) {
|
||||
privacyContractName = name || '《用户隐私保护指引》'
|
||||
},
|
||||
|
||||
getPrivacyContractName() {
|
||||
return privacyContractName
|
||||
},
|
||||
|
||||
/** 展示隐私协议授权弹窗 */
|
||||
show(resolve: PrivacyResolve) {
|
||||
currentResolve = resolve
|
||||
showCallback?.(true)
|
||||
},
|
||||
|
||||
/** 用户点击同意(由 open-type=agreePrivacyAuthorization 的 button 触发) */
|
||||
agree() {
|
||||
currentResolve?.({ event: 'agree', button: 'agree' })
|
||||
currentResolve = null
|
||||
showCallback?.(false)
|
||||
},
|
||||
|
||||
/** 用户点击拒绝 */
|
||||
disagree() {
|
||||
currentResolve?.({ event: 'disagree', button: 'disagree' })
|
||||
currentResolve = null
|
||||
showCallback?.(false)
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user