Passer au contenu principal

Push Notifications

Ouvrir dans ChatGPT
26.02
Java API

Les notifications push peuvent atteindre les utilisateurs même lorsque l'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 le désabonnement 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, tel que 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 fournis 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 imprime 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 toute autre secret, gardez-le 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'inclut que certaines d'entre elles, le démarrage échoue et rapporte 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 se déroule en trois étapes :

  1. S'abonner. 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) passe le message au service push du fournisseur de 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 -> {
// ...
});

S'abonner le navigateur

Appelez subscribe() en réponse à une action 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, cela se termine exceptionnellement avec un 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 subscribe() de nouveau retourne l'abonnement existant. Vous pouvez donc l'appeler en toute sécurité à chaque visite.

Permission du navigateur

Le premier appel à subscribe() invite l'utilisateur à donner son autorisation. Le navigateur affiche cette invite, elle ne fait pas partie de l'UI de l'app. Étant donné que les navigateurs n'affichent l'invite qu'en réponse à une action de l'utilisateur, appelez subscribe() à partir d'un écouteur de clic au lieu du constructeur de la vue.

Si l'utilisateur bloque l'invite, l'application ne peut pas inviter à nouveau pour cette origine.

Stocker les 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 point de terminaison comme clé. Incluez toute information dont l'application a besoin pour sélectionner les navigateurs appropriés plus tard, comme l'utilisateur associé. Chaque abonnement contient trois valeurs textuelles :

ValeurSignification
getEndpoint()L'URL de livraison assignée par le service push du fournisseur de 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 lorsqu'un envoi indique qu'il a expiré. Voir Statut d'échec.

Restaurer un abonnement

getSubscription() renvoie l'abonnement actuel du navigateur, ou un résultat vide s'il n'en 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() indique si l'utilisateur a accordé, refusé ou n'a pas encore répondu à l'invite de notification. Utilisez ce résultat pour masquer le bouton "Activer les notifications" lorsque cliquer dessus n'aurait aucun effet.

Désabonnement

unsubscribe() annule l'abonnement du navigateur. Cela se termine avec l'abonnement supprimé afin que l'application puisse supprimer sa copie stocké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 des notifications

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

Une fois les propriétés configurées, l'expéditeur est disponible en tant que bean que vous pouvez injecter dans des vues, des services et des travaux planifié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 à partir de la configuration de l'application. Créez l'expéditeur sur un thread de l'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 unique aux services push, donc il n'y a aucun coût à en créer un où que ce soit.

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 le TaskScheduler de Spring. Ne pas utiliser un minuteur de page tel que Interval, car il s'arrête lorsque l'onglet se ferme.

Composer 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("suivre", "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 exceptionnellement si le service ne l'accepte pas. Si send() est appelé sur un thread d'application, comme 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 l'arrivée de la réponse, les rappels ne s'exécutent pas, mais la notification est toujours livrée.

Un envoi attend jusqu'à 30 secondes pour le service push 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. Il accepte les URL absolues et les protocoles icons:// et ws://. Voir Assets. Il 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 URL 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 distincte pour chaque bouton. Voir Support des navigateurs
setTagDéfinit une étiquette identifiant. Si une notification affichée a la même étiquette, 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 laisser le dispositif retarder les messages de faible urgence et économiser de la batterie
setTopicRemplace un message qui est toujours en attente 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 met l'application au premier plan. 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. Étant donné que les navigateurs ne réveillent pas une page pour un message qui n'affiche rien, les push 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 pushes ne sont pas supportés 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 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 point de terminaison stocké n'est pas une URL valide, ou l'abonnement ou le message n'a pas pu être encodéVé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 un message en retard

Les services push désenregistrent les abonnements lentement. Ils acceptent toujours le premier message après qu'un utilisateur se désabonne, mais il ne va nulle part. Le message suivant signale 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 notifications push fonctionnent uniquement pour les applications web ajoutées à l'écran d'accueil sur iOS 16.4 ou ultérieur. Dans un onglet Safari, subscribe() renvoie UNSUPPORTED. Voir Applications installables pour le manifeste d'application requis.
  • Safari n'affiche pas de boutons de notification. Il affiche des messages avec des actions sans leurs boutons, mais cliquer sur la notification ouvre toujours l'URL du message.
  • Les WebViews Android et iOS n'affichent pas de notifications.

Pour les détails par navigateur, voir le tableau de compatibilité showNotification sur 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 de l'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ésabonné" : "Aucun 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 l'onglet maintenant");
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é" : "Non abonné");
});

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