Files
lucky_shop/common/js/privacy.js
ZF sun e3e63899d8 feat(privacy): add markSkipped method and H5 skip logic for privacy popup
add markSkipped method to synchronously skip privacy popup, and call it in App.vue created hook for H5 environment when current URL matches skip condition
2026-07-10 14:11:51 +08:00

287 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 全局隐私协议管理器(单例)
*
* 职责:
* 1. 统一判断是否需要弹窗(storage + 平台级隐私状态)
* 2. 通过 uni.$emit('privacy:show') 通知当前页面的 privacy-popup 组件弹窗
* 3. 用户同意后写 storage,拒绝后不写(允许下次再弹)
* 4. 只弹一次:同意后永不再弹
* 5. 事件补偿:若 emit 时无监听者,保留 _pendingShow,页面 mounted 时补偿弹窗
*
* 平台差异:
* H5: 仅查 storage
* 微信小程序: wx.getPrivacySetting() 查平台级隐私状态
* 鸿蒙元服务: has.getPrivacySetting() 查平台级隐私状态
*
* 事件:
* uni.$emit('privacy:show') — 通知当前页面组件弹窗
* uni.$emit('privacy:agreed') — 用户已同意
* uni.$emit('privacy:disagreed') — 用户已拒绝
* uni.$emit('privacy:closeOthers') — 通知其他页面关闭弹窗
*
* 使用:
* App.vue onLaunch (boot完成后): await PrivacyManager.show()
* privacy-popup.vue: mounted 中 uni.$on('privacy:show') + popPendingShow() 补偿
* privacy-popup.vue: handleAgree 中 PrivacyManager.agree()
* privacy-popup.vue: handleDisagree 中 PrivacyManager.disagree()
*/
const STORAGE_KEY = 'privacyAgreed';
// 缓存上一次 getPrivacySetting 的结果, 避免每个组件 mounted 都重新检查
let _lastCheckResult = null;
let _lastCheckTs = 0;
// 内部状态
let _needShow = false; // 是否需要弹窗
let _hasChecked = false; // 是否已执行过 check
let _pendingShow = false; // emit 时无监听者的补偿标志
/**
* 读取 storage 中的同意状态
*/
function getStorageAgreed() {
try {
return !!uni.getStorageSync(STORAGE_KEY);
} catch (e) {
return false;
}
}
/**
* 微信小程序: 查询平台级隐私设置
* @returns {Promise<{needAuthorization: boolean, privacyContractName?: string}>}
*/
function getWxPrivacySetting() {
return new Promise((resolve) => {
// #ifdef MP-WEIXIN
try {
if (wx.getPrivacySetting) {
wx.getPrivacySetting({
success: (res) => {
resolve({
needAuthorization: !!res.needAuthorization,
privacyContractName: res.privacyContractName || ''
});
},
fail: () => resolve({ needAuthorization: !getStorageAgreed() })
});
} else {
// 低版本基础库,回退到 storage
resolve({ needAuthorization: !getStorageAgreed() });
}
} catch (e) {
resolve({ needAuthorization: !getStorageAgreed() });
}
// #endif
// #ifndef MP-WEIXIN
resolve({ needAuthorization: !getStorageAgreed() });
// #endif
});
}
/**
* 快应用: 查询平台级隐私设置
* @returns {Promise<{needAuthorization: boolean, privacyContractName?: string}>}
*/
function getQuickAppPrivacySetting() {
return new Promise((resolve) => {
// #ifdef QUICKAPP-WEBVIEW
try {
const has = require('@system.has');
if (has && has.getPrivacySetting) {
has.getPrivacySetting({
success: (res) => {
resolve({
needAuthorization: !!res.needAuthorization,
privacyContractName: res.privacyContractName || ''
});
},
fail: () => resolve({ needAuthorization: !getStorageAgreed() })
});
return;
}
} catch (e) {}
// #endif
// 非快应用环境: 仅查 storage
resolve({ needAuthorization: !getStorageAgreed() });
});
}
/**
* 鸿蒙元服务: 查询平台级隐私设置
* 鸿蒙元服务的隐私授权由系统自动管理,应用层只需检查 storage
* @returns {Promise<{needAuthorization: boolean, privacyContractName?: string}>}
*/
function getHarmonyPrivacySetting() {
return new Promise((resolve) => {
// #ifdef MP-HARMONY
// 鸿蒙元服务: 隐私授权由系统管理,应用层仅检查 storage
// 系统会在用户首次使用隐私API时自动弹出系统级隐私授权弹窗
resolve({ needAuthorization: !getStorageAgreed() });
// #endif
// #ifndef MP-HARMONY
resolve({ needAuthorization: !getStorageAgreed() });
// #endif
});
}
/**
* 综合查询: 平台级隐私状态 + storage 状态
* 任一判断为"需要授权"则弹窗
* @returns {Promise<{needAuthorization: boolean, privacyContractName?: string}>}
*/
async function checkPrivacy() {
// 微信小程序
// #ifdef MP-WEIXIN
return await getWxPrivacySetting();
// #endif
// 快应用
// #ifdef QUICKAPP-WEBVIEW
return await getQuickAppPrivacySetting();
// #endif
// 鸿蒙元服务
// #ifdef MP-HARMONY
return await getHarmonyPrivacySetting();
// #endif
// H5 / 其他平台: 仅查 storage
// #ifndef MP-WEIXIN || QUICKAPP-WEBVIEW || MP-HARMONY
return { needAuthorization: !getStorageAgreed() };
// #endif
}
const PrivacyManager = {
/**
* 同步检查 storage 中的同意状态(快速判断,不查平台)
* @returns {boolean} true=需要弹窗, false=不需要
*/
check() {
_needShow = !getStorageAgreed();
_hasChecked = true;
return _needShow;
},
/**
* 同步标记本次跳过弹窗(不查 storage、不写 storage、不弹窗)
* 必须在 page mounted 之前调用,避免 onLaunch await 让出事件循环后
* privacy-popup.vue 的 consumePendingShow 走兜底分支把弹窗弹出来
* 用于 H5 浏览器直访特定 URL(子串匹配)时不让隐私弹窗挡住
*/
markSkipped() {
_needShow = false;
_hasChecked = true;
_pendingShow = false;
},
/**
* 触发弹窗(如果需要)
* 由 App.vue onLaunch 在 boot 完成后调用
* 异步查询平台隐私状态,设置 _pendingShow 标志
* 页面组件 mounted 时通过 consumePendingShow() 消费标志并弹窗
* 不直接 emit 事件,避免 emit + 补偿双触发
*
* @param {Object} [options]
* @param {boolean} [options.skip=false] true=本次跳过弹窗(不写 storage,下次正常访问其他页面还会弹)
*/
async show(options = {}) {
// 主动跳过: 不查 storage、不写 storage、不弹窗
// 用于 H5 浏览器直接访问特定 URL(子串匹配)时不让隐私弹窗挡住
if (options && options.skip) {
_needShow = false;
_hasChecked = true;
_pendingShow = false;
return;
}
const _storageAgreed = getStorageAgreed();
const result = await checkPrivacy();
_needShow = result.needAuthorization;
_hasChecked = true;
if (!_needShow) {
return;
}
// 设置待弹窗标志
// 已 mounted 的页面组件通过事件接收
// 未 mounted 的页面组件 mounted 时通过 consumePendingShow 补偿
_pendingShow = true;
// 通知已 mounted 的页面组件(如果有监听者)
try { uni.$emit('privacy:show'); } catch (e) {}
// 若组件已接收事件并弹窗, 会在 handlePrivacyShow 中调 consumePendingShow 清除 _pendingShow
// 若无监听者, _pendingShow 保持 true, 等页面 mounted 时补偿
},
/**
* 页面 mounted 时补偿检查: 若 show() 在页面 mounted 之前 emit 了事件
* 此方法返回 true 并清除标志,页面组件据此自动弹窗
*
* 鸿蒙元服务兼容:
* Lang.boot() 可能 hang, PrivacyManager.show() 可能从未被调用过;
* 此时 _pendingShow 仍是默认 false, 但用户从未同意过隐私, 必须强制弹一次。
* 所以增加 storage 检测: 用户未同意也返回 true (补偿弹窗)。
* @returns {boolean} true=需要弹窗,false=不需要
*/
consumePendingShow() {
if (_pendingShow && _needShow) {
_pendingShow = false;
return true;
}
// 鸿蒙兜底: PrivacyManager.show() 没跑过 + 用户没同意过 storage → 必须弹
// 同一次进程内只补一次,避免隐私弹窗被反复触发
if (!_hasChecked && !_pendingShow && !getStorageAgreed()) {
_pendingShow = false;
_needShow = true;
_hasChecked = true;
return true;
}
_pendingShow = false;
return false;
},
/**
* 用户同意:写 storage,标记不再弹
* 由 privacy-popup.vue 的 handleAgree 调用
*/
agree() {
try { uni.setStorageSync(STORAGE_KEY, true); } catch (e) {}
_needShow = false;
_pendingShow = false;
try { uni.$emit('privacy:agreed'); } catch (e) {}
},
/**
* 用户拒绝:不写 storage,允许下次再弹
* 由 privacy-popup.vue 的 handleDisagree 调用
*/
disagree() {
_needShow = false;
_pendingShow = false;
try { uni.$emit('privacy:disagreed'); } catch (e) {}
},
/**
* 当前是否需要弹窗(供页面查询)
* @returns {boolean}
*/
needShow() {
return _needShow;
},
/**
* 是否已执行过 check
* @returns {boolean}
*/
hasChecked() {
return _hasChecked;
},
};
export default PrivacyManager;