Web Push Notifications
This page explains how to configure a Vaadin Flow application to utilize Web Push. This can enable Web Push for a completed Customer Relationship Management (CRM) application so that users can receive notifications — even when they’re not accessing the application.
Understanding Web Push
Web push notifications can be used to deliver real-time updates, reminders, and other important messages.
When a user registers for web push notifications, their browser contacts a third-party web push server hosted by the browser vendor.
Set Up Web Push
To set up a web push, you’ll have to do a few things: generate proper keys; configure for needed dependencies; generate a service worker (i.e., PWA resources); and create the WebPushService. You should also add the ability for others to register for the push service, and configure for sending notifications to those subscribers. These steps are covered in the sub-sections here.
VAPID Keys
The first step is to generate a public-private VAPID key pair. This can be done by executing the following from the command-line:
npx web-push generate-vapid-keys
That should return an output that looks something like this:
=======================================
Public Key:
BO66S__WPMwVXKBXEh1OiQrM--9pSGXwxqAWQudqmcv41RcWgk1ssUeItv4_ArqkJwPBtayUR4
Private Key:
VNlfcVVFB4V50tqKO8WFHHOhx_ZrabUkZ2BYVOnNg9A
=======================================
After you generate a pair of keys, they can be added to the application.properties
file or set as environment variables.
For the examples here, the property names used are public.key
, private.key
and subject
.
Note
|
The subject should be given as a url or mailto: , since Apple web push server requires this.
|
Web Push Dependencies
For web push usage, the project needs to have the Flow WebPush
dependency. Add the following to your pom.xml
file:
<dependencies>
<!-- Other application dependencies -->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>flow-webpush</artifactId>
</dependency>
</dependencies>
Generate Service Worker
Using Web Push methods, Vaadin provides the @PWA
annotations that automatically generate the required PWA resources. Add the @PWA
annotation on Application.java
as follows:
@SpringBootApplication
@PWA(name = "Vaadin CRM", shortName = "CRM")
public class Application extends SpringBootServletInitializer implements AppShellConfigurator {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@PWA
enables ServiceWorker creation and web push functionality.
Create WebPushService
Next, you’ll have to create WebPushService for initializing WebPush and storing the user subscriptions.
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import com.vaadin.flow.server.webpush.WebPush;
import com.vaadin.flow.server.webpush.WebPushMessage;
import com.vaadin.flow.server.webpush.WebPushSubscription;
@Service
public class WebPushService {
@Value("${public.key}")
private String publicKey;
@Value("${private.key}")
private String privateKey;
@Value("${subject}")
private String subject;
private final Map<String, WebPushSubscription> endpointToSubscription = new HashMap<>();
WebPush webPush;
/**
* Initialize security and push service for initial get request.
*
* @throws GeneralSecurityException security exception for security complications
*/
public WebPush getWebPush() {
if(webPush == null) {
webPush = new WebPush(publicKey, privateKey, subject);
}
return webPush;
}
/**
* Send a notification to all subscriptions.
*
* @param title message title
* @param body message body
*/
public void notifyAll(String title, String body) {
endpointToSubscription.values().forEach(subscription -> {
webPush.sendNotification(subscription, new WebPushMessage(title, body));
});
}
private Logger getLogger() {
return LoggerFactory.getLogger(WebPushService.class);
}
public void store(WebPushSubscription subscription) {
getLogger().info("Subscribed to {}", subscription.endpoint());
/*
* Note, in a real world app you'll want to persist these
* in the backend. Also, you probably want to know which
* subscription belongs to which user to send custom messages
* for different users. In this demo, we'll just use
* endpoint URL as key to store subscriptions in memory.
*/
endpointToSubscription.put(subscription.endpoint(), subscription);
}
public void remove(WebPushSubscription subscription) {
getLogger().info("Unsubscribed {}", subscription.endpoint());
endpointToSubscription.remove(subscription.endpoint());
}
public boolean isEmpty() {
return endpointToSubscription.isEmpty();
}
}
Adding Push Registration
The last step is to add the ability to register for the push service.
Flow contains the WebPushRegistration
class that can be used to handle registering and deregistering of web push on the client. The WebPushRegistration needs the VAPID public key on construction.
The UI components for this can be two buttons: one for registering; and one for deregistering notifications.
WebPush webpush = webPushService.getWebPush();
Button subscribe = new Button("Subscribe");
Button unsubscribe = new Button("UnSubscribe");
subscribe.setEnabled(false);
subscribe.addClickListener(e -> {
webpush.subscribe(subscribe.getUI().get(), subscription -> {
webPushService.store(subscription);
subscribe.setEnabled(false);
unsubscribe.setEnabled(true);
});
});
unsubscribe.setEnabled(false);
unsubscribe.addClickListener(e -> {
webpush.unsubscribe(unsubscribe.getUI().get(), subscription -> {
webPushService.remove(subscription);
subscribe.setEnabled(true);
unsubscribe.setEnabled(false);
});
});
In cases where there exists a subscription on the client for the application, but it’s been lost on the server, it can be obtained from the service worker.
@Override
protected void onAttach(AttachEvent attachEvent) {
UI ui = attachEvent.getUI();
pushApi.subscriptionExists(ui, registered -> {
subscribe.setEnabled(!registered);
unsubscribe.setEnabled(registered);
if(registered && webPushService.isEmpty()) {
pushApi.fetchExistingSubscription(ui, webPushService::store);
}
});
}
Sending Notifications
The WebPushService
had the methods sendNotification(subscription, messageJson)
and notifyAll(title, body)
.
Sending a message to all registered subscribers using the notifyAll()
method would look like this:
TextField message = new TextField("Message");
Button broadcast = new Button("Broadcast message");
broadcast.addClickListener(e ->
webPushService.notifyAll("Message from administration", message.getValue())
);
For using sendNotification
, the correct user subscription is needed. You can find source code for the examples on GitHub.
You can also find source code for a CRM example with database usage on crm-tutorial.
Caution
|
Brave Browser Support
For the Brave browser, web push notifications may work by default, when the browser is first installed. If not, notifications need to be enabled in the browser. Inform the user to open their browser privacy settings (i.e., |
Caution
|
iOS & iPadOS Support
Mobile Web Push for iOS and iPadOS requires the following:
For iOS and iPadOS, the registration needs to happen in the installed web application. The Safari web browser needs the web push notification features enabled. To do this, go to |
Note
|
Mobile Notifications
Mobile devices require the site to be served through https with a TLS/SSL certificate or they won’t accept the service worker.
|
Apply Custom Settings To Notifications
A notification can display additional information, such as an icon, custom actions in a drop-down, or an arbitrary data object. The available options are documented on mdn
web docs. Notification options can be provided to a WebPushMessage
as a Java class object or record, or as a Jackson’s ObjectNode
representing the options in JSON format.
The following WebPushAction
and WebPushOptions
Java records hold custom options, such as actions, an icon, and a data object. Note that these records are not part of the Vaadin Web Push API and must be defined within the project, depending on the required options.
public record WebPushAction(String action, String title) implements Serializable {
}
public record WebPushOptions(String body,
List<WebPushAction> actions,
Serializable data,
String icon) implements Serializable {
}
They can be used as follows to send a notification with options. The example here is a web push notification with an icon, a data object, and a custom action.
WebPushAction webPushAction = new WebPushAction("dashboard", "Open Dashboard");
WebPushOptions webPushOptions = new WebPushOptions(
body,
List.of(webPushAction),
"This is my data",
"https://example.com/my-icon.png"
);
webPush.sendNotification(subscription, new WebPushMessage(title, webPushOptions));
The custom "dashboard" action opens the Dashboard view with the /dashboard
URL mapping. This requires extending the Service Worker with a custom listener for the notificationclick
event in sw.ts
. For more details on customizing sw.ts
, see Overriding the Generated Service Worker.
The example below shows a customised Service Worker with a custom action listener:
self.addEventListener('notificationclick', (e) => {
e.notification.close();
const dashboardUrl = '/dashboard';
e.waitUntil(
(async () => {
if (e.action === 'dashboard') {
// Get an arbitrary data object attached to a notification
const data = e.notification.data || "no-data";
console.log('Notification data: ' + data);
// Use 'self as ServiceWorkerGlobalScope' to access clients
const clientList = await (self as ServiceWorkerGlobalScope).clients.matchAll({ type: 'window', includeUncontrolled: true });
for (const client of clientList) {
if (client.url.includes(dashboardUrl) && 'focus' in client) {
return client.focus();
}
}
return (self as ServiceWorkerGlobalScope).clients.openWindow(dashboardUrl);
} else {
return focusOrOpenWindow(); // Your default handling function
}
})()
);
});
The listener above opens the Dashboard view or focuses on the already open browser when a user clicks the 'Open Dashboard' action in a notification’s drop-down. If a user clicks on the notification area instead, it opens a default page with an empty URL mapping. Additionally, it demonstrates how to access custom data in a notification.
AA0C567E-EEC6-4CEB-95FA-D9D96666D98F