- 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>
159 行
4.8 KiB
TypeScript
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,
|
|
}
|