XuqmGroup-RNSDK/packages/update/src/UpdateSDK.ts
XuqmGroup febefc8d69 refactor: SDK monorepo with modular packages + clean init API
- Restructure as yarn workspace with packages/common, im, push, update
- @xuqm/rn-common: built-in URLs (no apiBaseUrl/imWsUrl in init), init({appId, debug})
- @xuqm/rn-im: login(userId) handles token internally, no token in public API
- @xuqm/rn-update: registerPlugin({moduleId,version}) for self-registration,
  checkAppUpdate() auto-detects version via XuqmVersionModule native bridge,
  checkRnUpdate(moduleId) uses registered version (no app-layer arg)
- Add XuqmVersionModule native stubs for Android/iOS
- Keep @xuqm/rn-sdk as convenience meta-package re-exporting all

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 16:16:31 +08:00

159 行
4.8 KiB
TypeScript

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<string, PluginMeta>()
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<AppUpdateInfo> {
const config = getConfig()
const currentVersionCode = getAppVersionCode()
const result = await apiRequest<AppUpdateInfo>('/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<void> {
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<RnUpdateInfo> {
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<RnUpdateInfo>('/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<string> {
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<CachedRnBundle> {
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<CachedRnBundle | null> {
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,
}