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 cancelDownload(): Promise openInstallPermissionSettings(): Promise } 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 { 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 { await getModule().cancelDownload() }, openInstallPermissionSettings(): Promise { return getModule().openInstallPermissionSettings() }, }