114 行
3.3 KiB
TypeScript
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()
|
||
|
|
},
|
||
|
|
}
|