Progressive Web App : Notification push

Voir la vidéo

Après avoir découvert le principe des progressive web apps je vous propose d'explorer plus en profondeur les service workers avec la mise en place d'un système de notification Push.

Flow du système de notification

Les différentes étapes

On demande la permission

Pour commencer on va vérifier si l'utilisateur n'a pas déjà activé la permission de recevoir des notifications pour la page en cours via Notification.permission et proposer un bouton pour lancer l'activation des notifications sur le site. Lors du clique sur ce bouton on va demander la permission de notifier l'utilisateur.

async function askPermission() {
  const permission = await Notification.requestPermission();
  if (permission === "granted") {
    registerServiceWorker();
  }
}

On abonne l'utilisateur via le PushManager

Si l'utilisateur accepte la permission on va pouvoir l'abonner au système de push offert par le navigateur au travers du PushManager.

Ce PushManager est accessible via l'interface ServiceWorkerRegistration que l'on pourra obtenir lorsque l'on enregistre un Service Worker. Lors de l'abonnement de l'utilisateur il faudra communiquer au navigateur une clef publique VAPID qui permettra d'assurer l'origine des futurs messages Push.

async function registerServiceWorker() {
  const registration = await navigator.serviceWorker.register("/sw.js");
  let subscription = await registration.pushManager.getSubscription();
  // L'utilisateur n'est pas déjà abonné, on l'abonne au notification push
  if (!subscription) {
    subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: await getPublicKey(),
    });
  }

  await saveSubscription(subscription);
}

async function getPublicKey() {
  const { key } = await fetch("/push/key", {
    headers: {
      Accept: "application/json",
    },
  }).then((r) => r.json());
  return key;
}

En réponse à l'abonnement on va récupérer un objet de type PushSubscription qui contiendra des informations sur l'abonnement de l'utilisateur

  • endpoint, qui sera le point d'entré à contacter pour envoyer la notification
  • options, un objet qui contiendra les options utilisées pour crée l'abonnement

On enregistre l'abonnement sur notre serveur

Une fois l'abonnement récupéré, on va pouvoir envoyer les informations à notre serveur qui va enregistrer les informations en base de données.

/**
 * @param {PushSubscription} subscription
 * @returns {Promise<void>}
 */
async function saveSubscription(subscription) {
  await fetch("/push/subscribe", {
    method: "post",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
    },
    body: subscription.toJSON(),
  });
}

Notre serveur recevra un JSON qui ressemblera à :

{
  "endpoint": "https://push.service.ltd/bd2715f4-ca9f-11eb-b8bc-0242ac130003"
  "keys": {
    "p256dh": "BNhaJR_GbGj4oLX7dV1xVIVLSmSo-gQVKhxEx5CNj-JapZg4PyQp6aSh-3SBzzZZcO-z7yIn7qfSvHAzYoLtB6E",
    "auth": "YwWWBhWbopKvge1eBT82AA"
  }
}

Service Worker

Lorsque notre utilisateur recevra une notification le service worker sera notifié au travers d'un évènement push et devra réagir en fonction.

self.addEventListener("install", () => {
  self.skipWaiting();
});

self.addEventListener("push", (event) => {
  const data = event.data ? event.data.json() : {};
  event.waitUntil(self.registration.showNotification(data.title, data));
});

On peut aussi détecter le clic sur la notification et agir en fonction.

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  event.waitUntil(openUrl("http://grafikart.fr"));
});

/**
 * Ouvre l'url ou focus la page qui est déjà ouverte sur cette URL
 * @param {string} url
 **/
async function openUrl(url) {
  const windowClients = await self.clients.matchAll({
    type: "window",
    includeUncontrolled: true,
  });
  for (let i = 0; i < windowClients.length; i++) {
    const client = windowClients[i];
    if (client.url === url && "focus" in client) {
      return client.focus();
    }
  }
  if (self.clients.openWindow) {
    return self.clients.openWindow(url);
  }
  return null;
}

Envoi des notification côté serveur

L'envoi des notifications push se fait côté serveur en contactant le point d'entré reçu lors de l'abonnement de l'utilisateur. Les clefs seront aussi utilisées pour authentifié l'utilisateur et chiffré le message à envoyer au service push. Notre clef privée sera utilisé pour signer le message et prouver qu'on est bien à l'origine de la demande. Le protocole étant assez complexe on pourra se reposer sur des librairies tiers pour simplifier le travail.

Voici un petit exemple pour PHP :

$webPush = new WebPush([
    'VAPID' => [
        'subject' => 'mailto:contact@grafikart.fr',
        'publicKey' => env('VAPID_PUBLIC_KEY'),
        'privateKey' => env('VAPID_PRIVATE_KEY'),
    ],
]);
foreach($user->subscriptions as $subscription) {
    $webPush->queueNotification(
        Subscription::create([
            'endpoint' => $subscription->endpoint,
            'publicKey' => $subscription->public_key,
            'authToken' => $subscription->auth_token,
        ]),
        json_encode([
            'message' => 'Bonjour les gens',
            'title' => 'Mon titre'
        ]);
    );
}
foreach ($webPush->flush() as $report) {
    $endpoint = $report->getRequest()->getUri()->__toString();
    if ($report->isSuccess()) {
        dump("[v] Le message bien été envoyé {$endpoint}.");
    } else {
        dump("[x] Impossible d'envoyer le message {$endpoint}: {$report->getReason()}");
    }
}

Lorsqu'une erreur est obtenue en retour de l'envoi d'un message vous pouvez supprimer l'abonnement de l'utilisateur de votre serveur car cela signifie que l'utilisateur ne souhaite pas (ou ne peux pas suite à une désinstallation) recevoir de notifications.

Publié
Technologies utilisées
Auteur :
Grafikart
Partager