Files
Academic-Affairs-System/web/src/services/appUpdates.ts
T
2026-08-21 22:02:33 +08:00

122 lines
3.6 KiB
TypeScript

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<void> | 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<AppUpdateCheckResponse>(
'/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())
}