feat: use wx.createUserInfoButton to get weixin's avarta
This commit is contained in:
+264
-53
@@ -97,48 +97,67 @@ class PlayerProfile {
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to fetch the anonymous nickname silently.
|
||||
* In modern WeChat this resolves with a placeholder user ("微信用户") but
|
||||
* without popping any authorization UI. Safe to call on MenuScene enter.
|
||||
* @returns {Promise<boolean>} true if something was set.
|
||||
* Fetch the user's nickname + avatar following the official WeChat flow:
|
||||
*
|
||||
* 1. `wx.getSetting` → check `scope.userInfo`
|
||||
* 2. Already authorized → `wx.getUserInfo` to get profile directly
|
||||
* 3. Not authorized → `wx.createUserInfoButton` overlay, wait for tap
|
||||
*
|
||||
* @param {object} [layout] - Position/size for the UserInfoButton overlay.
|
||||
* { x, y, width, height } in CSS pixels (logical pixels).
|
||||
* If omitted, a default area in the top-left corner is used.
|
||||
* @returns {Promise<boolean>} true if a real (non-placeholder) nickname was set.
|
||||
*/
|
||||
fetchSilent() {
|
||||
fetchSilent(layout) {
|
||||
return new Promise((resolve) => {
|
||||
if (this._granted || this._silentFetched) {
|
||||
resolve(!!this._nickname);
|
||||
if (this._granted) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
this._silentFetched = true;
|
||||
|
||||
try {
|
||||
if (typeof wx === 'undefined' || typeof wx.getUserInfo !== 'function') {
|
||||
if (typeof wx === 'undefined') {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
wx.getUserInfo({
|
||||
withCredentials: false,
|
||||
success: (res) => {
|
||||
const u = res && res.userInfo;
|
||||
if (u && u.nickName) {
|
||||
// NOTE: Since WeChat base library 2.27+, wx.getUserInfo returns
|
||||
// an anonymous placeholder ("微信用户") on real devices, but the
|
||||
// devtools environment may still return the real nickname — we
|
||||
// MUST NOT promote it to "granted" here, otherwise the cached
|
||||
// granted=true would prevent the UserInfoButton from ever being
|
||||
// created on real devices. The only source of truth for a real,
|
||||
// granted nickname is `applyUserInfoResult` (button tap).
|
||||
if (!this._nickname) {
|
||||
this._nickname = u.nickName;
|
||||
this._avatarUrl = u.avatarUrl || '';
|
||||
this._saveToCache();
|
||||
|
||||
// ── Step 1: wx.getSetting — check if scope.userInfo is already granted ──
|
||||
if (typeof wx.getSetting === 'function') {
|
||||
console.log('[PlayerProfile] Checking auth setting via wx.getSetting...');
|
||||
wx.getSetting({
|
||||
success: (settingRes) => {
|
||||
const authorized = settingRes && settingRes.authSetting && settingRes.authSetting['scope.userInfo'];
|
||||
console.log('[PlayerProfile] getSetting result: scope.userInfo =', authorized);
|
||||
|
||||
if (authorized) {
|
||||
// ── Step 2: Already authorized → getUserInfo directly ──
|
||||
this._fetchViaGetUserInfo(resolve);
|
||||
} else {
|
||||
// ── Step 3: Not authorized → create UserInfoButton ──
|
||||
this._createUserInfoButton(layout, resolve);
|
||||
}
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
resolve(false);
|
||||
},
|
||||
fail: () => resolve(false),
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
console.warn('[PlayerProfile] wx.getSetting fail:', err && err.errMsg, '— falling back to UserInfoButton');
|
||||
// Can't determine auth status — create the button as a safe default
|
||||
this._createUserInfoButton(layout, resolve);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Fallback: no wx.getSetting → try UserInfoButton, then getUserInfo ──
|
||||
if (typeof wx.createUserInfoButton === 'function') {
|
||||
this._createUserInfoButton(layout, resolve);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof wx.getUserInfo === 'function') {
|
||||
this._fetchViaGetUserInfo(resolve);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(false);
|
||||
} catch (e) {
|
||||
console.warn('[PlayerProfile] fetchSilent error:', e && e.message);
|
||||
resolve(false);
|
||||
@@ -146,6 +165,162 @@ class PlayerProfile {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a transparent UserInfoButton overlay and wait for user tap.
|
||||
* This is the ONLY way to acquire real profile data from a user who
|
||||
* has not yet granted scope.userInfo.
|
||||
* @param {object} [layout] - { x, y, width, height }
|
||||
* @param {Function} resolve - Promise resolver
|
||||
* @private
|
||||
*/
|
||||
_createUserInfoButton(layout, resolve) {
|
||||
if (typeof wx.createUserInfoButton !== 'function') {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[PlayerProfile] Creating UserInfoButton (scope not authorized)...');
|
||||
const btnLayout = layout || { x: 10, y: 10, width: 120, height: 32 };
|
||||
|
||||
const button = wx.createUserInfoButton({
|
||||
type: 'text',
|
||||
text: '',
|
||||
style: {
|
||||
left: btnLayout.x,
|
||||
top: btnLayout.y,
|
||||
width: btnLayout.width,
|
||||
height: btnLayout.height,
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: 'transparent',
|
||||
color: 'transparent',
|
||||
fontSize: 1,
|
||||
borderRadius: 0,
|
||||
textAlign: 'center',
|
||||
lineHeight: btnLayout.height,
|
||||
},
|
||||
});
|
||||
|
||||
button.onTap((res) => {
|
||||
console.log('[PlayerProfile] UserInfoButton onTap:',
|
||||
res && res.userInfo ? { nickName: res.userInfo.nickName, hasAvatar: !!res.userInfo.avatarUrl }
|
||||
: (res && res.errMsg ? { errMsg: res.errMsg } : 'null'));
|
||||
|
||||
const u = res && res.userInfo;
|
||||
if (!u || !u.nickName || this.isPlaceholderName(u.nickName)) {
|
||||
console.log('[PlayerProfile] UserInfoButton returned placeholder or no data — keeping button for retry');
|
||||
if (u && u.avatarUrl && !this._avatarUrl) {
|
||||
this._avatarUrl = u.avatarUrl;
|
||||
this._saveToCache();
|
||||
this._emitProfileUpdate();
|
||||
}
|
||||
// Don't destroy the button — user may tap again later.
|
||||
if (!this._granted) resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Success — destroy the button, we don't need it anymore
|
||||
try { button.destroy(); } catch (e) { /* ignore */ }
|
||||
this._userInfoButton = null;
|
||||
|
||||
this._nickname = u.nickName;
|
||||
this._avatarUrl = u.avatarUrl || '';
|
||||
this._granted = true;
|
||||
this._silentFetched = true;
|
||||
this._saveToCache();
|
||||
this._emitProfileUpdate();
|
||||
console.log('[PlayerProfile] Profile granted via UserInfoButton:', this._nickname);
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
button.show();
|
||||
|
||||
// Store button ref so MenuScene can destroy it on exit
|
||||
this._userInfoButton = button;
|
||||
|
||||
// Timeout: if user never taps, unblock the Promise but keep the button visible
|
||||
setTimeout(() => {
|
||||
if (!this._granted && this._userInfoButton === button) {
|
||||
console.log('[PlayerProfile] UserInfoButton not tapped after 30s — resolving false, button stays visible');
|
||||
resolve(false);
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated fallback: call wx.getUserInfo.
|
||||
* On modern WeChat (2022-10+) this returns "scope unauthorized" and
|
||||
* should NOT be used as the primary acquisition path.
|
||||
* @param {Function} resolve
|
||||
* @private
|
||||
*/
|
||||
_fetchViaGetUserInfo(resolve) {
|
||||
if (this._silentFetched) {
|
||||
resolve(!!this._nickname);
|
||||
return;
|
||||
}
|
||||
this._silentFetched = true;
|
||||
|
||||
let resolved = false;
|
||||
console.log('[PlayerProfile] Calling wx.getUserInfo (deprecated fallback)...');
|
||||
wx.getUserInfo({
|
||||
withCredentials: false,
|
||||
success: (res) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
const u = res && res.userInfo;
|
||||
console.log('[PlayerProfile] wx.getUserInfo success, userInfo:', u ? { nickName: u.nickName, hasAvatar: !!u.avatarUrl } : 'null');
|
||||
if (u) {
|
||||
let changed = false;
|
||||
if (u.nickName && u.nickName !== this._nickname && !this.isPlaceholderName(u.nickName)) {
|
||||
this._nickname = u.nickName;
|
||||
this._granted = true;
|
||||
changed = true;
|
||||
}
|
||||
if (u.avatarUrl && u.avatarUrl !== this._avatarUrl) {
|
||||
this._avatarUrl = u.avatarUrl;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
console.log('[PlayerProfile] Profile updated from getUserInfo');
|
||||
this._saveToCache();
|
||||
this._emitProfileUpdate();
|
||||
}
|
||||
resolve(!!this._nickname);
|
||||
return;
|
||||
}
|
||||
resolve(false);
|
||||
},
|
||||
fail: (err) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
console.warn('[PlayerProfile] wx.getUserInfo fail:', err && err.errMsg);
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
|
||||
// Timeout guard
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.warn('[PlayerProfile] wx.getUserInfo timed out after 5s');
|
||||
resolve(false);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the UserInfoButton overlay if one exists.
|
||||
* Call this when leaving the MenuScene or when the profile is granted.
|
||||
*/
|
||||
destroyUserInfoButton() {
|
||||
if (this._userInfoButton) {
|
||||
try {
|
||||
this._userInfoButton.destroy();
|
||||
} catch (e) { /* ignore */ }
|
||||
this._userInfoButton = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the well-known WeChat anonymous placeholder. Since 2022-10,
|
||||
* `wx.getUserInfo` / `UserInfoButton` return this string for any user who
|
||||
@@ -180,6 +355,7 @@ class PlayerProfile {
|
||||
this._avatarUrl = u.avatarUrl || '';
|
||||
this._granted = true;
|
||||
this._saveToCache();
|
||||
this._emitProfileUpdate();
|
||||
console.log('[PlayerProfile] Nickname granted:', this._nickname);
|
||||
return true;
|
||||
}
|
||||
@@ -203,28 +379,41 @@ class PlayerProfile {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
wx.getUserProfile({
|
||||
desc: '用于在对战中展示你的昵称',
|
||||
lang: 'zh_CN',
|
||||
success: (res) => {
|
||||
const u = res && res.userInfo;
|
||||
if (!u || !u.nickName || this.isPlaceholderName(u.nickName)) {
|
||||
console.log('[PlayerProfile] getUserProfile returned placeholder.');
|
||||
|
||||
const doProfile = () => {
|
||||
wx.getUserProfile({
|
||||
desc: '用于在对战中展示你的昵称',
|
||||
lang: 'zh_CN',
|
||||
success: (res) => {
|
||||
const u = res && res.userInfo;
|
||||
if (!u || !u.nickName || this.isPlaceholderName(u.nickName)) {
|
||||
console.log('[PlayerProfile] getUserProfile returned placeholder.');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
this._nickname = u.nickName;
|
||||
this._avatarUrl = u.avatarUrl || '';
|
||||
this._granted = true;
|
||||
this._saveToCache();
|
||||
this._emitProfileUpdate();
|
||||
console.log('[PlayerProfile] Nickname granted via getUserProfile:', this._nickname);
|
||||
resolve(true);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('[PlayerProfile] getUserProfile fail:', err && err.errMsg);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
this._nickname = u.nickName;
|
||||
this._avatarUrl = u.avatarUrl || '';
|
||||
this._granted = true;
|
||||
this._saveToCache();
|
||||
console.log('[PlayerProfile] Nickname granted via getUserProfile:', this._nickname);
|
||||
resolve(true);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('[PlayerProfile] getUserProfile fail:', err && err.errMsg);
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Call wx.getUserProfile directly. If the user hasn't authorized
|
||||
// privacy yet, WeChat will trigger wx.onNeedPrivacyAuthorization
|
||||
// (registered in game.js) which shows our PrivacyPopup, then
|
||||
// automatically retries the pending API call after the user agrees.
|
||||
//
|
||||
// We do NOT call wx.requirePrivacyAuthorize because it is unreliable
|
||||
// in WeChat mini-games — see fetchSilent() comments for details.
|
||||
doProfile();
|
||||
} catch (e) {
|
||||
console.warn('[PlayerProfile] requestUserProfile error:', e && e.message);
|
||||
resolve(false);
|
||||
@@ -288,6 +477,28 @@ class PlayerProfile {
|
||||
// Ignore storage errors.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a `profile:updated` event via the global EventBus so that
|
||||
* other systems (e.g. NetworkManager, TeamRoomScene) can react to
|
||||
* nickname / avatarUrl changes — typically by re-sending the latest
|
||||
* profile data to the server so peers see the updated avatar.
|
||||
* @private
|
||||
*/
|
||||
_emitProfileUpdate() {
|
||||
try {
|
||||
const bus = (typeof GameGlobal !== 'undefined') ? GameGlobal.eventBus : null;
|
||||
if (bus && typeof bus.emit === 'function') {
|
||||
bus.emit('profile:updated', {
|
||||
nickname: this._nickname,
|
||||
avatarUrl: this._avatarUrl,
|
||||
granted: this._granted,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Non-critical — event emission should never break the game.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PlayerProfile;
|
||||
|
||||
Reference in New Issue
Block a user