feat(rn-common): v0.4.0 — api/ui 子模块 + config 扩展 + autoInit 修复

- 新增 api/ 子模块:useRequest、useApi、usePageApi(替换 @szyx-mobile)
- 新增 ui/ 子模块:showToast、showAlert、showConfirm、configureToast
- 新增 globalErrorHandler:setGlobalApiErrorHandler
- config.ts 新增 logApiUrl、logEnabled 字段
- device.ts 新增 manufacturer、vendorVersion 可选字段
- autoInit.ts 修复静默降级,__DEV__ 模式下 re-throw
- index.ts 补全 api/ui 导出
- package.json 版本升至 0.4.0,新增 axios/react/zod 依赖
这个提交包含在:
XuqmGroup 2026-06-16 12:01:44 +08:00
父节点 9870a0a368
当前提交 97d4d9498a
共有 15 个文件被更改,包括 860 次插入5 次删除

查看文件

@ -1,6 +1,6 @@
{
"name": "@xuqm/rn-common",
"version": "0.3.2",
"version": "0.4.0",
"description": "XuqmGroup RN SDK — core: init, network, token management",
"license": "UNLICENSED",
"main": "src/index.ts",
@ -14,12 +14,18 @@
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-native": ">=0.76.0",
"@react-native-async-storage/async-storage": ">=1.21.0"
"@react-native-async-storage/async-storage": ">=1.21.0",
"axios": ">=1.0.0"
},
"devDependencies": {
"typescript": "^5.9.3",
"@types/react": "^19.0.0",
"@types/react-native": "^0.73.0",
"@react-native-async-storage/async-storage": "^2.1.2"
"@react-native-async-storage/async-storage": "^2.1.2",
"axios": "^1.7.0",
"react": "^19.0.0",
"zod": "^3.23.0"
}
}

查看文件

@ -0,0 +1,11 @@
export class RequestError<TData = unknown> extends Error {
constructor(
message: string,
public readonly type: 'Cancel' | 'ValidationError' | 'AxiosError' | 'OtherError',
public readonly cause?: unknown,
public readonly response?: { data?: TData; status?: number },
) {
super(message)
this.name = 'RequestError'
}
}

查看文件

@ -0,0 +1,15 @@
import { RequestError } from './errors'
type ApiErrorHandler = (error: RequestError) => void
let _handler: ApiErrorHandler | null = null
/** 注册全局 API 错误处理器(宿主在初始化时注入,通常将错误转发给 rn-log。*/
export function setGlobalApiErrorHandler(handler: ApiErrorHandler): void {
_handler = handler
}
/** 内部调用:在 useRequest 的 catch 分支中触发。*/
export function _notifyApiError(error: RequestError): void {
_handler?.(error)
}

查看文件

@ -0,0 +1,7 @@
export { useApi } from './useApi'
export { usePageApi } from './usePageApi'
export { RequestError } from './errors'
export { setGlobalApiErrorHandler } from './globalErrorHandler'
export type { RequestOptions } from './useRequest'
export type { ApiMethod } from './useApi'
export type { AxiosRequestConfig } from 'axios'

查看文件

@ -0,0 +1,172 @@
import { useCallback, useEffect, useMemo } from 'react'
import {
type AxiosInterceptorManager,
type AxiosRequestConfig,
type AxiosResponse,
isAxiosError,
isCancel,
} from 'axios'
import { z } from 'zod'
import { RequestError } from './errors'
import { type RequestOptions, useRequest } from './useRequest'
const SUCCESS_STATUS = /^0$/
type ApiResponse<T> = {
status: string
message: string
data: T
}
export type ApiMethod = 'GET' | 'POSTJSON' | 'POSTFORM' | 'PUT' | 'UPLOAD'
// ValidationError shape (duck-typed to avoid circular dependency)
interface HasIssues {
issues: Array<{ path: (string | number)[]; message: string }>
response?: { data?: { message?: string } }
}
function isValidationError(e: unknown): e is HasIssues {
return (
e != null &&
typeof e === 'object' &&
'issues' in e &&
Array.isArray((e as { issues: unknown }).issues)
)
}
export const useApi = <
S extends z.ZodTypeAny,
T extends z.infer<S> = z.infer<S>,
D = unknown,
>(
url: string,
method: ApiMethod,
params?: D,
validationSchema?: S,
options?: RequestOptions,
config?: AxiosRequestConfig<D>,
): {
response?: T
error?: RequestError<unknown>
loading: boolean
fetch: (config?: AxiosRequestConfig<D>) => void
fetchAsync: (config?: AxiosRequestConfig<D>) => Promise<T>
cancel: () => void
requestInterceptors: AxiosInterceptorManager<AxiosRequestConfig<unknown>>
responseInterceptors: AxiosInterceptorManager<AxiosResponse<ApiResponse<T>, unknown>>
} => {
const responseSchema = useMemo(
() =>
z.object({
status: z.string().regex(SUCCESS_STATUS),
message: z.string(),
data: validationSchema ?? z.unknown(),
}),
[validationSchema],
)
const {
response,
error,
loading,
fetch,
fetchAsync,
cancel,
requestInterceptors,
responseInterceptors,
} = useRequest<typeof responseSchema, ApiResponse<T>, unknown>(
url,
method,
params,
responseSchema,
options,
{
timeout: 30_000,
...config,
},
)
useEffect(() => {
// Note: signing headers are injected by the app layer via requestInterceptors.use(...)
// The SDK itself does not know MD5_KEY or CLIENT_ID — those are app-specific.
// Callers can add a request interceptor to inject signing headers.
// (This mirrors the pattern in YiwangxinApp4's useApi.ts but removes app-specific logic.)
return undefined
}, [requestInterceptors])
useEffect(() => {
const responseInterceptor = responseInterceptors.use(
nextResponse => nextResponse,
(thrownError: unknown) => {
if (isCancel(thrownError)) {
return Promise.reject(
new RequestError('网络请求已取消', 'Cancel', thrownError),
)
}
if (isValidationError(thrownError)) {
if (thrownError.issues.length <= 0) {
return Promise.reject(
new RequestError('数据格式校验错误', 'ValidationError', thrownError),
)
}
const firstIssue = thrownError.issues[0]
if (firstIssue.path.length > 0 && firstIssue.path[0] === 'status') {
return Promise.reject(
new RequestError(
thrownError.response?.data?.message ?? '接口返回失败',
'ValidationError',
thrownError,
),
)
}
return Promise.reject(
new RequestError(firstIssue.message, 'ValidationError', thrownError),
)
}
if (isAxiosError(thrownError)) {
return Promise.reject(
new RequestError('网络请求失败', 'AxiosError', thrownError),
)
}
return Promise.reject(
new RequestError('网络请求失败', 'OtherError', thrownError),
)
},
)
return () => {
responseInterceptors.eject(responseInterceptor)
}
}, [responseInterceptors])
const transformedFetchAsync = useCallback(
async (nextConfig?: AxiosRequestConfig<D>) => {
try {
const requestResponse = await fetchAsync(nextConfig)
return (requestResponse as unknown as AxiosResponse<ApiResponse<T>>).data.data
} catch (requestError) {
return Promise.reject(requestError)
}
},
[fetchAsync],
)
return {
response: (response as unknown as AxiosResponse<ApiResponse<T>> | undefined)?.data.data,
error: error as RequestError<unknown>,
loading,
fetch,
fetchAsync: transformedFetchAsync,
cancel,
requestInterceptors: requestInterceptors as AxiosInterceptorManager<AxiosRequestConfig<unknown>>,
responseInterceptors: responseInterceptors as AxiosInterceptorManager<AxiosResponse<ApiResponse<T>, unknown>>,
}
}
export type { AxiosRequestConfig, RequestError, RequestOptions }

查看文件

@ -0,0 +1,127 @@
import { useEffect, useState } from 'react'
import type { AxiosRequestConfig } from 'axios'
import type { RequestError, RequestOptions } from './useRequest'
import { z } from 'zod'
import { useApi } from './useApi'
type List<T> = {
records: T[]
}
type Root<T> = T[]
interface PageOptions extends RequestOptions {
pageSize?: number
path?: 'list' | 'root'
pagination?: 'cursor' | 'offset'
}
export const usePageApi = <
S extends z.ZodTypeAny,
T extends z.infer<S> = z.infer<S>,
P extends List<T> | Root<T> = List<T>,
D = Record<string, unknown>,
>(
url: string,
method: 'GET' | 'POSTJSON' | 'POSTFORM',
params?: D,
validationSchema?: S,
options?: PageOptions,
config?: AxiosRequestConfig<D>,
): {
response?: T[]
error?: RequestError<unknown>
loading: boolean
fetch: React.Dispatch<React.SetStateAction<'reload' | 'loadmore'>>
cancel: () => void
} => {
const [data, setData] = useState<T[] | undefined>(undefined)
const [status, setStatus] = useState<'loaded' | 'reload' | 'loadmore'>('loaded')
const listSchema: z.ZodSchema<List<T>> = z.object({
records: z.array(validationSchema ?? z.any()),
})
const rootSchema: z.ZodSchema<Root<T>> = z.array(validationSchema ?? z.any())
const { error, loading, fetchAsync, cancel } = useApi<
typeof listSchema | typeof rootSchema,
P
>(
url,
method,
options?.pagination === 'offset'
? {
...params,
current:
status === 'loadmore'
? Math.ceil((data ?? []).length / (options?.pageSize ?? 10)) + 1
: 1,
size: options?.pageSize ?? 10,
}
: {
...params,
startNum: status === 'loadmore' ? (data ?? []).length : 0,
endNum:
status === 'loadmore'
? (data ?? []).length + (options?.pageSize ?? 10) - 1
: (options?.pageSize ?? 10) - 1,
},
options?.path === 'root' ? rootSchema : listSchema,
options,
config,
)
useEffect(() => {
if (
status === 'loaded' ||
(status === 'loadmore' &&
((data ?? []).length === 0 ||
(data ?? []).length % (options?.pageSize ?? 10) !== 0))
) {
return
}
fetchAsync()
.then(result => {
if (status === 'reload') {
switch (options?.path) {
case 'root':
setData(result as Root<T>)
break
default:
setData((result as List<T>).records)
break
}
setStatus('loaded')
return
}
switch (options?.path) {
case 'root':
setData(previous => [...(previous ?? []), ...(result as Root<T>)])
break
default:
setData(previous => [
...(previous ?? []),
...(result as List<T>).records,
])
break
}
setStatus('loaded')
})
.catch(() => {
setStatus('loaded')
})
}, [data, fetchAsync, options?.pageSize, options?.path, status])
return {
response: data,
error: error as RequestError<unknown>,
loading,
fetch: setStatus as React.Dispatch<React.SetStateAction<'reload' | 'loadmore'>>,
cancel,
}
}
export type { AxiosRequestConfig, PageOptions }

查看文件

@ -0,0 +1,237 @@
/**
* useRequest @szyx-mobile/use-request
* 使 axios.create() + AbortController
*/
import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'
import axios, {
type AxiosRequestConfig,
type AxiosResponse,
isAxiosError,
isCancel,
} from 'axios'
import { z } from 'zod'
import { RequestError } from './errors'
import { _notifyApiError } from './globalErrorHandler'
export interface RequestOptions {
/** true = 不自动触发请求(需手动调用 fetch/fetchAsync。*/
manual?: boolean
/** 延迟多少毫秒后才将 loading 置为 true默认 0。*/
loadingDelay?: number
}
// ─── 内部 ValidationError对齐 @szyx-mobile/use-axios 的 ValidationError────
class ValidationError<T = unknown, D = unknown> extends z.ZodError {
constructor(
issues: z.ZodIssue[],
public readonly response: AxiosResponse<T, D>,
) {
super(issues)
this.name = 'ValidationError'
}
}
// ─── Reducer ──────────────────────────────────────────────────────────────────
interface State<R> {
response: R | undefined
error: RequestError | undefined
loading: boolean
}
type Action<R> =
| { type: 'start' }
| { type: 'resolve'; response: R }
| { type: 'reject'; error: RequestError }
function reducer<R>(state: State<R>, action: Action<R>): State<R> {
switch (action.type) {
case 'start':
return { ...state, loading: true }
case 'resolve':
return { response: action.response, error: undefined, loading: false }
case 'reject':
return { ...state, error: action.error, loading: false }
}
}
const initialState = { response: undefined, error: undefined, loading: false }
// ─── useRequest ───────────────────────────────────────────────────────────────
export function useRequest<
S extends z.ZodTypeAny,
T extends z.infer<S> = z.infer<S>,
D = unknown,
R = AxiosResponse<T, D>,
>(
url: string,
method: 'GET' | 'POSTJSON' | 'POSTFORM' | 'PUT' | 'UPLOAD',
params?: D,
validationSchema?: S,
options?: RequestOptions,
config?: AxiosRequestConfig<D>,
): {
response: R | undefined
error: RequestError | undefined
loading: boolean
fetch: (config?: AxiosRequestConfig<D>) => void
fetchAsync: (config?: AxiosRequestConfig<D>) => Promise<R>
cancel: () => void
requestInterceptors: ReturnType<typeof axios.create>['interceptors']['request']
responseInterceptors: ReturnType<typeof axios.create>['interceptors']['response']
} {
const [state, dispatch] = useReducer(reducer<R>, initialState as State<R>)
// axios 实例用 useRef 持有,避免每次 render 重建(丢失拦截器)
// eslint-disable-next-line react-hooks/exhaustive-deps
const instance = useMemo(() => axios.create(), [])
const abortControllerRef = useRef<AbortController | undefined>(undefined)
const validationSchemaRef = useRef(validationSchema)
const optionsRef = useRef(options)
// 保持 ref 同步(避免闭包陈旧引用)
useEffect(() => { validationSchemaRef.current = validationSchema })
useEffect(() => { optionsRef.current = options })
// Zod 校验响应拦截器(对齐 useValidatedAxios 行为)
useEffect(() => {
const id = instance.interceptors.response.use(
(r: AxiosResponse) => {
const schema = validationSchemaRef.current
if (schema) {
const result = schema.safeParse(r.data)
if (!result.success) {
return Promise.reject(new ValidationError(result.error.issues, r))
}
r.data = result.data
}
return r
},
)
return () => { instance.interceptors.response.eject(id) }
}, [instance])
const cancel = useCallback(() => {
abortControllerRef.current?.abort()
}, [])
// axios 方法映射
function resolveAxiosMethod(m: typeof method): string {
switch (m) {
case 'GET': return 'get'
case 'PUT': return 'put'
default: return 'post'
}
}
const fetchAsync = useCallback(
async (overrideConfig?: AxiosRequestConfig<D>): Promise<R> => {
const loadingDelay = optionsRef.current?.loadingDelay ?? 0
const timer = setTimeout(() => { dispatch({ type: 'start' }) }, loadingDelay)
cancel()
abortControllerRef.current = new AbortController()
// Content-Type 按 method 设置
const methodHeaders: Record<string, string> = {}
if (method === 'POSTFORM') {
methodHeaders['Content-Type'] = 'application/x-www-form-urlencoded'
} else if (method === 'UPLOAD') {
methodHeaders['Content-Type'] = 'multipart/form-data'
}
const axiosMethod = resolveAxiosMethod(method)
const isGet = method === 'GET'
const mergedConfig: AxiosRequestConfig<D> = {
timeout: 30_000,
...config,
...overrideConfig,
url,
method: axiosMethod,
params: isGet ? params : (overrideConfig?.params ?? config?.params),
data: isGet ? (overrideConfig?.data ?? config?.data) : params,
headers: {
...methodHeaders,
...(config?.headers as Record<string, string> | undefined),
...(overrideConfig?.headers as Record<string, string> | undefined),
},
signal: abortControllerRef.current.signal,
}
try {
const r = await instance.request<T, AxiosResponse<T, D>, D>(mergedConfig)
clearTimeout(timer)
dispatch({ type: 'resolve', response: r as unknown as R })
return r as unknown as R
} catch (e: unknown) {
clearTimeout(timer)
let wrapped: RequestError
if (isCancel(e)) {
wrapped = new RequestError('网络请求已取消', 'Cancel', e)
} else if (e instanceof ValidationError) {
if (e.issues.length === 0) {
wrapped = new RequestError('数据格式校验错误', 'ValidationError', e)
} else {
const first = e.issues[0]
if (first.path.length > 0 && first.path[0] === 'status') {
const resp = e.response as AxiosResponse<{ message?: string }> | undefined
wrapped = new RequestError(
resp?.data?.message ?? '接口返回失败',
'ValidationError',
e,
)
} else {
wrapped = new RequestError(first.message, 'ValidationError', e)
}
}
} else if (isAxiosError(e)) {
wrapped = new RequestError('网络请求失败', 'AxiosError', e)
} else {
wrapped = new RequestError('网络请求失败', 'OtherError', e)
}
_notifyApiError(wrapped)
dispatch({ type: 'reject', error: wrapped })
return Promise.reject(wrapped)
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[cancel, config, instance, method, params, url],
)
const fetch = useCallback(
(overrideConfig?: AxiosRequestConfig<D>) => {
fetchAsync(overrideConfig).catch(() => { /* error already in state */ })
},
[fetchAsync],
)
// 自动触发manual !== true 时挂载自动发请求)
useEffect(() => {
if (!optionsRef.current?.manual) {
fetch()
}
// 仅在 mount 时触发一次
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// unmount 时取消请求
useEffect(() => { return () => { cancel() } }, [cancel])
return {
response: state.response,
error: state.error,
loading: state.loading,
fetch,
fetchAsync,
cancel,
requestInterceptors: instance.interceptors.request,
responseInterceptors: instance.interceptors.response,
}
}

查看文件

@ -40,7 +40,8 @@ export function tryAutoInit(): void {
const encrypted = tryRequireConfig()
if (!encrypted) return
XuqmSDK.initWithConfigFile(encrypted, {debug: __DEV__}).catch(() => {
// 静默降级
XuqmSDK.initWithConfigFile(encrypted, {debug: __DEV__}).catch((e: unknown) => {
console.error('[XuqmSDK] Auto-init failed:', e)
if (__DEV__) throw e
})
}

查看文件

@ -15,6 +15,9 @@ export interface XuqmConfig {
imEnabled: boolean
pushEnabled: boolean
licenseEnabled: boolean
// 日志服务rn-log 使用)
logApiUrl: string
logEnabled: boolean
}
export interface XuqmUserInfo {
@ -44,6 +47,8 @@ export interface XuqmRemoteConfig {
imEnabled?: boolean
pushEnabled?: boolean
licenseEnabled?: boolean
logApiUrl?: string
logEnabled?: boolean
}
export function initConfigFromRemote(
@ -61,6 +66,8 @@ export function initConfigFromRemote(
imEnabled: remote.imEnabled ?? !!remote.imWsUrl,
pushEnabled: remote.pushEnabled ?? true,
licenseEnabled: remote.licenseEnabled ?? false,
logApiUrl: remote.logApiUrl ?? '',
logEnabled: remote.logEnabled ?? false,
}
}

查看文件

@ -33,6 +33,10 @@ export interface DeviceInfo {
model: string
osVersion: string
pushVendor: PushVendor
/** 设备制造商(如 "APPLE"/"HUAWEI";iOS 暂留 undefined。*/
manufacturer?: string
/** 厂商 ROM 版本;Android 可选填充,iOS 暂留 undefined。*/
vendorVersion?: string
}
const BRAND_MAP: Record<string, PushVendor> = {

查看文件

@ -23,7 +23,10 @@ export {
getXWebViewConfig,
openXWebView,
setXWebViewController,
setXWebViewNavigationRef,
processJSBridgeMessage,
} from './xwebview/XWebViewBridge'
export type { XWebViewNavigationRef } from './xwebview/XWebViewBridge'
export type {
XWebViewClickMenu,
XWebViewConfig,
@ -35,3 +38,18 @@ export type {
XWebViewMessageEvent,
XWebViewPermissionRequest,
} from './xwebview/types'
// api/ 子模块
export {
RequestError,
setGlobalApiErrorHandler,
useRequest,
useApi,
usePageApi,
} from './api'
export type { RequestOptions as UseRequestOptions, ApiMethod } from './api'
export type { PageOptions } from './api'
export type { AxiosRequestConfig } from 'axios'
// ui/ 子模块
export { showToast, showAlert, showConfirm, configureToast } from './ui'

查看文件

@ -63,6 +63,8 @@ export const XuqmSDK = {
imEnabled: remote.imEnabled as boolean | undefined,
pushEnabled: remote.pushEnabled as boolean | undefined,
licenseEnabled: remote.licenseEnabled as boolean | undefined,
logApiUrl: remote.logApiUrl as string | undefined,
logEnabled: remote.logEnabled as boolean | undefined,
})
configureHttp({
baseUrl: (remote.apiUrl as string | undefined) ?? baseUrl,

查看文件

@ -0,0 +1,73 @@
import { Alert } from 'react-native'
let _toastImpl: ((msg: string) => void) | null = null
/**
* Toast
* Alert宿 configureToast
*/
export function showToast(message: string): void {
if (_toastImpl) {
_toastImpl(message)
return
}
Alert.alert('', message)
}
/**
*
*/
export function showAlert(options: {
title?: string
message: string
confirmText?: string
onConfirm?: () => void
}): void {
const { title = '提示', message, confirmText = '知道了', onConfirm } = options
Alert.alert(title, message, [
{
text: confirmText,
onPress: onConfirm,
},
])
}
/**
* +
*/
export function showConfirm(options: {
title?: string
message: string
confirmText?: string
cancelText?: string
onConfirm?: () => void
onCancel?: () => void
}): void {
const {
title = '提示',
message,
confirmText = '确定',
cancelText = '取消',
onConfirm,
onCancel,
} = options
Alert.alert(title, message, [
{
text: cancelText,
style: 'cancel',
onPress: onCancel,
},
{
text: confirmText,
onPress: onConfirm,
},
])
}
/**
* 宿 Toast
* showToast 使 Alert
*/
export function configureToast(impl: (msg: string) => void): void {
_toastImpl = impl
}

查看文件

@ -0,0 +1 @@
export { showToast, showAlert, showConfirm, configureToast } from './feedback'

查看文件

@ -1,3 +1,7 @@
import { Alert, Platform, ToastAndroid } from 'react-native'
import { getDeviceInfo } from '../device'
import { getUserInfo } from '../config'
import { _getToken } from '../http'
import type { XWebViewConfig } from './types'
export type XWebViewControllerAPI = {
@ -10,6 +14,176 @@ export type XWebViewControllerAPI = {
getTitle: () => string
}
// ─── Navigation ref (宿主注入) ─────────────────────────────────────────────────
export type XWebViewNavigationRef = {
navigate: (route: string, params?: Record<string, unknown>) => void
goBack: () => void
}
let _navigationRef: XWebViewNavigationRef | null = null
/**
* 宿 App
* JSBridge `xuqm.openNativePage` / `xuqm.closeWebView` 使
*/
export function setXWebViewNavigationRef(ref: XWebViewNavigationRef | null): void {
_navigationRef = ref
}
// ─── ScanQRCode handler (宿主注入) ────────────────────────────────────────────
export type ScanQRCodeHandler = () => Promise<string>
let _scanQRCodeHandler: ScanQRCodeHandler | null = null
/**
* 宿 App
* JSBridge `xuqm.scanQRCode` 使
* handler
*/
export function setXWebViewScanQRCodeHandler(handler: ScanQRCodeHandler | null): void {
_scanQRCodeHandler = handler
}
// ─── JSBridge response helpers ─────────────────────────────────────────────────
type BridgeResponse = { code: 0; data: unknown } | { code: -1; message: string }
function ok(data: unknown): BridgeResponse {
return { code: 0, data }
}
function fail(message: string): BridgeResponse {
return { code: -1, message }
}
function showToast(message: string): void {
if (Platform.OS === 'android') {
ToastAndroid.show(message, ToastAndroid.SHORT)
} else {
Alert.alert('', message)
}
}
// ─── JSBridge message processor ───────────────────────────────────────────────
/**
* WebView JSBridge action `xuqm.`
*
* WebView `onMessage` bridge
* `true` userOnMessage `false`
*
* @param rawData WebViewMessageEvent.nativeEvent.data
* @param postMessageToWeb WebView injectJavaScript
*/
export async function processJSBridgeMessage(
rawData: string,
postMessageToWeb: (js: string) => void,
): Promise<boolean> {
let parsed: {
__xuqm?: string
id?: string | number
params?: Record<string, unknown>
}
try {
parsed = JSON.parse(rawData)
} catch {
return false
}
const action = parsed.__xuqm
if (typeof action !== 'string' || !action.startsWith('xuqm.')) {
return false
}
const id = parsed.id
const params = parsed.params ?? {}
function respond(result: BridgeResponse): void {
const js = `
(function(){
var e = new CustomEvent('xuqm_bridge_response_${String(id ?? '')}', { detail: ${JSON.stringify(result)} });
window.dispatchEvent(e);
if (window.__xuqmBridgeCallback) {
window.__xuqmBridgeCallback(${JSON.stringify(String(id ?? ''))}, ${JSON.stringify(result)});
}
})();
true;`.trim()
postMessageToWeb(js)
}
try {
switch (action) {
case 'xuqm.getDeviceInfo': {
const info = await getDeviceInfo()
respond(ok(info))
break
}
case 'xuqm.getToken': {
const token = await _getToken()
respond(ok(token))
break
}
case 'xuqm.getUserInfo': {
const info = getUserInfo()
respond(ok(info))
break
}
case 'xuqm.openNativePage': {
const route = params.route
if (typeof route !== 'string') {
respond(fail('params.route is required'))
break
}
const navParams = params.params as Record<string, unknown> | undefined
if (!_navigationRef) {
respond(fail('navigation not available — call setXWebViewNavigationRef() in host app'))
break
}
_navigationRef.navigate(route, navParams)
respond(ok(null))
break
}
case 'xuqm.closeWebView': {
if (!_navigationRef) {
respond(fail('navigation not available — call setXWebViewNavigationRef() in host app'))
break
}
_navigationRef.goBack()
respond(ok(null))
break
}
case 'xuqm.showToast': {
const message = params.message
if (typeof message !== 'string') {
respond(fail('params.message is required'))
break
}
showToast(message)
respond(ok(null))
break
}
default:
// Unknown xuqm.* action — not handled by built-in bridge
return false
}
} catch (err) {
respond(fail(err instanceof Error ? err.message : String(err)))
}
return true
}
// ─── Config + Controller ───────────────────────────────────────────────────────
let _config: XWebViewConfig = {}
let _controller: XWebViewControllerAPI | null = null