Push Notifications
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 :
- Maven
- Gradle
<dependency>
<groupId>com.webforj</groupId>
<artifactId>webforj-push</artifactId>
</dependency>
dependencies {
implementation 'com.webforj:webforj-push'
}
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 depuislocalhostpendant le développement.
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 :
- Maven
- Gradle
mvn webforj:push-keys
./gradlew webforjPushKeys
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.
webforj.push.public-key=...
webforj.push.private-key=...
webforj.push.subject=mailto:ops@example.com
| Propriété | Explication |
|---|---|
webforj.push.public-key | La moitié publique de la paire de clés utilisée par le déploiement pour signer les notifications |
webforj.push.private-key | La moitié privée de la paire de clés. Comme toute autre secret, gardez-le hors du contrôle de version |
webforj.push.subject | L'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.
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 :
- S'abonner. Depuis une vue,
Push.getCurrent().subscribe()demande la permission de l'utilisateur et renvoie unPushSubscriptionqui identifie l'adresse du navigateur. - Stocker. L'application enregistre l'abonnement avec ses données et l'associe à l'utilisateur correspondant.
- 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.
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 :
| Valeur | Signification |
|---|---|
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.
| Option | Effet |
|---|---|
setBody | Définit le texte affiché sous le titre |
setIcon | Dé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 |
setUrl | Dé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 |
setActions | Définit les boutons affichés sur la notification, avec une URL distincte pour chaque bouton. Voir Support des navigateurs |
setTag | Définit une étiquette identifiant. Si une notification affichée a la même étiquette, la nouvelle notification la remplace |
setSilent | Affiche la notification sans son ni vibration |
setTimeToLive | Définit combien de temps le service push conserve le message pour un appareil hors ligne, jusqu'à quatre semaines |
setUrgency | Utilise PushUrgency pour laisser le dispositif retarder les messages de faible urgence et économiser de la batterie |
setTopic | Remplace 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.
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 :
| Statut | Quand | Que faire |
|---|---|---|
PERMISSION_DENIED | L'utilisateur a bloqué les notifications pour l'application | Expliquez où l'utilisateur peut autoriser les notifications dans les paramètres du navigateur |
UNSUPPORTED | Les 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 servlet | Masquez la fonctionnalité |
NOT_CONFIGURED | Au moins une propriété webforj.push.* est manquante ou incomplète | Générez les clés et configurez toutes les trois propriétés |
SUBSCRIPTION_EXPIRED | Le service push ne reconnaît plus l'abonnement parce que l'utilisateur s'est désabonné ou a réinstallé le navigateur | Supprimez l'abonnement stocké |
REJECTED | Le service push a rejeté le message ; getStatusCode() contient sa réponse | Vérifiez les clés et la taille du message |
UNREACHABLE | Le service push n'a pas répondu avant le délai d'attente | Réessayez plus tard |
UNKNOWN | Le 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;
});
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()renvoieUNSUPPORTED. 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.
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();
}
}