修复拼图生成前订阅授权

新增小程序原生订阅消息授权页,在用户点击后请求生成结果通知授权。

拼图 compile_puzzle_draft 前等待授权页返回或跳过后再发起生成 action。

移除 web-view message 订阅授权路径,改用 storage/hash 回写订阅结果。

补充订阅授权测试、文档和团队踩坑记录。
This commit is contained in:
kdletters
2026-06-08 13:06:07 +08:00
parent 38d9c292ae
commit 3a918687c5
17 changed files with 708 additions and 71 deletions

View File

@@ -0,0 +1,160 @@
import { isWechatMiniProgramRuntime } from './payment/paymentPlatform';
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
const SUBSCRIBE_RESULT_HASH_KEY = 'wx_subscribe_result';
const SUBSCRIBE_RESULT_TIMEOUT_MS = 30_000;
function clearSubscribeResultHash() {
const rawHash = window.location.hash.replace(/^#/, '');
if (!rawHash.includes(`${SUBSCRIBE_RESULT_HASH_KEY}=`)) {
return;
}
const params = new URLSearchParams(rawHash);
params.delete(SUBSCRIBE_RESULT_HASH_KEY);
const nextHash = params.toString();
window.history.replaceState(
null,
'',
`${window.location.pathname}${window.location.search}${nextHash ? `#${nextHash}` : ''}`,
);
}
function readSubscribeResultFromHash() {
const value = new URLSearchParams(window.location.hash.replace(/^#/, '')).get(
SUBSCRIBE_RESULT_HASH_KEY,
);
if (!value) {
return null;
}
clearSubscribeResultHash();
return value;
}
function waitSubscribeResultFromHash(timeoutMs = SUBSCRIBE_RESULT_TIMEOUT_MS) {
const immediateResult = readSubscribeResultFromHash();
if (immediateResult) {
return Promise.resolve(immediateResult);
}
return new Promise<string | null>((resolve) => {
let timer: number | null = null;
const cleanup = () => {
window.removeEventListener('hashchange', handleHashChange);
window.removeEventListener('focus', handleResume);
window.removeEventListener('pageshow', handleResume);
document.removeEventListener('visibilitychange', handleResume);
if (timer !== null) {
window.clearTimeout(timer);
}
};
const finish = (result: string | null) => {
cleanup();
resolve(result);
};
const consume = () => {
const result = readSubscribeResultFromHash();
if (result) {
finish(result);
return true;
}
return false;
};
const handleHashChange = () => {
consume();
};
const handleResume = () => {
if (
typeof document !== 'undefined' &&
document.visibilityState === 'hidden'
) {
return;
}
consume();
};
window.addEventListener('hashchange', handleHashChange);
window.addEventListener('focus', handleResume);
window.addEventListener('pageshow', handleResume);
document.addEventListener('visibilitychange', handleResume);
timer = window.setTimeout(() => finish(null), timeoutMs);
});
}
function loadWechatJsSdk() {
if (
!isWechatMiniProgramRuntime() ||
typeof window === 'undefined'
) {
return Promise.reject(new Error('not_mini_program'));
}
if (window.wx?.miniProgram?.navigateTo) {
return Promise.resolve(window.wx);
}
return new Promise<NonNullable<Window['wx']>>((resolve, reject) => {
const existingScript = document.querySelector<HTMLScriptElement>(
`script[src="${WECHAT_JS_SDK_URL}"]`,
);
const complete = () => {
if (window.wx?.miniProgram?.navigateTo) {
resolve(window.wx);
} else {
reject(new Error('wechat_js_sdk_unavailable'));
}
};
if (existingScript) {
existingScript.addEventListener('load', complete, { once: true });
existingScript.addEventListener('error', () => reject(new Error('wechat_js_sdk_load_failed')), {
once: true,
});
complete();
return;
}
const script = document.createElement('script');
script.src = WECHAT_JS_SDK_URL;
script.async = true;
script.onload = complete;
script.onerror = () => reject(new Error('wechat_js_sdk_load_failed'));
document.head.appendChild(script);
});
}
export async function requestGenerationResultSubscribePermission() {
if (!isWechatMiniProgramRuntime() || typeof window === 'undefined') {
return false;
}
let wxBridge: NonNullable<Window['wx']>;
try {
wxBridge = await loadWechatJsSdk();
} catch {
return false;
}
const miniProgram = wxBridge.miniProgram;
if (!miniProgram || typeof miniProgram.navigateTo !== 'function') {
return false;
}
const requestId = `subscribe_generation_result_${Date.now()}`;
const navigated = await new Promise<boolean>((resolve) => {
miniProgram.navigateTo?.({
url: `/pages/subscribe-message/index?requestId=${encodeURIComponent(requestId)}&scene=generation-result`,
success() {
resolve(true);
},
fail() {
resolve(false);
},
});
});
if (!navigated) {
return false;
}
const resultPromise = waitSubscribeResultFromHash();
const result = await resultPromise;
return Boolean(result);
}