Chapitre 12.5 — ⭐ EAS Update : les mises à jour OTA
Où on en est : Drill est installé en release sur ton téléphone. Voici la pièce qui rend la voie perso VIABLE : corriger un bug ou améliorer un écran sans rebuilder ni réinstaller — le déploiement web, dans une app native.
⏱️ TL;DR — Le ch. 1.2 l’avait promis : le bundle JS est remplaçable. EAS Update publie un nouveau bundle sur un channel ; l’app installée le télécharge à l’ouverture et l’applique au lancement suivant. Ce qui passe en OTA : TOUT le JS + les assets bundlés. Ce qui ne passe JAMAIS : le natif (modules, config, icône) — gardé cohérent par la runtimeVersion. Workflow :
eas update --channel production -m "fix scoring"→ tous les téléphones ont le fix demain matin.
🎯 Objectifs
- Installer et configurer expo-updates (channels, runtimeVersion).
- Publier une OTA et la voir arriver sur le téléphone.
- Comprendre la frontière absolue OTA/natif et la runtimeVersion qui la garde.
- Régler la politique de chargement (checkAutomatically, fallbackToCacheTimeout).
Le setup
npx expo install expo-updates
eas update:configure # remplit app.config.ts (updates.url, runtimeVersion) + eas.json (channels)Ce que la config ajoute :
// app.config.ts — ajouté par eas update:configure
updates: {
url: 'https://u.expo.dev/<projectId>',
},
runtimeVersion: { policy: 'appVersion' }, // ← la clé de voûte, expliquée plus bas// eas.json — chaque profil de build reçoit son channel
"preview": { "channel": "preview", … },
"production": { "channel": "production", … }Le modèle mental : un binaire est construit branché sur un channel (l’étiquette cuite
dans l’APK). eas update --channel X publie un bundle sur cette étiquette : tous les
binaires branchés sur X le récupèrent. Nos deux channels : preview (l’APK perso) et
production (le futur Play Store) — on peut tester une OTA sur son téléphone avant de la
pousser au monde.
Un rebuild s’impose une dernière fois (expo-updates est un module natif !) :
eas build --profile preview --platform android --local → install. C’est le DERNIER rebuild
« obligatoire » — tout le JS d’après passera par OTA.
Publier
eas update --channel preview --message "Timer : fix du pulse sous 5s"La CLI bundle le JS (Metro, comme un build), uploade bundle + assets, publie sur le channel. Sur le téléphone : à la prochaine ouverture, l’app télécharge en arrière-plan, et applique au lancement suivant (comportement par défaut — réglable ci-dessous). Délai réel constaté : vous poussez à 23 h, le téléphone a le fix au café du matin.
La frontière absolue : OTA vs natif
| Passe en OTA ✅ | Ne passe JAMAIS ❌ (→ rebuild + réinstall) |
|---|---|
| Tout votre TS/JS (écrans, logique, styles) | Ajout/retrait d’un module NATIF |
| Assets bundlés (images require, sons) | app.config.ts natif (icône, splash, permissions, scheme) |
| Corrections, features JS entières | Montée de SDK Expo / RN |
| Le fallback bundlé de questions (ch. 7.4) | Changement de versionCode/signature |
Le garde-fou est la runtimeVersion : l’app ne télécharge que les updates dont la
runtimeVersion correspond à la sienne. Policy appVersion = la runtime suit version
d’app.config.ts. La discipline qui en découle :
- Changement natif (nouveau module, config) → bump
version(1.2.0 → 1.3.0) → rebuild → les OTA suivantes ciblent 1.3.0, les vieux binaires 1.2.0 ne reçoivent RIEN d’incompatible. (Le schemaVersion du ch. 7.3, appliqué au conteneur — même idée.) - Changement JS pur →
eas update, version inchangée.
Oublier le bump = pousser du JS qui appelle un module natif absent = crash au lancement chez tous les destinataires. C’est LE piège d’EAS Update — la runtimeVersion existe pour le rendre impossible, à condition de respecter la discipline du bump.
La politique de chargement
Le défaut (télécharger en fond, appliquer au prochain lancement) est le bon pour Drill : jamais d’attente utilisateur. Les réglages à connaître :
// app.config.ts
updates: {
url: …,
checkAutomatically: 'ON_LOAD', // vérifier à chaque ouverture (défaut sain)
fallbackToCacheTimeout: 0, // 0 = ne JAMAIS faire attendre (lancer l'existant)
},Et pour les cas « fix critique à appliquer TOUT DE SUITE », l’API impérative :
import * as Updates from 'expo-updates'
const update = await Updates.checkForUpdateAsync()
if (update.isAvailable) {
await Updates.fetchUpdateAsync()
await Updates.reloadAsync() // applique immédiatement (à proposer, pas imposer)
}Le pattern poli : un petit banner « Mise à jour prête — Redémarrer ? » sur l’écran d’accueil
(jamais au milieu d’une partie !). Updates.updateId/createdAt alimentent l’écran « À
propos » des Réglages — savoir quelle version JS tourne vaut de l’or en debug.
⚠️ Piège — En développement (Metro), expo-updates est inactif : les OTA ne se testent QUE sur un build release installé. Le rituel de test : build preview → install →
eas update --channel preview→ fermer/rouvrir l’app DEUX fois (téléchargement, puis application) → vérifier le changement. Et surveillez le dashboard expo.dev/updates : il liste qui a quoi. Plan gratuit : confortable pour un usage perso (limites sur le nombre d’utilisateurs actifs mensuels — largement au-dessus de votre famille).
💡 Pour un dev Next.js — C’est LE moment « le mobile rejoint le web » :
eas update≈vercel deploy. Votre réflexe « je fixe, je pousse, c’est en ligne » fonctionne enfin pour une app native — avec la nuance de la frontière natif (inexistante sur le web) et du double lancement. Pour vos apps perso, ça change TOUT : le coût marginal d’une amélioration tombe à zéro.
✏️ Exercices
1. Le rituel de test complet : build+install preview, changez un texte visible (le
sous-titre de l’accueil), eas update --channel preview, et suivez l’arrivée (deux
ouvertures). Vérifiez updateId dans un écran À propos improvisé.
✅ Solution
Après la 2e ouverture, le texte change ; Updates.updateId (affiché dans Réglages → À
propos) passe de null/embedded à l’id de l’update. Si rien n’arrive : vérifier le channel du
binaire (eas build:list — champ channel), la runtimeVersion des deux côtés (dashboard), et
que l’app est bien un build release (pas Metro).
2. Scénario à trier : (a) fix du calcul de streak, (b) nouvel écran de stats, (c) ajout d’expo-camera pour un mode photo, (d) nouvelle icône, (e) nouveau thème de questions dans le repo GitHub. OTA, rebuild, ou ni l’un ni l’autre ?
✅ Solution
(a) OTA. (b) OTA (du JS pur, même un écran entier). (c) Rebuild + bump version (module natif). (d) Rebuild (l’icône est cuite au build). (e) Ni l’un ni l’autre : les questions vivent sur GitHub (ADR 0001) — un git push suffit, l’app les fetch. Trois canaux de mise à jour, trois vitesses : données (instantané), JS (heures), natif (réinstallation).
3. Implémentez le banner « Mise à jour prête » : vérification au focus de l’accueil, proposition de reload, jamais pendant une partie.
✅ Solution
const [ready, setReady] = useState(false)
useFocusEffect(useCallback(() => {
Updates.checkForUpdateAsync()
.then(u => u.isAvailable ? Updates.fetchUpdateAsync().then(() => setReady(true)) : null)
.catch(() => {}) // silence : l'OTA n'est jamais une erreur visible
}, []))
{ready && (
<Card className="mb-4 flex-row items-center justify-between">
<AppText variant="caption">Mise à jour prête</AppText>
<Button label="Redémarrer" variant="ghost" onPress={() => Updates.reloadAsync()} />
</Card>
)}Sur l’accueil uniquement (jamais /quiz — le focus effect y est absent par construction). Le catch silencieux : une vérification d’update qui échoue (offline…) ne doit JAMAIS déranger.
🧠 Quiz
1. Quel principe du ch. 1.2 rend l’OTA possible ?
Réponse
La séparation binaire natif / bundle JS : le bundle est un artefact remplaçable — EAS Update en livre un nouveau, le conteneur natif ne bouge pas.
2. Qu’est-ce qu’un channel, et où est-il fixé ?
Réponse
L’étiquette de distribution cuite dans le binaire au build (via le profil eas.json) :
eas update --channel X sert tous les binaires branchés sur X. Nos deux : preview et
production.
3. Que garantit la runtimeVersion ?
Réponse
Qu’un binaire ne reçoit que des bundles compatibles avec son natif : policy appVersion → tout changement natif s’accompagne d’un bump de version → les vieux binaires sont isolés des JS qui les crasheraient.
4. Quand une OTA publiée est-elle visible à l’écran ?
Réponse
Par défaut : téléchargée à une ouverture, appliquée au lancement suivant (d’où « fermer/ rouvrir deux fois » en test). Updates.reloadAsync() permet l’application immédiate — à proposer poliment.
5. Citez les trois canaux de mise à jour de Drill et leurs vitesses.
Réponse
Les questions (git push → GitHub raw : minutes), le JS (eas update : à la prochaine ouverture), le natif (rebuild + réinstallation : quand il faut). Chaque changement passe par le canal le plus léger possible.
👉 Chapitre suivant : 12.6 — Versionner & vivre avec — la routine de croisière : quel changement, quel canal, quelle version.