Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allowing user to implement WebPush notification handler #20803

Open
anuj-online opened this issue Jan 5, 2025 · 0 comments
Open

Allowing user to implement WebPush notification handler #20803

anuj-online opened this issue Jan 5, 2025 · 0 comments
Labels
enhancement Help wanted webpush WebPush Notifications API

Comments

@anuj-online
Copy link

anuj-online commented Jan 5, 2025

Describe your motivation

I am building an app, where web push notification are sent. Notifications are user's choice to enable to disable, however, I want to show the user that the notifications are disabled and this would affect apps functionality.

The error handler in the WebPush.java for method public void subscribe(UI ui, WebPushSubscriptionResponse receiver) [highlighting this method as this is where the logs are getting populated] is having a private handler, which can not be accessed, if a user has denied the notification, the server can not react to it.

Also the logs are of no use and are populated on the server, while it appears that the message is for the customer ("You have disabled notifications, you have to enabled them in your settings"). This populates the server logs with errors, while this can not be acted upon. Like for example updating the database with bool notificationGranted.

java.lang.RuntimeException: Unable to execute web push command. JS error is 'Error: You have blocked notifications. You need to manually enable them in your browser.'
	at com.vaadin.flow.server.webpush.WebPush.lambda$new$f744fbe0$1(WebPush.java:63) ~[flow-webpush-24.6.0.jar:na]
	at com.vaadin.flow.component.internal.PendingJavaScriptInvocation.completeExceptionally(PendingJavaScriptInvocation.java:112) ~[flow-server-24.6.0.jar:24.6.0]

The above shown exception is intended for user, but this can not be accessed by the UI.

Describe the solution you'd like

ExceptionHandler consumer is externalized, so the actual view can react to it, and use the state to update DB show notification etc. Expected Class improvement is attached below.

Another solution could be to not have the errorHandler final, and have a public setExceptionHandler method which will do the same thing that is done in the new constructor of the class.

Describe alternatives you've considered

As an alternative I am calling public void isNotificationDenied(UI ui, WebPushState receiver) and showing the error message and updated the DB, while I do not like this approach as this takes multiple cycles between client and server.

Additional context

public class WebPush {
    private PushService pushService;
    private String publicKey;
    private final SerializableConsumer<String> errorHandler;

    public WebPush(String publicKey, String privateKey, String subject) {
        this.publicKey = publicKey;
        this.errorHandler = getDefaultExceptionHandler();
        Security.addProvider(new BouncyCastleProvider());

        try {
            this.pushService = PushService.builder().withVapidPublicKey(publicKey).withVapidPrivateKey(privateKey).withVapidSubject(subject).build();
        } catch (GeneralSecurityException e) {
            throw new WebPushException("Security exception initializing web push PushService", e);
        }
    }

    //Adding a second constructor to externalize exception handling
    public WebPush(String publicKey, String privateKey, String subject, SerializableConsumer<String> errorHandler) {
        this.publicKey = publicKey;
        this.errorHandler = errorHandler;
        Security.addProvider(new BouncyCastleProvider());

        try {
            this.pushService = PushService.builder().withVapidPublicKey(publicKey).withVapidPrivateKey(privateKey).withVapidSubject(subject).build();
        } catch (GeneralSecurityException e) {
            throw new WebPushException("Security exception initializing web push PushService", e);
        }
    }

    private static SerializableConsumer<String> getDefaultExceptionHandler() {
        return (err) -> {
            throw new RuntimeException("Unable to execute web push command. JS error is '" + err + "'");
        };
    }

    public void sendNotification(WebPushSubscription subscription, WebPushMessage message) throws WebPushException {
        int statusCode = -1;
        HttpResponse<String> response = null;

        try {
            Subscription.Keys keys = null;
            if (subscription.keys() != null) {
                keys = new Subscription.Keys(subscription.keys().p256dh(), subscription.keys().auth());
            }

            Subscription nativeSubscription = new Subscription(subscription.endpoint(), keys);
            Notification notification = Notification.builder().subscription(nativeSubscription).payload(message.toJson()).build();
            response = this.pushService.send(notification, PushService.DEFAULT_ENCODING, BodyHandlers.ofString());
            statusCode = response.statusCode();
        } catch (Exception e) {
            this.getLogger().error("Failed to send notification.", e);
            throw new WebPushException("Sending of web push notification failed", e);
        }

        if (statusCode != 201) {
            this.getLogger().error("Failed to send web push notification, received status code:" + statusCode);
            this.getLogger().error(String.join("\n", (CharSequence)response.body()));
            throw new WebPushException("Sending of web push notification failed with status code " + statusCode);
        }
    }

    public void subscriptionExists(UI ui, WebPushState receiver) {
        SerializableConsumer<JsonValue> resultHandler = (json) -> receiver.state(Boolean.parseBoolean(json.toJson()));
        this.executeJavascript(ui, "return window.Vaadin.Flow.webPush.registrationStatus()").then(resultHandler, this.errorHandler);
    }

    public void isNotificationDenied(UI ui, WebPushState receiver) {
        SerializableConsumer<JsonValue> resultHandler = (json) -> receiver.state(Boolean.parseBoolean(json.toJson()));
        this.executeJavascript(ui, "return window.Vaadin.Flow.webPush.notificationDenied()").then(resultHandler, this.errorHandler);
    }

    public void isNotificationGranted(UI ui, WebPushState receiver) {
        SerializableConsumer<JsonValue> resultHandler = (json) -> receiver.state(Boolean.parseBoolean(json.toJson()));
        this.executeJavascript(ui, "return window.Vaadin.Flow.webPush.notificationGranted()").then(resultHandler, this.errorHandler);
    }

    public void subscribe(UI ui, WebPushSubscriptionResponse receiver) {
        SerializableConsumer<JsonValue> resultHandler = (json) -> {
            JsonObject responseJson = Json.parse(json.toJson());
            receiver.subscription(this.generateSubscription(responseJson));
        };
        this.executeJavascript(ui, "return window.Vaadin.Flow.webPush.subscribe($0)", this.publicKey).then(resultHandler, this.errorHandler);
    }

    public void unsubscribe(UI ui, WebPushSubscriptionResponse receiver) {
        this.executeJavascript(ui, "return window.Vaadin.Flow.webPush.unsubscribe()").then(this.handlePossiblyEmptySubscription(receiver), this.errorHandler);
    }

    public void fetchExistingSubscription(UI ui, WebPushSubscriptionResponse receiver) {
        this.executeJavascript(ui, "return window.Vaadin.Flow.webPush.getSubscription()").then(this.handlePossiblyEmptySubscription(receiver), this.errorHandler);
    }

    private PendingJavaScriptResult executeJavascript(UI ui, String script, Serializable... parameters) {
        this.initWebPushClient(ui);
        return ui.getPage().executeJs(script, parameters);
    }

    private void initWebPushClient(UI ui) {
        if (ComponentUtil.getData(ui, "webPushInitialized") == null) {
            ComponentUtil.setData(ui, "webPushInitialized", true);
            Page page = ui.getPage();

            try {
                try (InputStream stream = com.vaadin.flow.server.webpush.WebPush.class.getClassLoader().getResourceAsStream("META-INF/frontend/FlowWebPush.js")) {
                    page.executeJs(StringUtil.removeComments(IOUtils.toString(stream, StandardCharsets.UTF_8)), new Serializable[0]).then((unused) -> this.getLogger().debug("Webpush client code initialized"), (err) -> this.getLogger().error("Webpush client code initialization failed: {}", err));
                }

            } catch (IOException var8) {
                throw new WebPushException("Could not load webpush client code");
            }
        }
    }

    private SerializableConsumer<JsonValue> handlePossiblyEmptySubscription(WebPushSubscriptionResponse receiver) {
        return (json) -> {
            JsonObject responseJson;
            if (json.getType() == JsonType.STRING) {
                responseJson = Json.createObject();
                responseJson.put("message", json.asString());
            } else {
                responseJson = Json.parse(json.toJson());
            }

            if (responseJson.hasKey("message")) {
                receiver.subscription((WebPushSubscription)null);
            } else {
                receiver.subscription(this.generateSubscription(responseJson));
            }

        };
    }

    private WebPushSubscription generateSubscription(JsonObject subscriptionJson) {
        WebPushKeys keys = new WebPushKeys(subscriptionJson.getObject("keys").getString("p256dh"), subscriptionJson.getObject("keys").getString("auth"));
        return new WebPushSubscription(subscriptionJson.getString("endpoint"), keys);
    }

    private Logger getLogger() {
        return LoggerFactory.getLogger(com.vaadin.flow.server.webpush.WebPush.class);
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Help wanted webpush WebPush Notifications API
Development

No branches or pull requests

2 participants