import AsyncStorage from '@react-native-async-storage/async-storage' import { Linking, Platform } from 'react-native' import { apiRequest, getConfig } from '@xuqm/rn-common' import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion' export interface PluginMeta { moduleId: string version: string } export interface AppUpdateInfo { needsUpdate: boolean versionName?: string versionCode?: number downloadUrl?: string changeLog?: string forceUpdate?: boolean appStoreUrl?: string marketUrl?: string } export interface RnUpdateInfo { needsUpdate: boolean latestVersion: string downloadUrl: string md5: string minCommonVersion: string note: string } export interface CachedRnBundle { moduleId: string version: string md5: string downloadedAt: string source: string } const _pluginRegistry = new Map() function bundleCacheKey(moduleId: string) { return `@xuqm:bundle:${moduleId}` } function normalizeDownloadUrl(rawUrl?: string): string | undefined { if (!rawUrl) return rawUrl if (rawUrl.includes('/api/v1/updates/api/v1/rn/files/')) { return rawUrl.replace('/api/v1/updates/api/v1/rn/files/', '/api/v1/rn/files/') } if (rawUrl.includes('/files/apk/')) { try { const url = new URL(rawUrl) if (url.pathname.startsWith('/files/apk/')) { return `${url.origin}/api/v1/updates${url.pathname}${url.search}` } } catch {} } return rawUrl } export const UpdateSDK = { /** * Register a plugin's metadata. Call this at the top of the plugin's bundle entry file. * * @example * // In your plugin's index.ts: * import meta from './plugin.json' * UpdateSDK.registerPlugin(meta) */ registerPlugin(meta: PluginMeta): void { _pluginRegistry.set(meta.moduleId, meta) }, /** * For dev/simulator environments where the native XuqmVersionModule is not linked. * Do NOT call this in production — the native module provides the value automatically. */ _devSetAppVersion(versionCode: number, versionName?: string): void { _devSetAppVersion(versionCode, versionName) }, /** * Check if there is a newer App version available. * App version is read automatically from native code (XuqmVersionModule). */ async checkAppUpdate(): Promise { const config = getConfig() const currentVersionCode = getAppVersionCode() const result = await apiRequest('/api/v1/updates/app/check', { skipAuth: true, params: { appId: config.appId, platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', currentVersionCode: String(currentVersionCode), }, }) return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) } }, async openStore(appStoreUrl?: string, marketUrl?: string): Promise { const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl if (url) await Linking.openURL(url) }, /** * Check if a newer RN bundle exists for the given plugin. * The plugin must have been registered via registerPlugin() first. */ async checkRnUpdate(moduleId: string): Promise { const config = getConfig() const meta = _pluginRegistry.get(moduleId) if (!meta) { throw new Error( `[UpdateSDK] Plugin "${moduleId}" not registered. ` + 'Call UpdateSDK.registerPlugin({ moduleId, version }) at bundle load time.', ) } const result = await apiRequest('/api/v1/rn/update/check', { skipAuth: true, params: { appId: config.appId, moduleId, platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', currentVersion: meta.version, }, }) return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl } }, async downloadRnBundle(downloadUrl: string): Promise { const response = await fetch(downloadUrl) if (!response.ok) throw new Error(`[UpdateSDK] Bundle download failed: ${response.status}`) return response.text() }, async cacheRnBundle(moduleId: string, version: string, md5: string, source: string): Promise { const payload: CachedRnBundle = { moduleId, version, md5, source, downloadedAt: new Date().toISOString(), } await AsyncStorage.setItem(bundleCacheKey(moduleId), JSON.stringify(payload)) return payload }, async getCachedRnBundle(moduleId: string): Promise { const raw = await AsyncStorage.getItem(bundleCacheKey(moduleId)) return raw ? (JSON.parse(raw) as CachedRnBundle) : null }, /** Returns the currently running version of a registered plugin. */ getRegisteredPluginVersion(moduleId: string): string | undefined { return _pluginRegistry.get(moduleId)?.version }, /** Returns the current app versionCode (read from native). */ getAppVersionCode, getAppVersionName, }