122 lines
3.6 KiB
TypeScript
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())
|
|
}
|