自2023年9月15日起,对于涉及处理用户个人信息的小程序开发者,微信要求,仅当开发者主动向平台同步用户已阅读并同意了小程序的隐私保护指引等信息处理规则后,方可调用微信提供的隐私接口。
相关公告见:关于小程序隐私保护指引设置的公告 | 微信开放社区
公告里已经介绍了相关流程,具体可以参考小程序隐私协议开发指南 | 微信开放文档。这里不再赘述。下面我们将着重谈一下代码实现。
特别需要注意的两点一个是button 的两个optype:agreePrivacyAuthorization和getPhoneNumber(或者getRealtimePhoneNumber)可以耦合使用。
一个是对于 <input type="nickname"> 组件,由于 <input> 的特殊性,如果用户未同意隐私协议,则<input type="nickname">
聚焦时不会触发 onNeedPrivacyAuthorization 事件,而是降级为 <input type="text"> 。
项目中实际登录按钮的demo:
<button class="loginBtn" open-type="getPhoneNumber|agreePrivacyAuthorization" @getphonenumber="getPhoneNumber" @agreeprivacyauthorization="handleAgreePrivacyAuthorization">立即登录</button>
而demo中的handleAgreePrivacyAuthorization函数里不需要写任何代码,就可以登录和隐私授权都搞定
相关公告见:关于小程序隐私保护指引设置的公告 | 微信开放社区
被动触发隐私协议
首先,我们要知道的一点是如果用户没有同意过隐私协议,调用某些API(具体参看:小程序用户隐私保护指引内容介绍 | 微信开放文档)是会触发隐私协议弹窗的,这种叫触发式隐私协议。如果用户同意,这个API调用还能继续执行,否则就会报失败。所以我们要做的是在这个时候,在当前页面弹出我们自定义的隐私协议alert,并且接收resolve。然后在用户同意和拒绝的时候调用这个resolve。具体代码如下:
// page.wxml
<view wx:if="{{showPrivacy}}">
<view>隐私弹窗内容....</view>
<button id="agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyAuthorization">同意</button>
</view>
// page.js
Page({
data: {
showPrivacy: false
},
onLoad() {
wx.onNeedPrivacyAuthorization(resolve => {
// 需要用户同意隐私授权时
// 弹出开发者自定义的隐私授权弹窗
this.setData({
showPrivacy: true
})
this.resolvePrivacyAuthorization = resolve
})
wx.getUserProfile({
success: console.log,
fail: console.error
})
},
handleAgreePrivacyAuthorization() {
// 用户点击同意按钮后
this.resolvePrivacyAuthorization({ buttonId: 'agree-btn', event: 'agree' })
// 用户点击同意后,开发者调用 resolve({ buttonId: 'agree-btn', event: 'agree' }) 告知平台用户已经同意,参数传同意按钮的id。为确保用户有同意的操作,基础库在 resolve 被调用后,会去检查对应的同意按钮有没有被点击过。检查通过后,相关隐私接口会继续调用
// 用户点击拒绝后,开发者调用 resolve({ event:'disagree' }) 告知平台用户已经拒绝
}
})
这里同意的按钮必须使用系统提供的这个写法,需要指定一个id和回调方法。拒绝的按钮没有什么要求。这里务必要保存resolve,并且在同意和拒绝中调用它,否则触发隐私协议的API的成功和失败回调就不会走。这里有个问题,可能多个页面都有API会触发,但是onNeedPrivacyAuthorization只能注册一个,前面注册的会被后面的覆盖。所以,如果把注册方法写在load里可能造成,页面回来的时候就不会触发了。因此建议这个注册放在show的时候注册。关于这个API的更多内容可以查看:wx.onNeedPrivacyAuthorization(function listener) | 微信开放文档
隐私协议的弹窗是我们自定义的,里面的文本建议根据官方的例子来写,要获取指引的名字,可以通过wx.getPrivacySetting这个API。点击指引前往查看隐私协议的页面,可以我们自己写,也可以用微信官方提供的页面,只需要调用接口wx.openPrivacyContract就行。注意,阅读隐私协议不是必须的,所以你可以强制要求用户前往阅读,也可以什么都不做。
如果遇到以下情景:用户点击确认隐私后再执行后续隐私代码,可以把隐私代码放在wx.requirePrivacyAuthorize里面,因为:
主动式隐私协议
前面我们讲的触发式隐私协议,相对比较麻烦,要去找到这些敏感接口调用的页面,然后全都处理一下。还有一种是主动式隐私协议。主动式隐私协议,就是你在关键入口,主动弹出这个协议窗口,让用户去同意或者拒绝。这时候就没有resolve了。但是这样做有个问题,如果用户点了同意,那么后续这些API都会调用成功,用户和开发者都很高兴。但是万一用户拒绝,如果你没有注册onNeedPrivacyAuthorization并进行适当处理,那后续调用都会失败。这时候偷懒的做法是,用户拒绝后就退出小程序,但是这种做法体验不佳。我们应该尽量让用户能使用其他功能,毕竟这些涉及的API可能我们并不太关心,譬如说用户只是不能上传下载图片,而这些并非我们小程序的核心功能。
组件实现
从用户体验上来讲,对于使用相关API比较多的小程序,为了避免遗漏和一些特殊场景,建议在入口主动弹出隐私协议。对于用量较少的小程序,可以采用触发式隐私协议。无论我们采取何种做法,我们都将其封装为一个组件。
<template>
<div>
<u-popup :show="TCshow" mode="center" overlayStyle="{background:'#6B6B6B'}" @close="popClose" round="20rpx" :safeAreaInsetBottom="false" overlayOpacity="0.5" @open="popOpen"
:closeOnClickOverlay="false" :customStyle="{'width':'80%'}">
<div style="padding: 30rpx 40rpx;">
<div class="box">
<div class="title">妙智健康小程序:</div>
<div class="content">
<text>在你使用【妙智健康小程序】服务之前,请仔细阅读<text class="yinsi" @click="goyinsi">《发券小助手隐私保护指引》</text>如你同意《妙智健康小程序隐私保护指引》,请点击同意开始使用【妙智健康小程序】。</text>
</div>
</div>
<div class="flex justify-center margin-top">
<div class="cancelBtn flex justify-center align-center" @click="cancel">取消</div>
<!-- 发现只要加了这个button 这个弹窗的所有点击事件都会被 handleAgreePrivacyAuthorization 覆盖 最后发现是css属性 all:initial导致的 -->
<div>
<button class="confirmBtn " id="agree-btn" open-type="agreePrivacyAuthorization" @agreeprivacyauthorization.stop="handleAgreePrivacyAuthorization">同意</button>
</div>
</div>
</div>
</u-popup>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
data() {
return {
TCshow: null,
}
},
props: {
show: {
type: Boolean,
default: false
},
cancelFn: {
type: Function,
default: () => {
return () => {}
}
},
},
watch: {
show: {
immediate: true,
handler(val) {
this.TCshow = val;
}
}
},
components: {},
computed: {
...mapState(["hasLogin", "yinsiBtnFn"])
},
onLoad() {
},
onShow() {
},
methods: {
popClose() {
this.TCshow = false
},
popOpen() {
},
cancel() {
console.log("拒绝授权");
console.log(typeof this.yinsiBtnFn);
if (typeof this.yinsiBtnFn === 'function') {
this.yinsiBtnFn({ event: 'disagree' })
}
this.cancelFn() //父级传递的
this.$emit('closePop', '')
this.TCshow = false
},
handleAgreePrivacyAuthorization() {
console.log("同意授权");
if (typeof this.yinsiBtnFn === 'function') {
this.yinsiBtnFn({ buttonId: 'agree-btn', event: 'agree' })
}
this.TCshow = false
},
goyinsi() {
uni.showLoading({
title: '加载中...'
})
wx.openPrivacyContract({
success: () => {}, // 打开成功
fail: () => {}, // 打开失败
complete: () => {
uni.hideLoading();
}
})
},
}
}
</script>
<style lang="scss" scoped>
.box {}
.title {
font-size: 32rpx;
font-weight: 500;
margin-bottom: 24rpx;
}
.content {
padding: 0 20rpx;
font-size: 32rpx;
letter-spacing: 5rpx;
line-height: 53rpx;
}
.yinsi {
color: #0086ec;
}
.cancelBtn {
width: 168rpx;
height: 76rpx;
background-color: #f2f2f2;
color: #83cb8f;
margin-right: 60rpx;
border-radius: 10rpx;
}
.confirmBtn {
// all: initial;
width: 168rpx;
height: 76rpx;
background-color: #54be6b;
color: #fff;
border-radius: 10rpx;
display: flex;
justify-content: center;
align-items: center;
}
</style>
原生组件封装:
<view class="privacy" wx:if="{{showPrivacy}}">
<view class="content">
<view class="title">隐私保护指引</view>
<view class="des">
在使用当前小程序服务之前,请仔细阅读<text class="link" bind:tap="openPrivacyContract">{{privacyContractName}}</text>。如你同意{{privacyContractName}},请点击“同意”开始使用。
</view>
<view class="btns">
<button class="item reject" bind:tap="refusePrivacy">拒绝</button>
<button id="agree-btn" class="item agree" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyAuthorization">同意</button>
</view>
</view>
</view>
.privacy {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0, 0, 0, .5);
z-index: 9999999;
display: flex;
align-items: center;
justify-content: center;
}
.content {
width: 632rpx;
padding: 48rpx;
box-sizing: border-box;
background: #fff;
border-radius: 16rpx;
}
.content .title {
text-align: center;
color: #333;
font-weight: bold;
font-size: 32rpx;
}
.content .des {
font-size: 26rpx;
color: #666;
margin-top: 40rpx;
text-align: justify;
line-height: 1.6;
}
.content .des .link {
color: #07c160;
text-decoration: underline;
}
.btns {
margin-top: 48rpx;
display: flex;
}
.btns .item {
justify-content: space-between;
width: 244rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
box-sizing: border-box;
border: none;
}
.btns .reject {
background: #f4f4f5;
color: #909399;
}
.btns .agree {
background: #07c160;
color: #fff;
}
在组件实现里,我们声明了三个属性,forceShow用于主动式隐私协议,设置true的时候可以强制显示。false的时候,则组件显示由注册回调触发,是触发式隐式协议。forceRead控制用户是否需要强制阅读协议。exitOnRefuse指当用户拒绝的时候,是否需要强制退出小程序。我们在页面show的时候,注册隐私协议触发回调,调用getPrivacySetting获取协议的名称。这里用到了组件的pageLifetimes。后面就是实现打开协议,同意和拒绝的回调。同意和拒绝的回调里,我们要判断是否保存了resolve,有的话是触发式的,需要进行调用,调用完了立刻将其清空。因为我们能区分这两种隐私协议,因此我们在拒绝的时候,显示了不同的错误提示:对于主动式的,我们就说部分功能不可用。对于触发式的,我们就说该功能不可用。