Passer au contenu principal

Push Notifications

Ouvrir dans ChatGPT
26.02
Java API

Les notifications push peuvent atteindre les utilisateurs même lorsqu'une application n'est pas ouverte. Le navigateur s'abonne une fois, l'application stocke l'abonnement et le serveur l'utilise pour livrer des notifications lorsqu'un événement se produit. Push gère l'abonnement et la désinscription dans le navigateur. Sur le serveur, PushSender envoie un PushMessage à un abonnement stocké.

Configuration et prérequis​

Les notifications push sont fournies par un module séparé. Ajoutez-le à votre application :

pom.xml
<dependency>
<groupId>com.webforj</groupId>
<artifactId>webforj-push</artifactId>
</dependency>

Les notifications push nécessitent :

  • Un déploiement de servlet, comme Jetty, Spring Boot ou un fichier WAR.
  • Une paire de clés, générée ci-dessous, que le déploiement utilise pour signer les notifications.
  • Une origine sécurisée. Les navigateurs rejettent les abonnements servis par tout autre moyen que https, sauf depuis localhost pendant le développement.
Origines sécurisées

Pour plus d'informations sur les contextes sécurisés et pourquoi ils sont importants, consultez la documentation MDN sur les contextes sécurisés.

Génération des clés​

Les services push n'acceptent que les notifications signées par le déploiement auquel le navigateur s'est abonné. Exécutez le plugin de build une fois pour chaque déploiement afin de générer sa paire de clés :

mvn webforj:push-keys

La commande renvoie trois lignes de configuration. Collez-les dans application.properties sans les guillemets, ou copiez-les telles qu'imprimées dans webforj.conf. Remplacez le sujet par l'adresse de contact du déploiement. Cela doit être une adresse mailto: ou https:// que les services push peuvent utiliser pour contacter l'opérateur.

application.properties
webforj.push.public-key=...
webforj.push.private-key=...
webforj.push.subject=mailto:ops@example.com
PropriétéExplication
webforj.push.public-keyLa moitié publique de la paire de clés utilisée par le déploiement pour signer les notifications
webforj.push.private-keyLa moitié privée de la paire de clés. Comme tout autre secret, gardez-la hors du contrôle de version
webforj.push.subjectL'adresse de contact du déploiement. Cela doit être une adresse mailto: ou https:// par laquelle les services push peuvent atteindre l'opérateur

L'application lit ces propriétés au démarrage. Si la configuration n'en inclut que certaines, le démarrage échoue et indique quelles propriétés sont manquantes.

Rotation des clés

Chaque navigateur s'abonne à une paire de clés. Si les clés changent, le service push rejette les abonnements existants. Le prochain appel subscribe() dans chaque navigateur remplace son abonnement.

Comment ça fonctionne​

Le processus comporte trois étapes :

  1. S'inscrire. Depuis une vue, Push.getCurrent().subscribe() demande la permission de l'utilisateur et renvoie un PushSubscription qui identifie l'adresse du navigateur.
  2. Stocker. L'application enregistre l'abonnement avec ses données et l'associe à l'utilisateur correspondant.
  3. Envoyer. Plus tard, depuis n'importe quel thread, PushSender.send(subscription, message) transmet le message au service push du fournisseur du navigateur. Le service affiche la notification que l'application soit ouverte ou non.
Push.getCurrent().subscribe().thenAccept(subscriptions::save);

sender.send(subscription,
PushMessage.create("Commande expédiée").setUrl("/orders/42").build());

Les sections suivantes expliquent ce que le navigateur affiche et comment gérer les échecs à chaque étape.

Instance​

Récupérez l'instance push pour l'environnement actuel :

import com.webforj.push.Push;

Push push = Push.getCurrent();

if (Push.isPresent()) {
// ...
}

Push.ifPresent(p -> {
// ...
});

Abonnement du navigateur​

Appelez subscribe() en réponse à une action de l'utilisateur, comme cliquer sur un bouton "Activer les notifications". Le PendingResult retourné se termine avec le PushSubscription du navigateur. Si le navigateur ne peut pas s'abonner, il se termine de façon exceptionnelle avec une WebforjPushException.

PendingResult<PushSubscription> request = Push.getCurrent().subscribe();
request.thenAccept(subscription -> {
subscriptions.save(subscription);
});
request.exceptionally(throwable -> {
WebforjPushException error = (WebforjPushException) throwable.getCause();
PushStatus status = error.getStatus();
String message = error.getMessage();

return null;
});

Si le navigateur est déjà abonné, appeler à nouveau subscribe() renvoie l'abonnement existant. Vous pouvez donc l'appeler en toute sécurité à chaque visite.

Permission du navigateur

Le premier appel à subscribe() demande la permission à l'utilisateur. Le navigateur affiche cette invite, elle ne fait pas partie de l'interface utilisateur de l'application. Comme les navigateurs montrent cette invite uniquement en réponse à une action de l'utilisateur, appelez subscribe() à partir d'un écouteur de clic plutôt que du constructeur de la vue.

Si l'utilisateur bloque l'invite, l'application ne peut pas renouveler la demande pour cette origine.

Stockage des abonnements​

Un abonnement représente l'adresse d'un navigateur et appartient au serveur. Stockez-le avec les données de l'application, en utilisant son endpoint comme clé. Incluez toute information dont l'application a besoin pour sélectionner les navigateurs appropriés plus tard, telle que l'utilisateur associé. Chaque abonnement contient trois valeurs textuelles :

ValeurSignification
getEndpoint()L'URL de livraison assignée par le service push du fournisseur du navigateur
getP256dh()La clé publique du navigateur
getAuth()Le secret d'authentification du navigateur

Un utilisateur qui s'abonne depuis deux navigateurs a deux abonnements. Supprimez un abonnement lorsque son navigateur se désabonne ou lorsque l'envoi rapporte qu'il a expiré. Voir Statut d'échec.

Restauration d'un abonnement​

getSubscription() renvoie l'abonnement actuel du navigateur, ou un résultat vide s'il n'existe pas. Utilisez-le pour synchroniser la copie du serveur, par exemple après que le stockage de l'application a été réinitialisé :

Push.getCurrent().getSubscription().thenAccept(existing -> {
existing.ifPresent(subscriptions::save);
});

À travers PushPermission, getPermission() signale si l'utilisateur a accordé, refusé ou n'a pas encore répondu à l'invite de notification. Utilisez ce résultat pour cacher le bouton "Activer les notifications" lorsque le fait de cliquer n'aurait aucun effet.

Désinscription​

unsubscribe() annule l'abonnement du navigateur. Il se termine avec l'abonnement supprimé afin que l'application puisse supprimer sa copie enregistrée, ou avec un résultat vide si le navigateur n'avait pas d'abonnement.

Push.getCurrent().unsubscribe().thenAccept(removed -> {
removed.ifPresent(subscriptions::delete);
});

Envoi de notifications​

PushSender envoie un PushMessage à un abonnement stocké. Il signe le message avec les clés du déploiement et le transmet au service push du fournisseur du navigateur. Ce service réveille le navigateur et affiche la notification. Comme l'opération ne bloque jamais le thread appelant, vous pouvez l'invoquer depuis un écouteur de clic, un job programmé ou un gestionnaire de requêtes.

Après que les propriétés sont configurées, l'expéditeur est disponible en tant que bean que vous pouvez injecter dans des vues, des services et des jobs programmés. Pour le remplacer, définissez votre propre bean PushSender.

@Route("/orders")
public class OrdersView extends Composite<FlexLayout> {

public OrdersView(PushSender sender, PushSubscriptions subscriptions) {
// ...
}
}

Sans Spring, new PushSender() lit les clés de la configuration de l'application. Créez l'expéditeur sur un thread d'application, soit dans une vue, soit dans App.run(), puis utilisez-le depuis n'importe quel thread. Tous les expéditeurs partagent un pool de connexions vers les services push, donc il n'y a pas de coût à en créer un là où il est nécessaire.

Pour les notifications qui doivent être envoyées plus tard ou après que l'utilisateur soit parti, utilisez un minuteur sur le serveur comme TaskScheduler de Spring. N'utilisez pas un minuteur de page tel qu Interval, car il s'arrête lorsque l'onglet se ferme.

Composition d'un message​

Créez un message avec son titre, puis configurez chaque autre option sur le constructeur :

PushMessage message = PushMessage.create("Commande expédiée")
.setBody("La commande #42 est en route")
.setIcon("icons://icon-192x192.png")
.setUrl("/orders/42")
.setActions(List.of(new PushAction("track", "Suivre", "/orders/42/tracking")))
.build();

PendingResult<Void> sent = sender.send(subscription, message);
sent.thenAccept(v -> status.setText("Envoyé"));
sent.exceptionally(throwable -> {
WebforjPushException error = (WebforjPushException) throwable;
status.setText(error.getStatus() + ": " + error.getMessage());

return null;
});

send() retourne immédiatement. Le PendingResult se termine lorsque le service push accepte le message, ou se termine de manière exceptionnelle si le service ne l'accepte pas. Si send() est appelé sur un thread d'application, tel qu'à partir d'un écouteur, ses rappels s'exécutent sur ce thread et peuvent mettre à jour les composants. Si la session qui a appelé send() se termine avant que la réponse n'arrive, les rappels ne s'exécutent pas, mais la notification est toujours livrée.

Un envoi attend jusqu'à 30 secondes pour que le service push réponde avant d'échouer avec UNREACHABLE. Utilisez setTimeout(Duration) pour changer le délai d'attente pour chaque expéditeur.

OptionEffet
setBodyDéfinit le texte affiché sous le titre
setIconDéfinit l'image affichée avec la notification. Elle accepte des URLs absolues ainsi que les protocoles icons:// et ws://. Voir Actifs. Elle n'accepte pas le protocole context:// car les services push limitent un message à 4 Ko
setUrlDéfinit la page qui s'ouvre lorsque l'utilisateur clique sur la notification. Les URLs relatives sont résolues par rapport à la racine de l'application. Si aucune URL n'est définie, la racine de l'application s'ouvre
setActionsDéfinit les boutons affichés sur la notification, avec une URL séparée pour chaque bouton. Voir Support des navigateurs
setTagDéfinit une balise d'identification. Si une notification affichée a la même balise, la nouvelle notification la remplace
setSilentAffiche la notification sans son ni vibration
setTimeToLiveDéfinit combien de temps le service push conserve le message pour un appareil hors ligne, jusqu'à quatre semaines
setUrgencyUtilise PushUrgency pour permettre à l'appareil de retarder les messages de faible urgence et de préserver la batterie
setTopicRemplace un message qui attend encore au service push lorsque les deux messages ont le même sujet. Les sujets peuvent contenir au maximum 32 caractères sûrs dans une URL

Lorsque un onglet affiche déjà la page, cliquer sur la notification focalise l'application. Sinon, la page s'ouvre dans un nouvel onglet. Cliquer sur un bouton de notification ouvre son URL de la même manière.

Une notification par message

Chaque message affiche une notification. Comme les navigateurs ne réveillent pas une page pour un message qui n'affiche rien, les pushes ne peuvent pas être utilisés pour des mises à jour de données silencieuses.

Statut d'échec​

Lorsque subscribe() ou send() échoue, son PendingResult signale une WebforjPushException. PushStatus identifie la raison :

StatutQuandQue faire
PERMISSION_DENIEDL'utilisateur a bloqué les notifications pour l'applicationExpliquez où l'utilisateur peut autoriser les notifications dans les paramètres du navigateur
UNSUPPORTEDLes push ne sont pas pris en charge par le navigateur, la page n'est pas dans un contexte sécurisé, ou l'application n'est pas déployée en tant que servletMasquez la fonctionnalité
NOT_CONFIGUREDAu moins une propriété webforj.push.* est manquante ou incomplèteGénérez les clés et configurez toutes les trois propriétés
SUBSCRIPTION_EXPIREDLe service push ne reconnaît plus l'abonnement parce que l'utilisateur s'est désabonné ou qu'il a réinstallé le navigateurSupprimez l'abonnement stocké
REJECTEDLe service push a rejeté le message ; getStatusCode() contient sa réponseVérifiez les clés et la taille du message
UNREACHABLELe service push n'a pas répondu avant le délai d'attenteRéessayez plus tard
UNKNOWNLe endpoint stocké n'est pas une URL valide, ou l'abonnement ou le message n'ont pas pu être encodésVérifiez l'abonnement stocké

Supprimez les abonnements expirés lors de chaque envoi :

sender.send(subscription, message).exceptionally(throwable -> {
WebforjPushException error = (WebforjPushException) throwable;
if (error.getStatus() == PushStatus.SUBSCRIPTION_EXPIRED) {
subscriptions.delete(subscription);
}

return null;
});
L'expiration arrive avec un message de retard

Les services push désinscrivent les abonnements de manière paresseuse. Ils acceptent toujours le premier message après qu'un utilisateur se soit désabonné, mais ce message n'ira nulle part. Le message suivant rapporte SUBSCRIPTION_EXPIRED. Un envoi accepté signifie que le message a atteint le service push, pas que l'utilisateur l'a vu.

Support des navigateurs​

Tous les principaux navigateurs de bureau et mobiles affichent des notifications push après s'être abonnés. Gardez ces limitations à l'esprit :

  • Sur iPhone et iPad, les push ne fonctionnent que pour les applications web ajoutées à l'écran d'accueil sur iOS 16.4 ou ultérieur. Dans un onglet Safari, subscribe() rapporte UNSUPPORTED. Voir Applications installables pour le manifeste d'application requis.
  • Safari n'affiche pas les boutons de notification. Il affiche les messages avec des actions sans leurs boutons, mais cliquer sur la notification ouvre tout de même l'URL du message.
  • Les WebViews Android et iOS n'affichent pas les notifications.

Pour des détails par navigateur, consultez le tableau de compatibilité showNotification MDN.

Exemple complet​

La vue suivante s'abonne et se désabonne du navigateur, stocke les abonnements en mémoire, et envoie un message à chaque abonnement stocké. Elle peut envoyer immédiatement ou attendre huit secondes en utilisant le TaskScheduler de Spring, permettant à l'onglet de se fermer avant que la notification n'arrive. La classe d'application utilise @EnableScheduling pour rendre le planificateur disponible.

PushSubscriptions.java
package com.example;

import com.webforj.push.PushSubscription;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Service;

@Service
public class PushSubscriptions {

private final Map<String, PushSubscription> byEndpoint = new ConcurrentHashMap<>();

public void save(PushSubscription subscription) {
byEndpoint.put(subscription.getEndpoint(), subscription);
}

public void delete(PushSubscription subscription) {
byEndpoint.remove(subscription.getEndpoint());
}

public Collection<PushSubscription> findAll() {
return byEndpoint.values();
}
}
PushView.java
subscribe.onClick(ev -> Push.getCurrent().subscribe()
.thenAccept(subscription -> {
subscriptions.save(subscription);
status.setText("Abonné");
})
.exceptionally(throwable -> {
WebforjPushException error = (WebforjPushException) throwable.getCause();
status.setText(error.getStatus() == PushStatus.PERMISSION_DENIED
? "Les notifications sont bloquées dans ce navigateur"
: error.getMessage());

return null;
}));

unsubscribe.onClick(ev -> Push.getCurrent().unsubscribe().thenAccept(removed -> {
removed.ifPresent(subscriptions::delete);
status.setText(removed.isPresent() ? "Désaboné" : "Il n'y avait pas d'abonnement");
}));

sendNow.onClick(ev -> sendToAll(subscriptions, sender, message.getValue(), status::setText));

sendLater.onClick(ev -> {
String text = message.getValue();
status.setText("Envoi dans 8 secondes, fermez maintenant l'onglet");
scheduler.schedule(() -> sendToAll(subscriptions, sender, text, outcome -> {
}), Instant.now().plusSeconds(8));
});

Push.getCurrent().getSubscription().thenAccept(existing -> {
existing.ifPresent(subscriptions::save);
status.setText(existing.isPresent() ? "Abonné" : "Pas abonné");
});

self.add(status, message, subscribe, unsubscribe, sendNow, sendLater);