meteo-exam/web/src/update.ts
Mathis 3ecd57119f fix : mise à jour in-app sans quitter l'application
- @capacitor/filesystem : téléchargement natif de l'APK (OkHttp)
- @capawesome-team/capacitor-file-opener : ouverture de l'installateur Android
- L'APK est écrit dans le cache de l'app puis ouvert via un Intent natif
- Plus de passage par le navigateur — tout se passe dans l'app
- file_paths.xml mis à jour pour couvrir le cache
2026-10-04 20:35:35 +02:00

113 lines
3.5 KiB
TypeScript

import { Capacitor, CapacitorHttp } from '@capacitor/core'
import { Filesystem, Directory } from '@capacitor/filesystem'
import { FileOpener } from '@capawesome-team/capacitor-file-opener'
import { getBaseUrl } from './config'
export interface UpdateInfo {
version: string
apkUrl: string | null
apkSize: number | null
notes: string | null
}
/** Compare deux versions sémantiques. Renvoie 1 si a > b, -1 si a < b, 0 si égales. */
function compareVersions(a: string, b: string): number {
const pa = a.split('.').map(Number)
const pb = b.split('.').map(Number)
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const na = pa[i] ?? 0
const nb = pb[i] ?? 0
if (na > nb) return 1
if (na < nb) return -1
}
return 0
}
/**
* Vérifie si une mise à jour est disponible sur le serveur.
* Seulement sur Android natif : la PWA iOS se met à jour en rechargeant.
*/
export async function checkForUpdate(): Promise<UpdateInfo | null> {
if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== 'android') {
return null
}
const localVersion = __APP_VERSION__
try {
const response = await fetch(`${getBaseUrl()}/api/version`, {
headers: { Accept: 'application/json' },
})
if (!response.ok) return null
const remote = (await response.json()) as UpdateInfo
if (compareVersions(remote.version, localVersion) <= 0) return null
const dismissed = localStorage.getItem('meteo-exam.dismissed-update')
if (dismissed === remote.version) return null
return remote
} catch {
return null
}
}
export const currentAppVersion = (): string => __APP_VERSION__
export function dismissUpdate(version: string): void {
localStorage.setItem('meteo-exam.dismissed-update', version)
}
/**
* Télécharge l'APK via le réseau natif (OkHttp), l'enregistre dans le cache
* de l'app puis ouvre l'installateur Android — sans quitter l'application.
*/
export async function downloadAndInstallApk(apkUrl: string): Promise<void> {
const url = apkUrl.startsWith('http') ? apkUrl : `${getBaseUrl()}${apkUrl}`
// 1. Télécharger le binaire via CapacitorHttp (natif, pas de WebView).
const response = await CapacitorHttp.request({
url,
method: 'GET',
responseType: 'arraybuffer',
})
if (response.status !== 200) {
throw new Error(`Téléchargement impossible (HTTP ${response.status})`)
}
// 2. Écrire le fichier dans le cache de l'app.
// CapacitorHttp renvoie les données en base64 pour responseType arraybuffer.
const data = response.data as string
const binary = atob(data)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
// Convertir en base64 pour Capacitor Filesystem.
let base64 = ''
const chunk = 0x8000
for (let i = 0; i < bytes.length; i += chunk) {
base64 += String.fromCharCode(...bytes.subarray(i, i + chunk))
}
const b64 = btoa(base64)
const fileName = `meteo-exam-${__APP_VERSION__}-update.apk`
await Filesystem.writeFile({
path: fileName,
data: b64,
directory: Directory.Cache,
})
// 3. Lire le chemin réel du fichier sur le système de fichiers.
const { uri } = await Filesystem.getUri({
path: fileName,
directory: Directory.Cache,
})
// 4. Ouvrir l'installateur Android avec ce fichier APK.
// Android affiche l'écran d'installation par-dessus l'app.
await FileOpener.openFile({
path: uri,
mimeType: 'application/vnd.android.package-archive',
})
}