Overslaan naar hoofdinhoud

Push Notifications

Openen in ChatGPT
26.02
Java API

Pushmeldingen kunnen gebruikers bereiken, zelfs als een app niet open is. De browser abonneert zich één keer, de app slaat de abonnement op, en de server gebruikt het om meldingen te verzenden wanneer er een gebeurtenis plaatsvindt. Push beheert het abonneren en afmelden in de browser. Op de server stuurt PushSender een PushMessage naar een opgeslagen abonnement.

Setup en vereisten​

Pushmeldingen worden geleverd door een aparte module. Voeg deze toe aan je app:

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

Pushmeldingen vereisen:

  • Een servlet-implementatie, zoals Jetty, Spring Boot, of een WAR-bestand.
  • Een sleutelpair, hieronder gegenereerd, dat de implementatie gebruikt om meldingen te ondertekenen.
  • Een veilige oorsprong. Browsers weigeren abonnementen die op iets anders dan https worden aangeboden, behalve van localhost tijdens ontwikkeling.
Veilige oorsprongen

Voor meer informatie over veilige contexten en waarom ze belangrijk zijn, zie de Secure Contexts MDN documentatie.

Sleutels genereren​

Pushdiensten accepteren alleen meldingen die zijn ondertekend door de implementatie waarvoor de browser zich heeft geabonneerd. Voer de build plugin één keer uit voor elke implementatie om zijn sleutelpair te genereren:

mvn webforj:push-keys

De opdracht geeft drie configuratielijnen weer. Plak ze in application.properties zonder de aanhalingstekens, of kopieer ze zoals afgedrukt in webforj.conf. Vervang het onderwerp door het contactadres van de implementatie. Dit moet een mailto: of https:// adres zijn dat pushdiensten kunnen gebruiken om contact op te nemen met de operator.

application.properties
webforj.push.public-key=...
webforj.push.private-key=...
webforj.push.subject=mailto:ops@example.com
EigenschapUitleg
webforj.push.public-keyDe publieke helft van de sleutelpair die door de implementatie wordt gebruikt om meldingen te ondertekenen
webforj.push.private-keyDe private helft van de sleutelpair. Net als elke andere geheim, houd deze uit de broncontrole
webforj.push.subjectHet contactadres van de implementatie. Dit moet een mailto: of https:// adres zijn waarmee pushdiensten contact kunnen opnemen met de operator

De app leest deze eigenschappen bij opstarten. Als de configuratie slechts enkele van deze bevat, mislukt de opstart en wordt gerapporteerd welke eigenschappen ontbreken.

Sleutels roteren

Elke browser abonneert zich op één sleutelpair. Als de sleutels veranderen, wijst de pushdienst bestaande abonnementen af. De volgende subscribe() aanroep in elke browser vervangt zijn abonnement.

Hoe het werkt​

Het proces heeft drie stappen:

  1. Abonneren. Vanuit een weergave vraagt Push.getCurrent().subscribe() toestemming van de gebruiker en retourneert een PushSubscription die het adres van de browser identificeert.
  2. Opslaan. De app slaat het abonnement met zijn gegevens op en associeert het met de bijbehorende gebruiker.
  3. Verzenden. Later, vanuit een willekeurige thread, geeft PushSender.send(subscription, message) het bericht door aan de pushdienst van de browserleverancier. De dienst toont de melding, of de app nu open is of niet.
Push.getCurrent().subscribe().thenAccept(subscriptions::save);

sender.send(subscription,
PushMessage.create("Bestelling verzonden").setUrl("/orders/42").build());

De volgende secties leggen uit wat de browser weergeeft en hoe om te gaan met fouten in elke stap.

Instanties​

Haal de push-instantie voor de huidige omgeving op:

import com.webforj.push.Push;

Push push = Push.getCurrent();

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

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

De browser abonneren​

Roep subscribe() aan als reactie op een gebruikersactie, zoals het klikken op een knop "Meldingen inschakelen". De geretourneerde PendingResult voltooit met de PushSubscription van de browser. Als de browser zich niet kan abonneren, voltooit hij uitzonderlijk met een 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;
});

Als de browser al is geabonneerd, retourneert een tweede aanroep van subscribe() het bestaande abonnement. Je kunt het daarom veilig bij elk bezoek aanroepen.

Browser toestemming

De eerste aanroep van subscribe() vraagt de gebruiker om toestemming. De browser toont deze prompt, deze maakt geen deel uit van de app UI. Omdat browsers de prompt alleen tonen als reactie op een gebruikersactie, roep subscribe() aan vanuit een klikluisteraar in plaats van de weergaveconstructor.

Als de gebruiker de prompt blokkeert, kan de app niet opnieuw vragen om die oorsprong.

Abonnementen opslaan​

Een abonnement vertegenwoordigt het adres van één browser en behoort op de server. Sla het op met de gegevens van de app, met zijn eindpunt als de sleutel. Inclusief alle informatie die de app nodig heeft om later de juiste browsers te selecteren, zoals de bijbehorende gebruiker. Elk abonnement bevat drie tekstwaarden:

WaardeBetekenis
getEndpoint()De leverings-URL die door de pushdienst van de browserleverancier is toegewezen
getP256dh()De publieke sleutel van de browser
getAuth()Het authenticatiegeheim van de browser

Een gebruiker die zich vanuit twee browsers abonneert, heeft twee abonnementen. Verwijder een abonnement wanneer de browser zich afmeldt of wanneer een verzending meldt dat het is verlopen. Zie Foutstatus.

Een abonnement herstellen​

getSubscription() retourneert het huidige abonnement van de browser, of een leeg resultaat als er geen bestaat. Gebruik het om een kopie van de server te synchroniseren, bijvoorbeeld nadat de opslag van de app is gereset:

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

Via PushPermission rapporteert getPermission() of de gebruiker toestemming heeft verleend, geweigerd of nog niet heeft geantwoord op de melding prompt. Gebruik dit resultaat om de knop "Meldingen inschakelen" te verbergen wanneer klikken daarop geen effect zou hebben.

Afmelden​

unsubscribe() annuleert het abonnement van de browser. Het voltooit met het verwijderde abonnement, zodat de app zijn opgeslagen kopie kan verwijderen, of met een leeg resultaat als de browser geen abonnement had.

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

Meldingen verzenden​

PushSender verzendt een PushMessage naar een opgeslagen abonnement. Het ondertekent het bericht met de sleutels van de implementatie en geeft het door aan de pushdienst van de browserleverancier. Deze dienst wekt de browser en toont de melding. Omdat de operatie de aanroepende thread nooit blokkeert, kun je deze aanroepen vanuit een klikluisteraar, geplande taak of aanvraaghandler.

Nadat de eigenschappen zijn geconfigureerd, is de verzender beschikbaar als een bean die je kunt injecteren in weergaven, services en geplande taken. Om deze te vervangen, definieer je je eigen PushSender bean.

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

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

Zonder Spring leest new PushSender() de sleutels uit de configuratie van de app. Maak de sender op een app-thread, hetzij in een weergave of in App.run(), en gebruik deze vervolgens vanuit elke thread. Alle zenders delen één verbindingspool met de pushdiensten, dus er zijn geen kosten aan het creëren ervan waar nodig.

Voor meldingen die later of nadat de gebruiker vertrekt, moeten worden verzonden, gebruik een timer op de server zoals Spring's TaskScheduler. Gebruik geen paginatimer zoals Interval, omdat deze stopt wanneer het tabblad wordt gesloten.

Een bericht samenstellen​

Maak een bericht met de titel, configureer vervolgens elke andere optie op de bouwer:

PushMessage message = PushMessage.create("Bestelling verzonden")
.setBody("Bestelling #42 is onderweg")
.setIcon("icons://icon-192x192.png")
.setUrl("/orders/42")
.setActions(List.of(new PushAction("track", "Volgen", "/orders/42/tracking")))
.build();

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

return null;
});

send() retourneert onmiddellijk. De PendingResult voltooit wanneer de pushdienst het bericht accepteert, of voltooit uitzonderlijk als de dienst het niet accepteert. Als send() wordt aangeroepen op een app-thread, zoals vanuit een luisteraar, worden zijn callbacks op die thread uitgevoerd en kunnen componenten bijwerken. Als de sessie die send() oproept eindigt voordat de reactie arriveert, worden de callbacks niet uitgevoerd, maar de melding wordt nog steeds afgeleverd.

Een zending wacht maximaal 30 seconden op de pushdienst voordat deze faalt met UNREACHABLE. Gebruik setTimeout(Duration) om de time-out voor elke zender te wijzigen.

OptieEffect
setBodyStelt de tekst in die onder de titel wordt weergegeven
setIconStelt de afbeelding in die met de melding wordt weergegeven. Het accepteert absolute URL's en de protocollen icons:// en ws://. Zie Assets. Het accepteert de context:// protocol niet omdat pushdiensten een bericht beperken tot 4 KB
setUrlStelt de pagina in die opent wanneer de gebruiker op de melding klikt. Relatieve URL's worden tegen de hoofdmap van de app opgelost. Als er geen URL is ingesteld, opent de hoofdmap van de app
setActionsStelt de knoppen in die op de melding worden weergegeven, met een aparte URL voor elke knop. Zie Browserondersteuning
setTagStelt een identificerende tag in. Als een weergegeven melding dezelfde tag heeft, vervangt de nieuwe melding deze
setSilentToont de melding zonder geluid of trillingen
setTimeToLiveStelt in hoe lang de pushdienst het bericht voor een offline apparaat behoudt, tot vier weken
setUrgencyGebruikt PushUrgency om het apparaat in staat te stellen meldingen van lage urgentie uit te stellen en batterij te besparen
setTopicVervangt een bericht dat nog wacht bij de pushdienst wanneer beide berichten hetzelfde onderwerp hebben. Onderwerpen kunnen maximaal 32 karakters bevatten die veilig zijn in een URL

Wanneer een tabblad de pagina al weergeeft, focust het klikken op de melding de app. Anders opent de pagina in een nieuw tabblad. Klikken op een meldingsknop opent de URL op dezelfde manier.

Eén melding per bericht

Elk bericht toont een melding. Omdat browsers een pagina niet wakker maken voor een bericht dat niets toont, kan push niet worden gebruikt voor stille gegevensupdates.

Foutstatus​

Wanneer subscribe() of send() faalt, rapporteert zijn PendingResult een WebforjPushException. PushStatus identificeert de reden:

StatusWanneerWat te doen
PERMISSION_DENIEDDe gebruiker heeft meldingen voor de app geblokkeerdLeg uit waar de gebruiker meldingen kan toestaan in de browserinstellingen
UNSUPPORTEDPush is niet ondersteund door de browser, de pagina bevindt zich niet in een veilige context, of de app is niet gedeployed als servletVerberg de functie
NOT_CONFIGUREDTen minste één webforj.push.* eigenschap ontbreekt of is onvolledigGenereer de sleutels en configureer alle drie de eigenschappen
SUBSCRIPTION_EXPIREDDe pushdienst herkent het abonnement niet meer omdat de gebruiker zich heeft afgemeld of de browser opnieuw heeft geïnstalleerdVerwijder het opgeslagen abonnement
REJECTEDDe pushdienst heeft het bericht afgewezen; getStatusCode() bevat de reactieVerifieer de sleutels en de berichtgrootte
UNREACHABLEDe pushdienst reageerde niet voor de time-outProbeer het later opnieuw
UNKNOWNHet opgeslagen eindpunt is geen geldige URL, of het abonnement of bericht kon niet worden gecodeerdVerifieer het opgeslagen abonnement

Verwijder verlopen abonnementen tijdens elke verzending:

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

return null;
});
Verloop komt één bericht te laat aan

Pushdiensten deregistreren abonnementen lui. Ze accepteren nog steeds het eerste bericht nadat een gebruiker zich heeft afgemeld, maar het gaat nergens heen. Het volgende bericht meldt SUBSCRIPTION_EXPIRED. Een geaccepteerde verzending betekent dat het bericht de pushdienst heeft bereikt, niet dat de gebruiker het heeft gezien.

Browserondersteuning​

Alle belangrijke desktop- en mobiele browsers tonen pushmeldingen na abonnement. Houd deze beperkingen in gedachten:

  • Op iPhone en iPad werkt push alleen voor webapps die zijn toegevoegd aan het startscherm op iOS 16.4 of later. In een Safari-tabblad meldt subscribe() UNSUPPORTED. Zie Installable Apps voor het vereiste app-manifest.
  • Safari toont geen meldingsknoppen. Het toont berichten met acties zonder hun knoppen, maar klikken op de melding opent nog steeds de URL van het bericht.
  • Android- en iOS-WebViews tonen geen meldingen.

Voor de details per browser, zie de MDN showNotification compatibiliteitstabel.

Volledig voorbeeld​

De volgende weergave abonneert en meldt de browser af, slaat abonnementen in het geheugen op en verzendt een bericht naar elk opgeslagen abonnement. Het kan onmiddellijk verzenden of acht seconden wachten door gebruik te maken van Spring's TaskScheduler, zodat het tabblad kan sluiten voordat de melding arriveert. De app-klasse gebruikt @EnableScheduling om de planner beschikbaar te maken.

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("Geabonneerd");
})
.exceptionally(throwable -> {
WebforjPushException error = (WebforjPushException) throwable.getCause();
status.setText(error.getStatus() == PushStatus.PERMISSION_DENIED
? "Meldingen zijn geblokkeerd in deze browser"
: error.getMessage());

return null;
}));

unsubscribe.onClick(ev -> Push.getCurrent().unsubscribe().thenAccept(removed -> {
removed.ifPresent(subscriptions::delete);
status.setText(removed.isPresent() ? "Afgemeld" : "Er was geen abonnement");
}));

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

sendLater.onClick(ev -> {
String text = message.getValue();
status.setText("Verzenden in 8 seconden, sluit nu het tabblad");
scheduler.schedule(() -> sendToAll(subscriptions, sender, text, outcome -> {
}), Instant.now().plusSeconds(8));
});

Push.getCurrent().getSubscription().thenAccept(existing -> {
existing.ifPresent(subscriptions::save);
status.setText(existing.isPresent() ? "Geabonneerd" : "Niet geabonneerd");
});

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