XuqmGroup-RNSDK/packages/update/src/NativeAppUpdate.ts

114 行
3.3 KiB
TypeScript

import { NativeEventEmitter, NativeModules, Platform } from 'react-native'
import type { UpdateDownloadProgress } from './UpdateSDK'
export type AppInstallErrorCode =
| 'DOWNLOAD_FAILED'
| 'DOWNLOAD_CANCELLED'
| 'HASH_MISMATCH'
| 'INSTALL_PERMISSION_REQUIRED'
| 'INSTALLER_UNAVAILABLE'
| 'ANDROID_SDK_MISSING'
| 'INSTALL_FAILED'
export class AppInstallError extends Error {
readonly name = 'AppInstallError'
constructor(
public readonly code: AppInstallErrorCode,
message: string,
options?: ErrorOptions,
) {
super(message, options)
}
}
interface XuqmAppUpdateModuleInterface {
downloadAndInstall(
downloadUrl: string,
versionCode: number,
sha256: string | null,
retryCount: number,
): Promise<void>
cancelDownload(): Promise<void>
openInstallPermissionSettings(): Promise<boolean>
}
function getModule(): XuqmAppUpdateModuleInterface {
const module = NativeModules.XuqmAppUpdateModule
if (!module) {
throw new AppInstallError(
'ANDROID_SDK_MISSING',
'[UpdateSDK] Xuqm Android update bridge is not linked',
)
}
return module as XuqmAppUpdateModuleInterface
}
function asInstallError(error: unknown): AppInstallError {
if (error instanceof AppInstallError) return error
const native = error as { code?: string; message?: string }
const code = native.code as AppInstallErrorCode | undefined
const known: AppInstallErrorCode[] = [
'DOWNLOAD_FAILED',
'DOWNLOAD_CANCELLED',
'HASH_MISMATCH',
'INSTALL_PERMISSION_REQUIRED',
'INSTALLER_UNAVAILABLE',
'ANDROID_SDK_MISSING',
'INSTALL_FAILED',
]
return new AppInstallError(
code && known.includes(code) ? code : 'INSTALL_FAILED',
native.message ?? '[UpdateSDK] App installation failed',
{ cause: error },
)
}
export const NativeAppUpdate = {
async downloadAndInstall(
input: { downloadUrl: string; versionCode: number; sha256?: string },
options: {
signal?: AbortSignal
retryCount?: number
onProgress?: (progress: UpdateDownloadProgress) => void
} = {},
): Promise<void> {
if (Platform.OS !== 'android') {
throw new AppInstallError('INSTALLER_UNAVAILABLE', 'App installation is Android-only')
}
const module = getModule()
if (!input.sha256) {
throw new AppInstallError('HASH_MISMATCH', 'APK SHA-256 is required')
}
const emitter = new NativeEventEmitter(NativeModules.XuqmAppUpdateModule)
const subscription = emitter.addListener(
'XuqmAppUpdateProgress',
(progress: UpdateDownloadProgress) => options.onProgress?.(progress),
)
const cancel = () => void module.cancelDownload()
options.signal?.addEventListener('abort', cancel, { once: true })
try {
if (options.signal?.aborted) throw new AppInstallError('DOWNLOAD_CANCELLED', 'Cancelled')
await module.downloadAndInstall(
input.downloadUrl,
input.versionCode,
input.sha256 ?? null,
Math.max(0, Math.trunc(options.retryCount ?? 1)),
)
} catch (error) {
throw asInstallError(error)
} finally {
subscription.remove()
options.signal?.removeEventListener('abort', cancel)
}
},
async cancel(): Promise<void> {
await getModule().cancelDownload()
},
openInstallPermissionSettings(): Promise<boolean> {
return getModule().openInstallPermissionSettings()
},
}