AccueilClientsExpertisesBlogOpen SourceContact

11 octobre 2024

Gérez les notifications dans une WebView avec React Native

11 minutes de lecture

Gérez les notifications dans une WebView avec React Native
🇺🇸 This post is also available in english

Dans cet article, nous allons voir ensemble comment mettre en place un système de notification au sein d'une application React Native utilisant une WebView.

Cet article fait suite à un précédent article sur les WebViews React Native, si vous n'êtes pas familier avec les WebViews, vous pouvez donc vous familiariser avec leurs fonctionnement en consultant cet article. Le but ici étant d'utiliser des notions abordées précédemment pour mettre en place un système de notification.

Pour cela, nous allons utiliser le package react-native-webview et le service de notification Firebase Cloud Messaging (FCM).

Gardez le contact avec vos utilisateurs

Facilement applicable à toutes sortes de projets, les notifications sont un moyen de communication très efficace. Elles permettent de garder le contact avec les utilisateurs, de les informer sur des nouveautés, des promotions, des actions, etc.

Firebase Cloud Messaging

Firebase Cloud Messaging (FCM) est un service de messagerie multi-plateforme qui vous permet d'envoyer des notifications sans coût supplémentaire. Il est possible de cibler des utilisateurs par canaux, par appareils, par groupes, etc.

Concepts fondamentaux

Avant de débuter la mise en place de notre système, il est important de comprendre les concepts fondamentaux de notre architecture. Le schéma suivant illustre les différentes parties de notre système et leurs interactions. On peux distinguer deux étapes principales :

  • L'enregistrement des utilisateurs et de leurs appareils
  • L'envoi de notifications

Schéma de l'architecture

Contexte technique

Pour la mise en place de ce système, nous allons avoir besoin de quatre briques :

  • Une application mobile - React Native avec une WebView
  • Une application front-end - React
  • Un serveur back-end - Node.js
  • Le service tiers de notification - Firebase Cloud Messaging (FCM)

Si vous utilisez des frameworks comme Next.js par exemple, votre application front-end et votre serveur back-end peuvent être regroupés dans un seul projet.

Les applications front-end et back-end seront respectivement appelées client et serveur dans la suite de cet article pour plus de clarté.

Nous n'allons pas détailler la mise en place de ces entités, mais plutôt nous concentrer sur la mise en place de la communication entre elles.

Configuration de Firebase Cloud Messaging

Pour commencer, il faut créer un projet sur Firebase en suivant le guide de création, une fois le projet créé, allez dans les paramètres du projet, sous l'onglet Comptes de service et générez une clé privée en JSON pour pouvoir configurer l'admin de votre système de notification. Et voilà, tout est prêt pour la suite.

Configuration serveur

Firebase Cloud Messaging

Pour la partie serveur, nous allons utiliser le package firebase-admin pour gérer les notifications.

npm install firebase-admin

Ensuite, nous allons initialiser le SDK Firebase avec la clé privée générée précédemment et créer des fonctions pour envoyer des notifications.

// Serveur: firebase.ts
import admin, { ServiceAccount } from 'firebase-admin'
import { Message } from 'firebase-admin/messaging'
import serviceAccount from 'path/to/serviceAccountKey.json'

// On vérifie si l'application Firebase est déjà initialisée
const firebase = !admin.apps.length
  ? admin.initializeApp({
      credential: admin.credential.cert(serviceAccount as ServiceAccount),
    })
  : admin.app()

const messaging = firebase.messaging()

const addConfig = <T extends Message | MulticastMessage>(message: T): T => ({
  ...message,
  android: {
    notification: {
      icon: 'notification_icon',
    },
  },
  apns: {
    payload: {
      aps: {
        alert: {
          ...message.notification,
        },
      },
    },
  },
})

// Envoi d'une notification à un utilisateur
export const sendNotification = async (message: Message) => {
  try {
    await messaging.send(addConfig(message))
  } catch (error) {
    console.error('Error sending message:', error)
  }
}

Ici, le paramètre message contient les informations de la notification, ainsi que les tokens des utilisateurs concernés par la notification.

Pour configurer les notifications, il est possible de passer des paramètres supplémentaires dans le message, comme le titre, le corps, l'icône, etc. Ces paramètres sont spécifiques à chaque plateforme (Android, iOS, Web). On peut retrouver la liste des paramètres disponibles dans la documentation Firebase.

La configuration est obligatoire, la configuration ci-dessus est un exemple minimal pour Android et iOS.

Pour les notifications à destination de plusieurs utilisateurs, il faut découper le nombre de destinataires en lots de 500 et envoyer les notifications en plusieurs fois.

// Serveur: firebase.ts
// Envoi une notification à plusieurs utilisateurs
export const sendNotificationMulticast = async (message: MulticastMessage) => {
  if (!message.tokens.length) return
  try {
    const tokens = message.tokens
    const tokensArrays = []
    while (tokens.length > 500) {
      tokensArrays.push(tokens.splice(0, 500))
    }
    tokensArrays.push(tokens)

    for (const tokensArray of tokensArrays) {
      const messageBody: MulticastMessage = {
        ...addConfig(message),
        tokens: tokensArray,
      }
      await messaging.sendEachForMulticast(messageBody)
    }
  } catch (error) {
    console.log(error)
  }
}

Schéma de données

FCM utilise un système de jetons pour identifier les utilisateurs. Ces jetons sont générés par les applications mobiles et doivent être envoyés au serveur pour être stockés.

La manière d'enregistrer ces jetons dépend de l'application, ils sont souvent stockés dans une base de données et associés à un utilisateur par exemple. C'est important de mettre en place un système de gestion de ces jetons pour pouvoir envoyer des notifications à des utilisateurs spécifiques.

Dans notre cas, pour simplifier l'exemple, on va imaginer que notre structure de données ne contient que les modèles Device et User permettant de gérer les jetons des utilisateurs et une authentification basique.

Un Device est défini par un identifiant unique UUID, un jeton Token et un User associé. Notre API permet de créer, lire, mettre à jour et supprimer des Devices et de login/logout un User.

Configuration client

La partie client va nous permettre d'exposer des méthodes sur l'interface window pour gérer ces Devices depuis la WebView. Nous allons en profiter pour ajouter des postMessage pour communiquer avec la WebView lors d'une connexion ou d'une déconnexion, qui auront pour effet de, respectivement, enregistrer ou supprimer un Device en utilisant les méthodes exposées.

// Client: _app.tsx
import React, { useEffect } from 'react'

const App = () => {
  const postMessage = (message: any) => {
    if (window.ReactNativeWebView) {
      window.ReactNativeWebView.postMessage(JSON.stringify(message))
    }
  }

  const onLoggedIn = async () => {
    postMessage({ isLoggedIn: true })
  }

  const onLoggedOut = async () => {
    postMessage({ isLoggedOut: true })
  }

  useEffect(() => {
    // On expose une méthode pour enregistrer un Device
    window.registerDevice = async (uuid: string, token: string) => {
      try {
        const response = await fetch('http://localhost:3000/devices', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ uuid, token }),
        })
        if (response.ok) {
          console.log('Device registered')
        }
      } catch (error) {
        console.error('Error registering device:', error)
      }
    }
  }, [])

  return <div>App</div>
}

Pourquoi ne pas faire la requête directement dans la WebView ? Les cookies et les headers ne sont pas partagés entre la WebView et l'application React Native, il est donc nécessaire de passer par l'application pour effectuer des requêtes authentifiés par exemple.

Maintenant que notre client est prêt, nous allons pouvoir nous concentrer sur l'application React Native.

Application mobile

Au sein de l'application mobile, nous allons devoir gérer les notifications, leurs permissions, un stockage pour identifier le device et une communication avec la WebView.

Configuration Firebase

Pour utiliser Firebase dans une application React Native, il faut installer le package @react-native-firebase/app et suivre les instructions de configuration.

npm install @react-native-firebase/app @react-native-firebase/messaging
npx expo install expo-build-properties

Et ajouter le plugin dans la configuration de app.json.

{
  ...[other properties]
  "plugins": [
    "@react-native-firebase/app",
    [
      "expo-build-properties",
      {
        "ios": {
          "useFrameworks": "static"
        }
      }
    ]
  ]
}

Permissions

Les notifications nécessitent des permissions pour être affichées, il est donc nécessaire de les demander à l'utilisateur. Sur iOS, la demande de permission utilise le package @react-native-firebase/messaging. Pour Android, on va utiliser la classe PermissionsAndroid de React Native. On va créer un fichier pour chaque, ils vont tout deux exporter une même méthode requestUserPermission qui va demander la permission à l'utilisateur.

// Mobile: utils/permissions.ios.ts
import messaging from '@react-native-firebase/messaging'

async function requestUserPermission() {
  const authStatus = await messaging().requestPermission()

  const enabled =
    authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
    authStatus === messaging.AuthorizationStatus.PROVISIONAL

  return enabled
}

export default requestUserPermission
// Mobile: utils/permissions.android.ts
import { PermissionsAndroid } from 'react-native'

async function requestUserPermission() {
  const enabled = await PermissionsAndroid.request(
    PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
  )
  return enabled === PermissionsAndroid.RESULTS.GRANTED
}

export default requestUserPermission

Bien respecter les noms des fichiers pour que React Native puisse les trouver automatiquement en fonction de la plateforme.

Stockage identifiant unique du device

Pour identifier un device, on va utiliser le package expo-secure-store pour stocker un identifiant unique généré par l'application.

npm install expo-secure-store react-native-uuid
// Mobile: utils/device.ts
import * as SecureStore from 'expo-secure-store'
import uuid from 'react-native-uuid'

const getDeviceUUID = async () => {
  let uuidStored = await SecureStore.getItemAsync('notification-device-uuid')
  if (!uuidStored) {
    uuidStored = uuid.v4().toString()
    await SecureStore.setItemAsync('notification-device-uuid', uuidStored)
  }
  return uuidStored
}

export default getDeviceUUID

Configuration de Firebase

Pour configurer Firebase, il faut ajouter des applications sur la console Firebase et suivre les instructions pour chaque plateforme.

Pour iOS par exemple, il faut ajouter le fichier GoogleService-Info.plist à la racine du projet et pour Android, le fichier google-services.json. Il faut également ajouter les chemins dans le fichier de configuration app.json.

// Mobile app.json
{
  ...
  "ios": {
    "googleServicesFile": "./GoogleService-Info.plist"
    ...
  },
  "android": {
    "googleServicesFile": "./google-services.json"
    ...
}
}

Dans la configuration iOS, il faut ajouter une option Firebase dans le fichier firebase.json et ajouter le paramètre suivant :

// Mobile firebase.json
{
  "react-native": {
    "messaging_ios_auto_register_for_remote_messages": true
  }
}

Initialisation WebView

Nous allons mettre en place la WebView dans notre application mobile en utilisant le package react-native-webview. Nous allons également ajouter des méthodes pour communiquer avec le client.

npm install react-native-webview
// Mobile: App.tsx
import React, { useEffect, useRef } from 'react'
import WebView, { WebViewMessageEvent } from 'react-native-webview'
import messaging from '@react-native-firebase/messaging'

export default function App() {
  const webViewRef = useRef<WebView>(null)

  const onLoadEnd = async () => {
    // On récupère l'identifiant unique du device
    const uuidStored = await getDeviceUUID()

    // On s'assurer que l'utilisateur a bien accepté les notifications
    if (await messaging().hasPermission()) {
      messaging()
        .getToken()
        .then((token) => {
          // On envoie le token au client à travers la méthode exposée
          webViewRef.current?.injectJavaScript(
            `window.registerDevice("${uuidStored}", "${token}");`
          )
        })
        .catch((error) => {
          console.log(error)
        })
    } else {
      // On envoie enlève le token lié au device
      webViewRef.current?.injectJavaScript(`window.registerDevice("${uuidStored}", "");`)
    }
  }

  const onMessage = async (event: WebViewMessageEvent) => {
    try {
      const message = JSON.parse(event.nativeEvent.data)
      let uuidStored = await getDeviceUUID()
      // Lors d'une connexion, on demande la permission à l'utilisateur et on envoie le token au client
      if (message.isLogged) {
        if (await requestUserPermission()) {
          messaging()
            .getToken()
            .then((token) => {
              // On envoie le token au client à travers la méthode exposée
              webViewRef.current?.injectJavaScript(
                `window.registerDevice("${uuidStored}", "${token}");`
              )
            })
        }
      }

      if (message.isLoggedOut) {
        // Lors d'une déconnexion, on enlève le token lié au device
        webViewRef.current?.injectJavaScript(`window.registerDevice("${uuidStored}", "");`)
        await messaging().deleteToken()
      }
    } catch (error) {
      console.log(error)
    }
  }

  return (
    <WebView
      ref={webViewRef}
      source={{ uri: 'http://localhost:3000' }}
      onLoadEnd={onLoadEnd}
      onMessage={onMessage}
    />
  )
}

Désormais nous avons un suivi des tokens de chaque Device avec lesquels les utilisateurs sont connectés.

Recevoir des notifications

Il nous manque encore le système pour recevoir des notifications. Pour cela, nous allons devoir définir le comportement pour les trois états d'une application mobile: en premier plan, en arrière-plan et fermée.

Les notifications reçues par l'application mobile peuvent parfois contenir des données supplémentaires, comme un pathname par exemple, qui permettent de rediriger l'utilisateur vers une page spécifique de la WebView.

Nous allons donc maintenir un état pour stocker le pathname de la WebView, et le modifier si une notification a été ouverte contenant un pathname.

// Mobile: App.tsx
import React, { useEffect, useRef, useState } from 'react'

export default function App() {
  [...]
  // On maintient un état pour stocker le pathname de la WebView
  const [pathname, setPathname] = useState<string | null>('/')

  useEffect(() => {
    // On récupère la notification initiale: application en premier plan
      messaging().getInitialNotification().then(remoteMessage => {
        //Lors de l'ouverture de la notification, on récupère le pathname
        if (remoteMessage?.data?.path) {
          setPath(remoteMessage?.data?.path);
        }
      });

      const unsubscribe =
      // On récupère les notifications lorsque l'application est en arrière-plan
        messaging().onNotificationOpenedApp(remoteMessage => {
          //Lors de l'ouverture de la notification, on récupère le pathname
          if (remoteMessage?.data?.path) {
            setPath(remoteMessage?.data?.path);
          }
        })

      // On récupère les notifications lorsque l'application est fermée
      return unsubscribe;
    }, []);

  return (
    <WebView
      ref={webViewRef}
      source={{ uri: `http://localhost:3000${pathname}` }}
      onLoadEnd={onLoadEnd}
      onMessage={onMessage}
    />
  )
}

On est désormais prêt à recevoir des notifications dans notre application mobile, et ce pour n'importe quel état de l'application.

Maintenant finissons par l'envoi de notifications depuis le serveur.

Envoi de notifications

Pour envoyer des notifications depuis le serveur, nous allons utiliser les tokens des Devices pour cibler les utilisateurs.

// Serveur: index.ts
import { sendNotificationMulticast } from './firebase'

type Device = {
  uuid: string
  token: string
}

type User = {
  id: string
  devices: Device[]
}

// Récupère les utilisateurs
const users: User[] = getUsers()

// On extrait les tokens des devices liés à chaque utilisateur
const tokens = users.flatMap((user) => user.devices.map((device) => device.token))

// On envoie une notification à tous les utilisateurs
sendNotificationMulticast({
  tokens,
  notification: {
    title: 'Nouvelle notification',
    body: 'Vous avez reçu une nouvelle notification',
  },
  data: {
    // Lors de l'ouverture de la notification, on redirige l'utilisateur vers une page spécifique
    path: '/home',
  },
})

Et voilà on est prêt à envoyer des notifications à nos utilisateurs, toujours de manière résonnée bien sûr.

Attention à ne pas exposer les secrets de configuration de Firebase dans le code source, il est préférable de les stocker dans des variables d'environnement.

Déploiement

Maintenant que notre application mobile dépend de fonctionnalités exposées dans notre application web, veillez à ce que la WebView pointe vers une version contenant ces fonctionnalités de votre application web.

La fonctionnalité native de notifications push n'est pas supporté par Expo Go, il est donc nécessaire de construire l'application avec expo build pour tester les notifications.

Alternative

Dans cet article, nous avons utilisé les outils de Firebase pour gérer les notifications, cela fait suite à un retour d'expérience. Il existe cependant d'autres alternatives pour la gestion des notifications, notamment expo-notifications qui fournit une documentation complète du produit et qui peut séduire des utilisateurs habitués aux outils Expo.

Conclusion

Dans cet article, nous avons vu comment mettre en place un système de notification au sein d'une application React Native utilisant une WebView. Nous avons vu comment configurer Firebase Cloud Messaging, gérer les permissions, stocker un identifiant unique pour les devices, communiquer entre l'application mobile et la WebView, et enfin envoyer des notifications depuis le serveur.

L'envoi de notifications devient un jeu d'enfant avec Firebase Cloud Messaging, et la communication entre l'application mobile et la WebView est facilitée par les méthodes exposées sur l'interface window. C'est aussi une fonctionnalité incontournable pour tenir informé vos utilisateurs.

Si cette intégration vous intéresse, ou si vous avez des questions, n'hésitez pas à nous contacter, nous serions ravis de vous aider et de discuter de ce sujet avec vous!

Source - Docs

À découvrir également

Embarquez vos sites Web dans une application React Native avec les WebViews

18 Mar 2024

Embarquez vos sites Web dans une application React Native avec les WebViews

Les WebViews avec React Native, une solution pour intégrer un site web dans une application mobile.

par

Colin

Animation 3D avec React Native

21 Jul 2023

Animation 3D avec React Native

Découverte de l'univers 3D avec Expo-GL, React-three-fiber et Reanimated

par

Laureen

Découverte du client customisé d'Expo

14 Oct 2021

Découverte du client customisé d'Expo

Créé il y a un peu plus de 5 ans, Expo n'a cessé de gagner en popularité auprès des développeurs React-Native.

par

Hugo

Premier Octet vous accompagne dans le développement de vos projets avec react-native

En savoir plusNous contacter
18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub
X (Twitter)
Flux RSS

Naviguez à vue