import { Capacitor } from '@capacitor/core' import { CapacitorUpdater } from '@capgo/capacitor-updater' import { ElNotification } from 'element-plus' import http from '../api/http' interface AppUpdateCheckResponse { available: boolean version?: string nativeVersion: string platform: 'Android' | 'Ios' channel: 'Production' | 'Staging' downloadUrl?: string releaseNotes?: string fileSize?: number sha256?: string publishedAt?: string } const updateChannel = import.meta.env.VITE_APP_UPDATE_CHANNEL?.trim() || 'production' let checkInProgress: Promise | undefined function absoluteApiUrl(relativePath: string) { const configuredBase = String(http.defaults.baseURL ?? '/api') const publicBase = import.meta.env.VITE_PUBLIC_BASE_URL?.trim() || window.location.origin const apiBase = new URL( configuredBase.endsWith('/') ? configuredBase : `${configuredBase}/`, publicBase, ) return new URL(relativePath, apiBase).toString() } async function checkForAppUpdate() { if (checkInProgress) return checkInProgress if (!navigator.onLine) return checkInProgress = (async () => { try { const platform = Capacitor.getPlatform() if (platform !== 'android' && platform !== 'ios') return const [current, builtin, next] = await Promise.all([ CapacitorUpdater.current(), CapacitorUpdater.getBuiltinVersion(), CapacitorUpdater.getNextBundle(), ]) const currentVersion = current.bundle.id === 'builtin' ? builtin.version : current.bundle.version const { data } = await http.get( '/app-updates/latest', { params: { platform, channel: updateChannel, nativeVersion: current.native, currentVersion, }, // Update discovery is deliberately public. Do not delay startup for a // token refresh or send a stale Authorization header to this endpoint. skipAuthSession: true, timeout: 15000, }, ) if ( !data.available || !data.version || !data.downloadUrl || !data.sha256 || data.version === currentVersion || next?.version === data.version ) { return } const downloaded = await CapacitorUpdater.download({ url: absoluteApiUrl(data.downloadUrl), version: data.version, checksum: data.sha256, }) await CapacitorUpdater.next({ id: downloaded.id }) ElNotification({ title: `新版本 ${data.version} 已就绪`, message: data.releaseNotes || '切换到后台或下次启动应用时将自动启用更新。', type: 'success', duration: 6000, }) } catch (error) { // OTA failure must never stop users from entering the bundled application. console.warn('[app-update] update check failed', error) } finally { checkInProgress = undefined } })() return checkInProgress } export async function initializeAppUpdates() { if (!Capacitor.isNativePlatform()) return try { // Confirm the bundle before any network request. If a newly installed // bundle cannot reach this point, the native plugin rolls it back. await CapacitorUpdater.notifyAppReady() } catch (error) { // Do not let a native-plugin error prevent the bundled app from starting. console.warn('[app-update] bundle readiness confirmation failed', error) } // Every cold start checks the latest public release immediately. A bundle // downloaded here is activated when the app is next launched. void checkForAppUpdate() window.addEventListener('online', () => void checkForAppUpdate()) }