Sep 4, 2024

Expo update

Mettez à jour instantanément tous vos utilisateurs sans passer par la case validation de Google et Apple

Expo update

Mise en place d'Expo Update

Dans cet article, je vais détailler la mise en place de la fonctionnalité Expo Update dans notre application React Native. Expo Update permet d'utiliser la mise à jour OTA (Over-The-Air) pour déployer rapidement des changements à vos utilisateurs sans avoir à passer par une re-soumission sur les stores. Tout comme un site web, cette mise à jour ne sera effective chez vos utilisateurs dès qu'ils auront "rechargé" leur app. 

Je vous explique dans cet article comment détecter la présence d'une mise à jour et inciter l'utilisateur à relancer son app. 

Voici ci-dessous un schema de ce qui va se dérouler

Un push sur mes branches git déclenche une github action qui va générer un update pour les channels liées à mes branches

🚨 A bien noter

Vous ne pouvez utiliser Expo update que pour les mises à jour qui ne concernent que le code Javascript de votre app. Si vous touchez au code natif ou que vous ajoutez un nouveau module natif (bluetooth par exemple). Vous devrez re-soumettre un build.

Étape 0: Installation d'expo-updates

npx expo install expo-updates

Étape 1: Configuration du fichier eas.json

Le fichier eas.json est le point central de la configuration des builds et updates dans Expo. Voici un exemple de sa configuration pour différents environnements et branches de développement:

Fichier eas.json :

{
  "build": {
    "develop": {
      "channel": "develop",
      "distribution": "internal",
      "env": {
        "APP_NAME": "masuperapp-dev",
        "API_URL": "https://masuperapp.api.dev/"
      }
    },
    "staging": {
      "channel": "staging",
      "distribution": "internal",
      "env": {
        "APP_NAME": "masuperapp",
        "API_URL": "https://masuperapp.api.staging/"
      }
    },
    "main": {
      "channel": "main",
      "env": {
        "APP_NAME": "masuperapp",
        "API_URL": "https://masuperapp.api.prod/"
      }
    }
  }
}

L'idée est de définir différentes configurations pour chaque environnement (développement, staging, production, etc.). Chaque configuration pointe vers un canal (ou channel) spécifique. Ces canaux permettent de gérer efficacement les mises à jour OTA.

Dans notre cas, il existe une version de build pour chaque branche principales de mon git : develop, staging et main. Tout comme pour un site web, je peux tester sur mon app "develop" les développements en cours. Sur staging, je teste les version avant leur mise en prod. La propriété "distribution": "internal" indique qu'il s'agit de build installables directement sur les devices sans passer par le store. 

Étape 2: Gestion des mises à jour dans le code

Nous allons implémenter un mécanisme pour vérifier et appliquer les mises à jour quand l'application passe en avant-plan.

J'ai choisi de créer un wrapper que nous importerons ensuite dans notre fichier /app/_layout.tsx où sont importés mes autres wrapper et providers. 

Fichier wrappers/UpdateWrapper.tsx :

import * as Updates from 'expo-updates';
import { type PropsWithChildren, useEffect, useState } from 'react';
import { AppState } from 'react-native';
import { UpdateModal } from '@/components';

/**
 * Avec l'API EAS Update, nous pouvons vérifier les mises à jour et effectuer une mise à jour silencieuse si nécessaire
 * Cette fonction est appelée lorsque l'application passe en avant-plan
 * https://expo.dev/changelog/2023/08-08-use-updates-api
 * https://docs.expo.dev/build/updates/
 */
const useEASUpdate = () => {
  const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);

  const downloadUpdate = async () => {
    await Updates.fetchUpdateAsync().then(async () => {
      await Updates.reloadAsync();
    });
  };

  /**
   * Vérifier les mises à jour lorsque l'application passe en avant-plan
   */
  useEffect(() => {
    AppState.addEventListener('change', (state) => {
      if (state === 'active') {
        void Updates.checkForUpdateAsync().then(async (update) => {
          if (update.isAvailable) {
            setIsUpdateAvailable(true);
          }
        });
      }
    });
  }, []);

  return { isUpdateAvailable, downloadUpdate };
};

/**
 * Ce wrapper est responsable de vérifier l'état de l'application (maintenance, mise à jour, etc.)
 * et de rediriger l'utilisateur vers la bonne page
 */
export default function UpdateWrapper(props: PropsWithChildren) {
  const [hasSeenUpdateModal, setHasSeenUpdateModal] = useState(false);

  const { isUpdateAvailable, downloadUpdate } = useEASUpdate();

  return (
    <>
      {props?.children}
      <UpdateModal
        // Nous affichons la modal de mise à jour douce uniquement si l'utilisateur ne l'a pas encore vue. Elle s'affiche sinon à chaque écran
        isVisible={Boolean(isUpdateAvailable && !hasSeenUpdateModal)}
        onDismiss={() => {
          // Nous ne voulons pas afficher la popup sur chaque écran ou chaque fois que l'application passe en avant-plan
          setHasSeenUpdateModal(true);
        }}
        onPress={() => {
          if (isUpdateAvailable) {
            void downloadUpdate();
          }
        }}
      />
    </>
  );
}

Le composant UpdateModal est une simple modale avec un callback sur le CTA. Elle est visible si une mise à jour est disponible.

Étape 3: Automatisation avec GitHub Actions

Nous voulons automatiser les mises à jour OTA lorsqu'il y a des changements dans certaines branches Git. Pour cela, nous configurons GitHub Actions comme suit :

Fichier .github/workflows/update.yml :

on:
  push:
    branches:
      - develop
      - staging
      - main
jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - name: 🏗 Setup repo
        uses: actions/checkout@v3
      - name: 🏗 Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 18.x
          cache: npm
      - name: 🏗 Setup EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - name: Cache dependencies
        id: cache
        uses: actions/cache@v3
        with:
          path: ./node_modules
      - name: 📦 Install dependencies
        run: npm ci --ignore-scripts

      # Create a new update if there are no changes in the package.json or package-lock.json
      - name:  Create update
        run: export GIT_COMMIT_HASH=$(git rev-parse HEAD) && eas update --auto --platform all --non-interactive
 

Pour la configuration de secrets.EXPO_TOKEN, lisez la doc suivante :

https://docs.expo.dev/accounts/programmatic-access/#personal-access-tokens

Étape 4 : Créer un premier build

Eh oui ! Comme nous venons d'ajouter un nouveau module implicant des modification au niveau natif, nous devons d'abord générer un nouveau build. 

En réalité 3 nouveau builds. Un pour chaque branche.

git checkout develop
eas build --platform all --profile develop

git checkout staging
eas build --platform all --profile staging

git checkout main
eas build --platform all --profile main

Le profile correspond au nom donné à chaque profile de builds dans eas.json

Vous pouvez suivre l'état d'avancement de vos builds dans le dashboard Expo

Installez ensuite vos builds sur des devices. Je vous laisse fouiller la doc pour savoir comment faire. Ce n'est pas l'objet de l'article ici.

Y a plus qu'à

Désormais, lorsque vous pusherez sur les branches définies dans eas.json, la github action déclenchera un update sur les builds correspondants que vous venez de générer. 

Vous avez une config prête pour utiliser Expo update. Il est néanmoins important de bien comprendre certaines choses avant de l'utiliser. 

Je vous conseille d'ailleurs de ne pas déclencher d'update sur la branche de production dans un premier temps. 

Gestion des versions

Vous avez remarque la présence de

export GIT_COMMIT_HASH=$(git rev-parse HEAD)

à la fin de ma github action ? 

Cette ligne permet d'afficher le numéro de commit sur un écran côté front en utilisant process.env.GIT_COMMIT_HASH. Je vous conseille vivement de l'utiliser pour être capable d'identifier clairement quelle version du code tourne chez vos utilisateurs. C'est hyper utile également pour vos testeurs. 

Je vous conseille également de bien versionner vos livraisons en utilisant par exemple release-please

https://github.com/googleapis/release-please

Version de build !== Version du code

Lorsque vous commencerez à utiliser Expo Update, vous devez bien différencier les numéros de version identifiant vos builds des numéros de version de votre code. 

En effet, Expo Update ne mettra pas à jour le numéro de version du build, puisqu'il ne peux pas agir sur le build en lui même. 

Exemple :

  • Vous déployez pour le première fois votre appli sur les stores. 
  • Vous devez donc génerer un build qui portera le numéro de version 1.0.0
  • Vous développez de nouvelles features qui n'impactent pas la partie native
  • Vous faites une release de la version 1.1.0.
  • Vous faites un update grâce à Expo Update
  • Les utilisateurs auront toujours votre build 1.0.0 mais auront comme version du code 1.1.0

Execution manuelle d'Expo Update

Il est tout à fait possible de lancer un update depuis un terminal en executant

eas update --auto

Cela créera un update automatiquement sur la branche concernée. 

Cela peut être très utile pour faire un rollback rapide après une mise en prod ratée par exemple. 

⚠️ Mais attention !!!  Lorsque vous exécutez cette commande, expo-updates va prendre en compte es variables d'environnement renseignées dans votre .env local.  Faites donc très attention à ne pas pusher en prod des valeurs destinées aux environnement de dev. 

Ne mettre à jour qu'une partie des utilisateurs d'un même build

Il est possible de mettre à jour qu'un certain pourcentage d'utilisateur. Cela peut être utile lors de la publication d'une feature sensible que vous aimeriez faire tester qu'à un nombre limité d'user pour éviter trop de retours négatifs en cas de bug. 

Branche !== Channel

Jusqu'ici, nous avons un peu considéré qu'une branche git équivalait grosso modo a un channel côté expo. Nous les avons effectivement nommées de manière identique pour pus de simplicité. 

Mais vous pouvez en réalité lier plusieurs branches git à une seule channel. Cela vous permettra de gérer un update en ne mettant qu'un certain pourcentage d'utilisateur sur la nouvelle version. 

Dans mon cas, lorsque je génère une nouvelle release, cela me génère une nouvelle branche avec le nom de la version. (J'ai volontairement simplifié).

Dans votre dashboard expo, allez sur déploiements puis sélectionnez votre channel de production (ici main). Vous pouvez alors sélectionner 2 branches pour un même channel et éditer le pourcentage d'utilisateur sur chaque branche. 

Une fois que vous avez validé votre test auprès de ce pourcentage d'utilisateur (ceux sur la branche v1.XX), vous pouvez merger votre branche git correspondante sur main. Un update sera alors déclenché et main et v1.X.X seront alors identiques. 

Vous n'avez plus qu'à redescendre le pourcentage de la branche v1.XX à 0.

Conclusion

Expo update est un outil très puissant pour faciliter le processus de mise à jour de vos apps. Il vous permettra de gagner un temps conséquent. Mais bien comprendre son fonctionnement nécessite du temps. Lisez bien la documentation avant de mettre en place cet outil sur vos environement de production.