11 octobre 2024
Gérez les notifications dans une WebView avec React Native
11 minutes de lecture
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
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!