import type { FileExportTextPayload, FileExportTextResult, HostBridgeMethod, HostBridgeRuntimeResult, } from '../../../packages/shared/src/contracts/hostBridge'; import type { WechatMiniProgramPayParams, WechatMiniProgramVirtualPayParams, } from '../../../packages/shared/src/contracts/runtime'; import { requestNativeAppHostBridge } from './nativeAppHostBridge'; const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js'; const MINI_PROGRAM_AUTH_PAGE_URL = '/pages/web-view/index?authAction=login&returnTo=previous'; const MINI_PROGRAM_PAY_PAGE_URL = '/pages/wechat-pay/index'; const MINI_PROGRAM_SHARE_GRID_PAGE_URL = '/pages/share-grid/index'; export type HostRuntimeKind = | 'browser' | 'wechat_mini_program' | 'native_app'; export type HostRuntimeSnapshot = { kind: HostRuntimeKind; clientType: string | null; clientRuntime: string | null; hostShell: string | null; hostPlatform: string | null; hostVersion: string | null; miniProgramEnv: string | null; }; export type HostRuntimeContext = { location?: Pick | null; navigator?: Partial> | null; wx?: Window['wx'] | null; tauri?: Window['__TAURI__'] | null; reactNativeWebView?: Window['ReactNativeWebView'] | null; }; export type HostNativePageNavigationOptions = { errorMessage?: string; beforeNavigate?: () => void; }; export type HostPaymentRequest = { payload: | WechatMiniProgramPayParams | WechatMiniProgramVirtualPayParams | null | undefined; orderId: string; }; export type HostShareGridRequest = { imageUrl: string; title: string; publicWorkCode: string; }; export type HostFileExportTextRequest = FileExportTextPayload; export type HostClipboardWriteTextRequest = { text: string; }; function isUnsupportedHostBridgeError(error: unknown) { return ( error instanceof Error && (error.name === 'unsupported_method' || error.name === 'unsupported_capability') ); } async function requestNativeHostBoolean( method: HostBridgeMethod, payload?: unknown, ) { try { return Boolean(await requestNativeAppHostBridge(method, payload)); } catch (error) { if (isUnsupportedHostBridgeError(error)) { return false; } throw error; } } function resolveLocation(context: HostRuntimeContext) { return ( context.location ?? (typeof window !== 'undefined' ? window.location : null) ); } function resolveNavigator(context: HostRuntimeContext) { return ( context.navigator ?? (typeof navigator !== 'undefined' ? navigator : null) ); } function resolveWxBridge(context: HostRuntimeContext) { return context.wx ?? (typeof window !== 'undefined' ? window.wx : null); } function resolveTauriBridge(context: HostRuntimeContext) { return ( context.tauri ?? (typeof window !== 'undefined' ? window.__TAURI__ : null) ); } function resolveReactNativeWebView(context: HostRuntimeContext) { return ( context.reactNativeWebView ?? (typeof window !== 'undefined' ? window.ReactNativeWebView : null) ); } function hasWechatMiniProgramBridge(wxBridge: Window['wx'] | null | undefined) { return Boolean( wxBridge?.miniProgram?.postMessage || wxBridge?.miniProgram?.navigateTo, ); } function isWechatMiniProgramUserAgent(userAgent: string) { const normalizedUserAgent = userAgent.toLowerCase(); return ( normalizedUserAgent.includes('micromessenger') && normalizedUserAgent.includes('miniprogram') ); } export function resolveHostRuntime( context: HostRuntimeContext = {}, ): HostRuntimeSnapshot { const location = resolveLocation(context); const params = new URLSearchParams(location?.search ?? ''); const clientType = params.get('clientType'); const clientRuntime = params.get('clientRuntime'); const hostShell = params.get('hostShell'); const hostPlatform = params.get('hostPlatform'); const hostVersion = params.get('hostVersion'); const miniProgramEnv = params.get('miniProgramEnv'); const navigatorLike = resolveNavigator(context); const wxBridge = resolveWxBridge(context); const tauriBridge = resolveTauriBridge(context); const reactNativeWebView = resolveReactNativeWebView(context); if ( clientRuntime === 'wechat_mini_program' || clientType === 'mini_program' || isWechatMiniProgramUserAgent(navigatorLike?.userAgent ?? '') || hasWechatMiniProgramBridge(wxBridge) ) { return { kind: 'wechat_mini_program', clientType, clientRuntime, hostShell, hostPlatform, hostVersion, miniProgramEnv, }; } if ( clientRuntime === 'native_app' || clientType === 'native_app' || typeof tauriBridge?.core?.invoke === 'function' || typeof reactNativeWebView?.postMessage === 'function' ) { return { kind: 'native_app', clientType, clientRuntime, hostShell, hostPlatform, hostVersion, miniProgramEnv, }; } return { kind: 'browser', clientType, clientRuntime, hostShell, hostPlatform, hostVersion, miniProgramEnv, }; } export function getHostRuntime(context: HostRuntimeContext = {}) { return resolveHostRuntime(context); } export function isWechatMiniProgramWebViewRuntime( context: HostRuntimeContext = {}, ) { return resolveHostRuntime(context).kind === 'wechat_mini_program'; } export function isNativeAppRuntime(context: HostRuntimeContext = {}) { return resolveHostRuntime(context).kind === 'native_app'; } export function loadWechatMiniProgramBridge( errorMessage = '请在微信小程序内完成操作', ) { if ( typeof window === 'undefined' || !isWechatMiniProgramWebViewRuntime() ) { return Promise.reject(new Error(errorMessage)); } if (window.wx?.miniProgram?.navigateTo) { return Promise.resolve(window.wx); } return new Promise>((resolve, reject) => { const existingScript = document.querySelector( `script[src="${WECHAT_JS_SDK_URL}"]`, ); const complete = () => { if (window.wx?.miniProgram?.navigateTo) { resolve(window.wx); } else { reject(new Error(errorMessage)); } }; if (existingScript) { if (window.wx?.miniProgram?.navigateTo) { complete(); return; } existingScript.addEventListener('load', complete, { once: true }); existingScript.addEventListener( 'error', () => reject(new Error(errorMessage)), { once: true }, ); return; } const script = document.createElement('script'); script.src = WECHAT_JS_SDK_URL; script.async = true; script.onload = complete; script.onerror = () => reject(new Error(errorMessage)); document.head.appendChild(script); }); } export async function navigateWechatMiniProgramPage( url: string, errorMessage = '请在微信小程序内完成操作', options: Pick = {}, ) { const wxBridge = await loadWechatMiniProgramBridge(errorMessage); const navigateTo = wxBridge.miniProgram?.navigateTo; if (typeof navigateTo !== 'function') { throw new Error(errorMessage); } await new Promise((resolve, reject) => { options.beforeNavigate?.(); navigateTo({ url, success() { resolve(); }, fail(error) { reject(new Error(error?.errMsg || errorMessage)); }, }); }); } export async function navigateHostNativePage( url: string, options: HostNativePageNavigationOptions = {}, ) { const runtime = getHostRuntime(); if (runtime.kind === 'native_app') { const result = await requestNativeHostBoolean( 'navigation.openNativePage', { url }, ); if (result) { options.beforeNavigate?.(); } return result; } if (runtime.kind !== 'wechat_mini_program') { return false; } await navigateWechatMiniProgramPage(url, options.errorMessage, { beforeNavigate: options.beforeNavigate, }); return true; } export async function requestHostLogin() { if (getHostRuntime().kind === 'native_app') { return await requestNativeHostBoolean('auth.requestLogin'); } return await navigateHostNativePage(MINI_PROGRAM_AUTH_PAGE_URL, { errorMessage: '请在微信小程序内完成登录', }); } export async function requestWechatMiniProgramPhoneLogin() { return await requestHostLogin(); } export async function requestHostPayment({ payload, orderId, }: HostPaymentRequest) { const runtime = getHostRuntime(); if (runtime.kind === 'native_app') { return await requestNativeHostBoolean('payment.request', { payload, orderId, }); } if (runtime.kind !== 'wechat_mini_program') { return false; } if (!payload) { throw new Error('请在微信小程序内完成支付'); } const requestId = `wechat_pay_${orderId}_${Date.now()}`; const searchParams = new URLSearchParams({ requestId, orderId, payParams: JSON.stringify(payload), }); await navigateWechatMiniProgramPage( `${MINI_PROGRAM_PAY_PAGE_URL}?${searchParams.toString()}`, '请在微信小程序内完成支付', ); return true; } export async function requestWechatMiniProgramPayment( payload: | WechatMiniProgramPayParams | WechatMiniProgramVirtualPayParams | null | undefined, orderId: string, ) { const handled = await requestHostPayment({ payload, orderId }); if (!handled) { throw new Error('请在微信小程序内完成支付'); } } function buildAbsoluteUrl(value: string) { if (typeof window === 'undefined') { return value; } return new URL(value, window.location.origin).href; } export function canUseHostShareGrid(context: HostRuntimeContext = {}) { return getHostRuntime(context).kind === 'wechat_mini_program'; } export function canUseWechatMiniProgramShareGrid() { return canUseHostShareGrid(); } export async function openHostShareGrid(params: HostShareGridRequest) { const imageUrl = params.imageUrl.trim(); if (!imageUrl || !canUseHostShareGrid()) { return false; } const searchParams = new URLSearchParams({ imageUrl: buildAbsoluteUrl(imageUrl), title: params.title.trim() || '我的作品', publicWorkCode: params.publicWorkCode.trim(), }); try { return await navigateHostNativePage( `${MINI_PROGRAM_SHARE_GRID_PAGE_URL}?${searchParams.toString()}`, { errorMessage: 'wechat_js_sdk_unavailable', }, ); } catch { return false; } } export async function openWechatMiniProgramShareGridPage( params: HostShareGridRequest, ) { return await openHostShareGrid(params); } export function setHostShareTarget(message: unknown) { if (getHostRuntime().kind === 'native_app') { void requestNativeAppHostBridge('share.setTarget', { target: message, }).catch(() => { // 分享目标同步是宿主提示能力,失败不应打断当前 H5 流程。 }); return true; } if ( typeof window === 'undefined' || getHostRuntime().kind !== 'wechat_mini_program' || typeof window.wx?.miniProgram?.postMessage !== 'function' ) { return false; } window.wx.miniProgram.postMessage({ data: message, }); return true; } export function postWechatMiniProgramMessage(message: unknown) { return setHostShareTarget(message); } export async function getNativeAppHostRuntime() { if (getHostRuntime().kind !== 'native_app') { return null; } return await requestNativeAppHostBridge( 'host.getRuntime', ); } export async function writeHostClipboardText({ text, }: HostClipboardWriteTextRequest) { if (getHostRuntime().kind !== 'native_app') { return false; } try { return await requestNativeHostBoolean('clipboard.writeText', { text }); } catch { return false; } } export async function exportHostTextFile( params: HostFileExportTextRequest, ) { if (getHostRuntime().kind !== 'native_app') { return false; } try { return await requestNativeAppHostBridge( 'file.exportText', params, { timeoutMs: 30000 }, ); } catch (error) { if (isUnsupportedHostBridgeError(error)) { return false; } throw error; } }