fix: align miniprogram web-view auth and share

This commit is contained in:
kdletters
2026-06-04 23:23:07 +08:00
parent 2678954627
commit dd5861d5f5
3 changed files with 91 additions and 25 deletions

View File

@@ -46,6 +46,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当
3. 登录弹窗继续复用现有独立 modal 和页签结构,不在页面中新增功能说明类文案,也不把邀请码输入放回登录面板。 3. 登录弹窗继续复用现有独立 modal 和页签结构,不在页面中新增功能说明类文案,也不把邀请码输入放回登录面板。
4. 微信小程序 `web-view` 外壳默认不预登录,首次进入直接打开 H5并保持与 Web 端一致的未登录状态;只有 H5 触发 `openLoginModal` / `requireAuth` 等受保护入口时,才跳转小程序原生授权态。 4. 微信小程序 `web-view` 外壳默认不预登录,首次进入直接打开 H5并保持与 Web 端一致的未登录状态;只有 H5 触发 `openLoginModal` / `requireAuth` 等受保护入口时,才跳转小程序原生授权态。
5. 小程序内需要登录时不展示 H5 登录弹窗,也不走手输手机号 / 短信验证码流程;统一通过原生 `button open-type="getPhoneNumber"` 获取微信手机号授权,再调用 `/api/auth/wechat/miniprogram-login``/api/auth/wechat/bind-phone` 换取系统登录态。 5. 小程序内需要登录时不展示 H5 登录弹窗,也不走手输手机号 / 短信验证码流程;统一通过原生 `button open-type="getPhoneNumber"` 获取微信手机号授权,再调用 `/api/auth/wechat/miniprogram-login``/api/auth/wechat/bind-phone` 换取系统登录态。
6. 小程序 `web-view` 页必须启用好友分享与朋友圈分享,分享目标固定回到 `pages/web-view/index`,不把 H5 当前 URL 作为不受控启动参数传回小程序页。
## 账户与充值 ## 账户与充值

View File

@@ -16,6 +16,33 @@ const PAY_RESULT_STORAGE_KEY = 'genarrative:wechat-pay-result';
const AUTH_RESULT_STORAGE_KEY = 'genarrative:mini-program-auth-result'; const AUTH_RESULT_STORAGE_KEY = 'genarrative:mini-program-auth-result';
const AUTH_ACTION_LOGIN = 'login'; const AUTH_ACTION_LOGIN = 'login';
const PAY_RESULT_RECHECK_DELAY_MS = 120; const PAY_RESULT_RECHECK_DELAY_MS = 120;
const WEB_VIEW_SHARE_TITLE = '陶泥儿';
const WEB_VIEW_SHARE_PATH = '/pages/web-view/index';
function showWebViewShareMenu() {
if (typeof wx.showShareMenu !== 'function') {
return;
}
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline'],
});
}
function buildWebViewShareAppMessage() {
return {
title: WEB_VIEW_SHARE_TITLE,
path: WEB_VIEW_SHARE_PATH,
};
}
function buildWebViewShareTimeline() {
return {
title: WEB_VIEW_SHARE_TITLE,
query: '',
};
}
function isConfiguredEntryUrl(value) { function isConfiguredEntryUrl(value) {
const trimmed = String(value || '').trim(); const trimmed = String(value || '').trim();
@@ -271,18 +298,6 @@ async function resolveAuthResult() {
}; };
} }
async function refreshMiniProgramSessionSilently() {
if (!isConfiguredApiBaseUrl(API_BASE_URL)) {
return null;
}
try {
return await resolveAuthResult();
} catch (error) {
console.warn('[web-view] silent mini program login refresh failed', error);
return null;
}
}
Page({ Page({
data: { data: {
authResult: null, authResult: null,
@@ -296,6 +311,7 @@ Page({
async onLoad(query = {}) { async onLoad(query = {}) {
this._lastLaunchQuery = query; this._lastLaunchQuery = query;
showWebViewShareMenu();
// 中文注释web-view 只能打开已配置业务域名;未配置时展示本地提示,避免空白页误判。 // 中文注释web-view 只能打开已配置业务域名;未配置时展示本地提示,避免空白页误判。
if (!isConfiguredEntryUrl(WEB_VIEW_ENTRY_URL)) { if (!isConfiguredEntryUrl(WEB_VIEW_ENTRY_URL)) {
this.setData({ this.setData({
@@ -309,14 +325,13 @@ Page({
const forcedPhoneBinding = parseBooleanQueryFlag(query.phoneBindingRequired); const forcedPhoneBinding = parseBooleanQueryFlag(query.phoneBindingRequired);
const returnToPreviousPage = shouldReturnToPreviousPage(query); const returnToPreviousPage = shouldReturnToPreviousPage(query);
if (!shouldStartAuthFromQuery(query) && !forcedPhoneBinding) { if (!shouldStartAuthFromQuery(query) && !forcedPhoneBinding) {
const authResult = await refreshMiniProgramSessionSilently();
this.setData({ this.setData({
authResult, authResult: null,
errorMessage: '', errorMessage: '',
loading: false, loading: false,
phoneBindingRequired: false, phoneBindingRequired: false,
returnToPreviousPage: false, returnToPreviousPage: false,
webViewUrl: resolveWebViewUrl(authResult), webViewUrl: resolveWebViewUrl(null),
}); });
return; return;
} }
@@ -490,4 +505,12 @@ Page({
// 中文注释:支付由独立 native 页面承接web-view 消息只保留调试输出。 // 中文注释:支付由独立 native 页面承接web-view 消息只保留调试输出。
console.info('[web-view] message', event.detail); console.info('[web-view] message', event.detail);
}, },
onShareAppMessage() {
return buildWebViewShareAppMessage();
},
onShareTimeline() {
return buildWebViewShareTimeline();
},
}); });

View File

@@ -17,6 +17,8 @@ type MiniProgramPage = {
data: Record<string, unknown>; data: Record<string, unknown>;
setData: (patch: Record<string, unknown>) => void; setData: (patch: Record<string, unknown>) => void;
onLoad: (query?: Record<string, string>) => Promise<void>; onLoad: (query?: Record<string, string>) => Promise<void>;
onShareAppMessage: () => Record<string, unknown>;
onShareTimeline: () => Record<string, unknown>;
onShow: () => void; onShow: () => void;
consumePayResult: () => void; consumePayResult: () => void;
}; };
@@ -29,6 +31,7 @@ function createWxMock() {
navigateBack: vi.fn(), navigateBack: vi.fn(),
removeStorageSync: vi.fn(), removeStorageSync: vi.fn(),
request: vi.fn(), request: vi.fn(),
showShareMenu: vi.fn(),
setStorageSync: vi.fn(), setStorageSync: vi.fn(),
}; };
} }
@@ -91,7 +94,7 @@ describe('mini-program web-view auth page', () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
test('默认进入时刷新微信小程序登录态后打开 web-view', async () => { test('默认进入时不预登录,直接打开未登录 web-view', async () => {
const wxMock = createWxMock(); const wxMock = createWxMock();
wxMock.login.mockImplementation(({ success }) => { wxMock.login.mockImplementation(({ success }) => {
success({ code: 'wx-login-code' }); success({ code: 'wx-login-code' });
@@ -109,19 +112,58 @@ describe('mini-program web-view auth page', () => {
await page.onLoad({}); await page.onLoad({});
expect(wxMock.login).toHaveBeenCalledTimes(1); expect(wxMock.login).not.toHaveBeenCalled();
expect(wxMock.request).toHaveBeenCalledWith( expect(wxMock.request).not.toHaveBeenCalled();
expect.objectContaining({
url: 'https://www.genarrative.world/api/auth/wechat/miniprogram-login',
method: 'POST',
data: { code: 'wx-login-code' },
}),
);
expect(page.data.loading).toBe(false); expect(page.data.loading).toBe(false);
expect(page.data.phoneBindingRequired).toBe(false); expect(page.data.phoneBindingRequired).toBe(false);
expect(page.data.webViewUrl).toBe( expect(page.data.webViewUrl).toBe(
'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program#auth_provider=wechat&auth_token=jwt-active-wechat&auth_binding_status=active', 'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program',
); );
expect(wxMock.showShareMenu).toHaveBeenCalledWith({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline'],
});
});
test('默认进入时即便微信新身份待绑手机号,也不弹出绑定手机号页', async () => {
const wxMock = createWxMock();
wxMock.login.mockImplementation(({ success }) => {
success({ code: 'wx-login-code' });
});
wxMock.request.mockImplementation(({ success }) => {
success({
statusCode: 200,
data: {
token: 'jwt-pending-wechat',
bindingStatus: 'pending_bind_phone',
},
});
});
const page = loadWebViewPage(wxMock);
await page.onLoad({});
expect(wxMock.login).not.toHaveBeenCalled();
expect(wxMock.request).not.toHaveBeenCalled();
expect(page.data.loading).toBe(false);
expect(page.data.phoneBindingRequired).toBe(false);
expect(page.data.webViewUrl).toBe(
'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program',
);
});
test('web-view 页面分享好友和朋友圈都回到小程序 web-view 入口', () => {
const wxMock = createWxMock();
const page = loadWebViewPage(wxMock);
expect(page.onShareAppMessage()).toEqual({
title: '陶泥儿',
path: '/pages/web-view/index',
});
expect(page.onShareTimeline()).toEqual({
title: '陶泥儿',
query: '',
});
}); });
test('默认匿名进入 web-view 仍不依赖 API_BASE_URL 配置', async () => { test('默认匿名进入 web-view 仍不依赖 API_BASE_URL 配置', async () => {