feat: use wx.createUserInfoButton to get weixin's avarta

This commit is contained in:
jakciehan
2026-05-14 22:41:32 +08:00
parent c4bd390478
commit 9359139186
13 changed files with 1181 additions and 184 deletions
+389
View File
@@ -0,0 +1,389 @@
/**
* PrivacyPopup.js
* Privacy authorization popup for WeChat mini-game.
*
* When a privacy-sensitive API (e.g. wx.getUserInfo) is called, WeChat
* triggers `wx.onNeedPrivacyAuthorization`. This popup shows a compliant
* dialog that explains what data we collect and lets the user agree or
* decline before we call `resolve({ buttonAction: 'agree' })`.
*
* Compliance notes (WeChat 2024+):
* - Must clearly state what data is collected and why.
* - Must provide a way to decline (user can still play with placeholder).
* - Must link to the full privacy policy.
*/
const {
SCREEN_WIDTH,
SCREEN_HEIGHT,
} = require('../base/GameGlobal');
const { t } = require('../i18n/I18n');
// Colors
const OVERLAY_BG = 'rgba(0, 0, 0, 0.75)';
const DIALOG_BG = '#1e1e32';
const DIALOG_BORDER = '#4a90d9';
const TITLE_COLOR = '#FFD700';
const TEXT_COLOR = '#CCCCCC';
const LINK_COLOR = '#4a90d9';
const AGREE_BG = '#4a90d9';
const AGREE_TEXT = '#FFFFFF';
const DECLINE_BG = 'rgba(255, 255, 255, 0.08)';
const DECLINE_BORDER = '#666666';
const DECLINE_TEXT = '#AAAAAA';
class PrivacyPopup {
constructor() {
/** @type {boolean} Whether the popup is currently visible. */
this._active = false;
/** @type {Function|null} The resolve callback from onNeedPrivacyAuthorization. */
this._resolveCallback = null;
/** @type {string} Which API triggered the privacy request. */
this._referrer = '';
/** @type {object|null} Button hit-test rects (computed during render). */
this._agreeBtn = null;
this._declineBtn = null;
this._policyLink = null;
}
// ============================================================
// Public API
// ============================================================
get active() {
return this._active;
}
/**
* Show the privacy popup.
* @param {Function} resolve - The resolve callback from onNeedPrivacyAuthorization.
* @param {object} eventInfo - The eventInfo from onNeedPrivacyAuthorization.
*/
show(resolve, eventInfo) {
// Guard: if popup is already active, chain the new resolve callback
// so that when the current popup resolves, both callbacks get called.
// This prevents showing two popups at once.
if (this._active && this._resolveCallback) {
console.warn('[PrivacyPopup] Already active, chaining resolve callback. referrer:', this._referrer, ', new referrer:', (eventInfo && eventInfo.referrer) || '');
const prevResolve = this._resolveCallback;
this._resolveCallback = (result) => {
try { prevResolve(result); } catch (e) { /* ignore */ }
try { resolve(result); } catch (e) { /* ignore */ }
};
return;
}
this._active = true;
this._resolveCallback = resolve;
this._referrer = (eventInfo && eventInfo.referrer) || '';
console.log('[PrivacyPopup] Showing popup, referrer:', this._referrer);
}
/**
* Hide the popup (without resolving — used internally after resolve is called).
*/
hide() {
this._active = false;
this._resolveCallback = null;
this._agreeBtn = null;
this._declineBtn = null;
this._policyLink = null;
}
/**
* Handle touch event. Returns true if the touch was consumed.
* @param {string} eventType - 'touchstart' | 'touchend'
* @param {object} e - The touch event object.
* @returns {boolean}
*/
handleTouch(eventType, e) {
if (!this._active) return false;
if (eventType !== 'touchend') return true; // Consume touchstart to prevent bleed-through
const touch = e.changedTouches && e.changedTouches[0];
if (!touch) return true;
const tx = touch.clientX;
const ty = touch.clientY;
// Agree button
if (this._agreeBtn && this._hitTest(tx, ty, this._agreeBtn)) {
console.log('[PrivacyPopup] User tapped AGREE');
this._resolve('agree');
return true;
}
// Decline button
if (this._declineBtn && this._hitTest(tx, ty, this._declineBtn)) {
console.log('[PrivacyPopup] User tapped DECLINE');
this._resolve('disagree');
return true;
}
// Privacy policy link
if (this._policyLink && this._hitTest(tx, ty, this._policyLink)) {
console.log('[PrivacyPopup] User tapped privacy policy link');
this._openPrivacyPolicy();
return true;
}
return true; // Consume all touches while popup is active
}
// ============================================================
// Render
// ============================================================
render(ctx) {
if (!this._active) return;
const cw = SCREEN_WIDTH;
const ch = SCREEN_HEIGHT;
const cx = cw / 2;
const cy = ch / 2;
// --- Semi-transparent overlay ---
ctx.fillStyle = OVERLAY_BG;
ctx.fillRect(0, 0, cw, ch);
// --- Dialog box ---
const dialogW = Math.min(cw * 0.88, 420);
const dialogH = Math.min(ch * 0.78, 320);
const dialogX = cx - dialogW / 2;
const dialogY = cy - dialogH / 2;
// Shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
this._roundRect(ctx, dialogX + 4, dialogY + 4, dialogW, dialogH, 12);
ctx.fill();
// Background
ctx.fillStyle = DIALOG_BG;
this._roundRect(ctx, dialogX, dialogY, dialogW, dialogH, 12);
ctx.fill();
// Border
ctx.strokeStyle = DIALOG_BORDER;
ctx.lineWidth = 1.5;
this._roundRect(ctx, dialogX, dialogY, dialogW, dialogH, 12);
ctx.stroke();
// --- Shield icon (Canvas path) ---
const iconY = dialogY + 32;
const sw = 22; // shield width
const sh = 26; // shield height
const sx = cx - sw / 2;
const sy = iconY - sh / 2;
// Shield body
ctx.fillStyle = DIALOG_BORDER;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx + sw, sy);
ctx.lineTo(sx + sw, sy + sh * 0.55);
ctx.quadraticCurveTo(sx + sw, sy + sh * 0.85, cx, sy + sh);
ctx.quadraticCurveTo(sx, sy + sh * 0.85, sx, sy + sh * 0.55);
ctx.closePath();
ctx.fill();
// Checkmark inside shield
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(cx - 6, iconY - 1);
ctx.lineTo(cx - 2, iconY + 4);
ctx.lineTo(cx + 7, iconY - 5);
ctx.stroke();
// --- Title ---
const titleY = iconY + 30;
ctx.fillStyle = TITLE_COLOR;
ctx.font = 'bold 17px Arial';
ctx.fillText(t('privacy.title'), cx, titleY);
// --- Body text ---
const bodyY = titleY + 28;
const bodyW = dialogW - 50;
ctx.fillStyle = TEXT_COLOR;
ctx.font = '13px Arial';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
const bodyLines = this._wrapText(ctx, t('privacy.body'), bodyW);
for (let i = 0; i < bodyLines.length; i++) {
ctx.fillText(bodyLines[i], dialogX + 25, bodyY + i * 20);
}
// --- Privacy policy link ---
const linkY = bodyY + bodyLines.length * 20 + 10;
const linkText = t('privacy.policyLink');
ctx.fillStyle = LINK_COLOR;
ctx.font = 'bold 13px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(linkText, cx, linkY);
// Store link hit area
const linkMetrics = ctx.measureText(linkText);
this._policyLink = {
x: cx - linkMetrics.width / 2 - 6,
y: linkY - 10,
w: linkMetrics.width + 12,
h: 20,
};
// Underline the link
ctx.strokeStyle = LINK_COLOR;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx - linkMetrics.width / 2, linkY + 7);
ctx.lineTo(cx + linkMetrics.width / 2, linkY + 7);
ctx.stroke();
// --- Buttons ---
const btnAreaY = linkY + 30;
const btnW = Math.min((dialogW - 60) / 2, 150);
const btnH = 38;
const btnGap = 16;
const agreeX = cx - btnW - btnGap / 2;
const declineX = cx + btnGap / 2;
// Agree button
ctx.fillStyle = AGREE_BG;
this._roundRect(ctx, agreeX, btnAreaY, btnW, btnH, 8);
ctx.fill();
ctx.fillStyle = AGREE_TEXT;
ctx.font = 'bold 15px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(t('privacy.agree'), agreeX + btnW / 2, btnAreaY + btnH / 2);
this._agreeBtn = { x: agreeX, y: btnAreaY, w: btnW, h: btnH };
// Decline button
ctx.fillStyle = DECLINE_BG;
this._roundRect(ctx, declineX, btnAreaY, btnW, btnH, 8);
ctx.fill();
ctx.strokeStyle = DECLINE_BORDER;
ctx.lineWidth = 1;
this._roundRect(ctx, declineX, btnAreaY, btnW, btnH, 8);
ctx.stroke();
ctx.fillStyle = DECLINE_TEXT;
ctx.font = '14px Arial';
ctx.fillText(t('privacy.decline'), declineX + btnW / 2, btnAreaY + btnH / 2);
this._declineBtn = { x: declineX, y: btnAreaY, w: btnW, h: btnH };
// --- Footer hint ---
const footerY = btnAreaY + btnH + 16;
ctx.fillStyle = '#666666';
ctx.font = '11px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(t('privacy.footer'), cx, footerY);
}
// ============================================================
// Private
// ============================================================
/**
* Resolve the privacy authorization request.
* @param {'agree'|'disagree'} action
*/
_resolve(action) {
if (!this._resolveCallback) {
console.warn('[PrivacyPopup] No resolve callback to call');
this.hide();
return;
}
try {
if (action === 'agree') {
console.log('[PrivacyPopup] Resolving with agree');
this._resolveCallback({ event: 'agree' });
} else {
console.log('[PrivacyPopup] Resolving with disagree');
// Disagree still needs to resolve, but the API will fail gracefully
this._resolveCallback({ event: 'disagree' });
}
} catch (e) {
console.error('[PrivacyPopup] resolve() threw:', e);
}
this.hide();
}
/**
* Open the privacy policy document.
* Uses wx.openPrivacyContract if available.
*/
_openPrivacyPolicy() {
try {
if (typeof wx !== 'undefined' && typeof wx.openPrivacyContract === 'function') {
wx.openPrivacyContract({
success: () => console.log('[PrivacyPopup] Privacy contract opened'),
fail: (err) => console.warn('[PrivacyPopup] Failed to open privacy contract:', err),
});
} else {
console.warn('[PrivacyPopup] wx.openPrivacyContract is not available');
}
} catch (e) {
console.warn('[PrivacyPopup] Error opening privacy contract:', e);
}
}
/**
* Simple hit-test for rectangular area.
*/
_hitTest(tx, ty, rect) {
return tx >= rect.x && tx <= rect.x + rect.w &&
ty >= rect.y && ty <= rect.y + rect.h;
}
/**
* Wrap text into lines that fit within maxWidth.
*/
_wrapText(ctx, text, maxWidth) {
const lines = [];
const paragraphs = text.split('\n');
for (const para of paragraphs) {
let line = '';
for (let i = 0; i < para.length; i++) {
const testLine = line + para[i];
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && line.length > 0) {
lines.push(line);
line = para[i];
} else {
line = testLine;
}
}
if (line) lines.push(line);
}
return lines;
}
/**
* Draw a rounded rectangle path.
*/
_roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
}
}
module.exports = PrivacyPopup;