Service Worker not receiving push message - javascript

I'm trying to implement push messages. I followed a tutorial to register a service worker and a push service subscription :
//requesting notification permisison
Notification.requestPermission(status => {
console.log('Notification permission status:', status);
if (status == 'granted') {
if ('serviceWorker' in navigator && 'PushManager' in window) {
console.log('Service Worker and Push is supported');
navigator.serviceWorker.register('/sw.js')
.then(function (registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(function (error) {
console.error('Service Worker Error', error);
});
} else {
console.error('Push messaging is not supported');
}
} else {
console.warn("Notifications denied");
}
});
navigator.serviceWorker.ready.then(function (registration) {
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
'<mypublickey>'
)
})
.then(function (subscription) {
//successfull
console.log('push service subscription successfull: ' + JSON.stringify(subscription));
})
.catch(function (e) {
console.error(e);
})
})
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
In my browser console, I get the following subscription
push service subscription successfull:{
"endpoint":"https://fcm.googleapis.com/fcm/send/id",
"expirationTime":null,
"keys":{
"p256dh":"myp256dh",
"auth":"myauth"
}
}
Then to send my push, I'm using the web-push php library. Here's my code to send a push :
<?php
use Minishlink\WebPush\MessageSentReport;
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;
class Test
{
public function execute()
{
$auth = [
'VAPID' => [
'subject' => 'mailto:me#website.com',
'publicKey' => 'mypubkey',
'privateKey' => 'myprivatekey'
]
];
$notifications = [
[
'subscription' => Subscription::Create([
"endpoint" => 'myendpoint',
"keys" => [
'p256dh' => 'myp256dh',
'auth' => 'myauth'
],
]),
'payload' => '{"msg":"hello world!"}'
]
];
$webPush = new WebPush($auth);
$webPush->sendNotification(
$notifications[0]['subscription'],
$notifications[0]['payload']
);
/** #var MessageSentReport $report */
foreach ($webPush->flush() as $report) {
$endpoint = $report->getEndpoint();
if ($report->isSuccess()) {
echo "[v] Message sent successfully for subscription {$endpoint}.";
} else {
echo $report->getReason();
}
}
}
}
And this is what I'm getting from this code
[v] Message sent successfully for subscription myendpoint.
And this does not trigger any notification in the browser. I'm using chrome, and the 'send test push message' in the dev tools works and displays a notification. There aren't any log in the browser console when I execute my php code to send the notification. I have also tried in Firefox and have reached the same results.
Any idea what could be wrong ?
Here's my service worker
sw.js
self.addEventListener('push', event => {
console.log('received a push!');
event.waitUntil(
self.registration.showNotification('This is a push');
);
});

Related

Lighthouse PWA Audit fail "does not register a service worker that controls page and start_url"

I am getting the above failure in my lighthouse audit. Having looked through SO and here it "should" be working.
There are no errors in the console.
This is the Manifest: -
{
"short_name": "Brotkrumen",
"name": "Brotkrumen Web App",
"description": "Native Background Geolocation POC",
"icons": [
{
"src": "gingerbreadhouse.png",
"sizes": "48x48 128x128 144x144 192x192 512x512",
"type": "image/png"
}
],
"start_url": "/TravelManager.html",
"background_color": "#00ccdd",
"theme_color": "#00ccdd",
"display": "fullscreen"
}
This is the ServiceWorker code: -
'use strict';
/* identification division.
* program-id. echo.
* author. Richard Maher.
*/
var doryRegister = []; // Meet someone new every day.
const INTRO = "GrussGott"; // Tell clients we're new.
const FLEET_MANAGER = "/Fleet/Move"; // Starship control.
const CACHE_NAME = "BrotkrumenV1.0"
self.addEventListener('install',
function (e) {
e.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
return cache.addAll([
'/TravelManager.html',
'/hg.png',
'/gingerbreadhouse.png',
'/striped-witch-hat.png',
'/brotkrumen.css',
'/echo.js',
'/registerserviceworker.js',
'/brotkrumen.json',
'/TravelManagerPolyfill.js',
'/HandleMap.js'
]).then(() => self.skipWaiting());
})
);
});
self.addEventListener('activate', function(e)
{
e.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(keyList.map((key) => {
if (key !== CACHE_NAME) {
console.log('Removing cache', key);
return caches.delete(key);
}
}));
})
);
e.waitUntil(self.clients.claim());
});
self.addEventListener('fetch', function (e) {
console.log(e.request.url);
if (e.request.url.startsWith(self.location.origin)) {
e.respondWith(
caches.match(e.request).then(function (response) {
console.log("Request " + e.request.url);
if (response) {
console.log("Response " + response.url);
if (e.request.cache === 'only-if-cached') {
e.request.mode = 'same-origin'
}
} else
console.log("No MATCH");
return response || fetch(e.request);
})
);
}
return fetch(e.request);
});
The register service worker code: -
'use strict';
/* identification division.
* program-id. RegisterServiceWorker.
* author. Richard Maher.
* version. 1.0
*/
function registerServiceWorker()
{
// Make sure SW is there
navigator.serviceWorker.register('echo.js', {scope: './'})
.then(reg => {
window.addEventListener("unload",unsubscribeTravelManager);
console.log('SW Registered');
})
.catch(err => {
console.log('SW Registration failed with ' + err);
reportError({header:"Could not register ServiceWorker",
message:err});
return;
});
// Register for Background Geolocation tracking. Take default for accuracy, max age.
navigator.serviceWorker.ready
.then(reg => {
reg.travelManager.subscribe({
minSilence: 5, // Car trip value. Collapse GPS.
maxSilence: 600, // Sanity check. Squelch off.
minProgress: 50, // Indoor accuracy an issue.
maxSnail: 55, // Brisk walk.
dropDodgy: true // Avoid wireless Pong.
})
.then(subscription => {travelSubscription = subscription}, locFail)
.catch(err => {
console.log('Travel Manager Subscription failed with ' + err);
reportError({header:"Could not subscribe to Travel Manager",
message:err});
});
});
}
function unsubscribeTravelManager(e)
{
// Tidy up
const CANCEL = true;
var shutdown = Promise.resolve(false);
if (travelSubscription)
shutdown = travelSubscription.unsubscribe(CANCEL);
shutdown
.then(success => {
console.log("Unsubscribed from TravelManager = " + success);
navigator.serviceWorker.ready
.then(reg => {
reg.unregister()
.then(success => {console.log('SW Unregistered = ' + success)})
})
})
.catch(err => console.log("Couldn't unregister SW " + err))
}
replace
"start_url": "/TravelManager.html"
with
"start_url": "."
Pilot error :-(
In the page unload event when I unsubscribed from the TravelManager I also unregistered the ServiceWorker which sort of defeated the purpose. My bad.
I think the problem is with your service worker. I haven't found any code where you've registered your service worker at a specific URL.
const serviceWorkerUrl = '/sw.js';
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register(serviceWorkerUrl).then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
You should register your service worker before you add event listener for install, activate & fetch. Please follow this documentation if you want to learn more about it.

PHP successful webpush not triggering push event listener in SW

I would like to implement push notifications to a website, but I can't trigger push event
with PHP and web push library, even though it seems that message is sent successfully.
I've tried to trigger the event from the console(chrome), and It works.
Service worker:
self.addEventListener('install', async event => {
// works fine
console.log('install event')
});
self.addEventListener("push", function(event) {
// console logs work only when I press push button in the console
console.log( 'push event' );
console.log( event );
var data = event.data.json();
event.waitUntil(self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
tag: data.tag
}));
});
Php endpoint(push.php):
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;
$auth = [
'VAPID' => [
'subject' => 'mailto:me#website.com', // can be a mailto: or your website address
'publicKey' => 'BL9qxdhqL_CM1ROLo6AUfeBvEyUuD7EHT3lAz8ksBZSYPsdE6q__uU2FoX9lr5FtmWtlHs-HRMHen3Ki8WWSVA4', // (recommended) uncompressed public key P-256 encoded in Base64-URL
'privateKey' => '7ldG3QYcY9KStB07ytTnd0CRCVSxbHfHYLyWEmgBKo0', // (recommended) in fact the secret multiplier of the private key encoded in Base64-URL
],
];
$subscription = Subscription::create([
// I'm using post just for test, in production I will fetch endpoints from database
'endpoint' => $_POST['endpoint'], // I get the value from subscription object
"keys" => [
'p256dh' => 'BL9qxdhqL_CM1ROLo6AUfeBvEyUuD7EHT3lAz8ksBZSYPsdE6q__uU2FoX9lr5FtmWtlHs-HRMHen3Ki8WWSVA4',
'auth' => '7ldG3QYcY9KStB07ytTnd0CRCVSxbHfHYLyWEmgBKo0'
],
'contentEncoding' => 'aesgcm',
]);
$webPush = new WebPush($auth);
$sent = $webPush->sendNotification($subscription, 'Hi');
foreach ($webPush->flush() as $report) {
$endpoint = $report->getRequest()->getUri()->__toString();
if ($report->isSuccess()) {
// I get this in the response
echo "[v] Message sent successfully for subscription {$endpoint}.";
} else {
echo "[x] Message failed to sent for subscription {$endpoint}: {$report->getReason()}";
}
}
My javascript:
async function registerSw () {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').then(function(registration) {
subscribeToPush();
}).catch(error => console.log(error));
} else {
alert('Service workers not supported');
}
}
async function subscribeToPush () {
navigator.serviceWorker.ready.then(function(registration) {
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey:
urlB64ToUint8Array('BL9qxdhqL_CM1ROLo6AUfeBvEyUuD7EHT3lAz8ksBZSYPsdE6q__uU2FoX9lr5FtmWtlHs-HRMHen3Ki8WWSVA4')
})
.then(function(subscription) {
// The subscription was successful
ajax(subscription);
})
.catch(function(e) {
console.log( e );
});
});
}
function urlB64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
function ajax (subscription) {
console.log( 'in ajax' );
console.log( subscription );
$.ajax({
url: 'push.php',
type: 'POST',
data: {
'endpoint': subscription.endpoint,
},
success: function( response ) {
// response = JSON.parse( response );
console.log(typeof response);
console.log(response);
// I get message was sent successfuly here
},
error: function (xhr, status, error) {
console.log( xhr.responseText );
}
});
}
Service worker is registered successfully, I can ask for user permission and send values from subscription object back to backend, everything goes fine with web push, but event is still not being triggered.
Any help would be appreciated, thanks.
One of possible problems is that you need to allow PUT and DELETE type of requests on your server.
Problem was that "keys" value of associative array that I've passed to Subscription::create had wrong hardcoded values.

Firebase function to fetch data from Firebase DB to make Push notification

I have chat app with firebase database and Firebase cloud messaging. I can send firebase notification via console but in real scenario it should be automatic. To make automatic notification,My friend wrote Index.js (Added in cloud functions) file for me but its not sending notifications.
As per our logic function should trigger whenever there is any new entries (in any node or in any room) and fetch these values by firebase function and make post request to FCM server to make notification to receiver device (get value of receiver device from token_To).
Message
Message_From
Time
Type
token_To
Index.js
var functions = require('firebase-functions');
var admin = require('firebase-admin');
var serviceAccount = require('./demofcm-78aad-firebase-adminsdk-4v1ot-2764e7b580.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://demofcm-78aad.firebaseio.com/"
})
// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
// exports.helloWorld = functions.https.onRequest((request, response) => {
// response.send("Hello from Firebase!");
// });
exports.setUserNode = functions.auth.user().onCreate(event => {
// ...
});
exports.notifyMsg = functions.database.ref('/{chatroom}/{mid}/')
.onWrite(event => {
if (!event.data.val()) {
return console.log('Message Deleted');
}
const getDeviceTokensPromise = admin.database().ref('/{chatroom}/{mid}/token_to').once('value');
return Promise.all([getDeviceTokensPromise]).then(results => {
const tokensSnapshot = results[0];
if (!tokensSnapshot.hasChildren()) {
return console.log('There are no notification tokens to send to.');
}
const payload = {
notification: {
title: 'You have a new Message!',
body: event.data.val().Message
}
};
const tokens = Object.keys(tokensSnapshot.val());
return admin.messaging().sendToDevice(tokens, payload).then(response => {
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('Failure sending notification to', tokens[index], error);
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove());
}
}
});
return Promise.all(tokensToRemove);
});
});
});
Firebase function Log
How can i fetch above mentioned values of any newly added node in same room(9810012321-9810012347) or any other room(9810012321-9810012325) from database and send it to FCM to make notification
Thanks in Advance.
What i did is created a Message node and I believe doing this by users key. ie, having the receiver(toId) and sender (fromId) key to send the notification.
Hope it helps.
exports.sendMessageNotification = functions.database.ref('/messages/{pushId}')
.onWrite(event => {
let message = event.data.current.val();
console.log('Fetched message', event.data.current.val());
let senderUid = message.fromId;
let receiverUid = message.toId;
let promises = [];
console.log('message fromId', receiverUid);
console.log('catch me', admin.database().ref(`/users/${receiverUid}`).once('value'));
if (senderUid == receiverUid) {
//if sender is receiver, don't send notification
//promises.push(event.data.current.ref.remove());
return Promise.all(promises);
}
let messageStats = message.messageStatus;
console.log('message Status', messageStats);
if (messageStats == "read") {
return Promise.all(promises);
}
let getInstanceIdPromise = admin.database().ref(`/users/${receiverUid}/pushToken`).once('value');
let getSenderUidPromise = admin.auth().getUser(senderUid);
return Promise.all([getInstanceIdPromise, getSenderUidPromise]).then(results => {
let instanceId = results[0].val();
let sender = results[1];
console.log('notifying ' + receiverUid + ' about ' + message.text + ' from ' + senderUid);
console.log('Sender ', sender);
var badgeCount = 1;
let payload = {
notification: {
uid: sender.uid,
title: 'New message from' + ' ' + sender.displayName,
body: message.text,
sound: 'default',
badge: badgeCount.toString()
},
'data': {
'notificationType': "messaging",
'uid': sender.uid
}
};
badgeCount++;
admin.messaging().sendToDevice(instanceId, payload)
.then(function (response) {
console.log("Successfully sent message:", response);
})
.catch(function (error) {
console.log("Error sending message:", error);
});
});
});
const getDeviceTokensPromise = event.data.child('token_To');
should be there instated of getting data from database reference.
or
with fixed path without wildcard like below
const getDeviceTokensPromise = admin.database().ref('/${chatroom}/${mid}/token_to').once('value');
where chatroom and mid is variable which contain value
Second thing:
if (!tokensSnapshot.exists()) {
should in replace of
if (!tokensSnapshot.hasChildren()) {
third thing:
I am not sure about push notification tokenId but
is it required to do?
const tokens = Object.keys(tokensSnapshot.val());
may be we can use directly like below to send push notification
const tokens = tokensSnapshot.val();
You could store all device tokens in a node called tokens like in my example. Tokens could be an array if you would like one user to be able to get notifications on multiple devices. Anyway, store them by their UID.
This works for both Andriod and iOS.
Here is my code:
function loadUsers() {
let dbRef = admin.database().ref('/tokens/' + recieveId);
console.log(recieveId)
let defer = new Promise((resolve, reject) => {
dbRef.once('value', (snap) => {
let data = snap.val();
console.log("token: " + data.token)
//userToken = data.token
resolve(data.token);
}, (err) => {
reject(err);
});
});
return defer;
}
Next we create the notification. I created a lastMessage node to capture just the last message sent in the chat. It is just updated every time a new message is sent in a chat between two users. Makes it easy to get the value. Also makes it easy to show the message on the Conversations screen where there is a list of users who are in a conversation with the current user.
exports.newMessagePush =
functions.database.ref('/lastMessages/{rcId}/{sendId}').onWrite(event => {
if (!event.data.exists()) {
console.log("deleted message")
return;
}
recieveId = event.params.rcId
//let path = event.data.adminRef.toString();
// let recieveId = path.slice(53, 81);
return loadUsers().then(user => {
console.log("Event " + event.data.child("text").val());
let payload = {
notification: {
title: event.data.child("name").val(),
body: event.data.child("text").val(),
sound: 'default',
priority: "10",
}
};
return admin.messaging().sendToDevice(user , payload);
});
});
To implement this logic on your current data structure, just change this line:
let dbRef = admin.database().ref('/tokens/' + recieveId);
and this line:
exports.newMessagePush =
functions.database.ref('/lastMessages/{rcId}/{sendId}').onWrite(event
=> {
to your token location:
let dbRef =
admin.database().ref('/${chatroom}/${mid}/token_to');
and your conversation location:
exports.notifyMsg = functions.database.ref('/{chatroom}/{mid}/')
.onWrite(event => {
Then just change the notification payload be the message you want to display and throw in your error handling on the end of the sendToDevice function, as you did in your code.
Hopefully you figured all this out already but if not maybe this will help you or others trying to use Cloud Functions for notifications.
let payload = {
notification: {
uid: sender.uid,
title: 'New message from' + ' ' + sender.displayName,
body: message.text,
sound: 'default',
badge: badgeCount.toString()
},
'data': {
'notificationType': "messaging",
'uid': sender.uid
}
};
There are two types of FCMs.
1) Data
2) Notification
For detailed overview : FCM Reference
You have to fix your payload for both FCMS. And for Data FCM you have to extract Data in your FCM Service (Client) and generate a push notification according to your need.

How to customize browser webpush notification body & icon?

How can we make the browser notification icon and body dynamically customized based upon the #challenge.image and the #challenge.description?
I got the notification to pop up by clicking webpush-button, but only the title of the message is dynamically changing. How can I make it work for body and icon too?
challenges/show.html.erb
<script>
$('.webpush-button').on('click', (e) => {
navigator.serviceWorker.ready
.then((serviceWorkerRegistration) => {
serviceWorkerRegistration.pushManager.getSubscription()
.then((subscription) => {
$.post('/push', {
subscription: subscription.toJSON(),
message: "<%= #challenge.name %>", # This works dynamically for the title, but how can I get message to also dynamically send body and icon so I can use <%= #challenge.image %> for icon and <%= #challenge.description %> for body?
});
});
});
});
</script>
push_notifications_controller
class PushNotificationsController < ApplicationController
def push
Webpush.payload_send(
message: params[:message],
endpoint: params[:subscription][:endpoint],
p256dh: params[:subscription][:keys][:p256dh],
auth: params[:subscription][:keys][:auth],
vapid: {
subject: "mailto:sender#example.com",
public_key: ENV['VAPID_PUBLIC_KEY'],
private_key: ENV['VAPID_PRIVATE_KEY']
}
)
end
end
application.js
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/serviceworker.js')
.then(function(reg) {
console.log('Service worker change, registered the service worker');
});
}
// Otherwise, no push notifications :(
else {
console.error('Service worker is not supported in this browser');
}
// When serviceWorker is supported, installed, and activated,
// subscribe the pushManager property with the vapidPublicKey
navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
serviceWorkerRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: window.vapidPublicKey
});
});
// Let's check if the browser supports notifications
if (!("Notification" in window)) {
console.error("This browser does not support desktop notification");
}
// Let's check whether notification permissions have already been granted
else if (Notification.permission === "granted") {
console.log("Permission to receive notifications has been granted");
}
// Otherwise, we need to ask the user for permission
else if (Notification.permission !== 'denied') {
Notification.requestPermission(function (permission) {
// If the user accepts, let's create a notification
if (permission === "granted") {
console.log("Permission to receive notifications has been granted");
}
});
}
serviceworker.js.erb
console.log('[Service Worker] Hello world!');
var CACHE_VERSION = 'v1';
var CACHE_NAME = CACHE_VERSION + ':sw-cache-';
function onInstall(event) {
console.log('[Serviceworker]', "Installing!", event);
event.waitUntil(
caches.open(CACHE_NAME).then(function prefill(cache) {
return cache.addAll([
// make sure serviceworker.js is not required by application.js
// if you want to reference application.js from here
'<%#= asset_path "application.js" %>',
'<%= asset_path "application.css" %>',
'/offline.html',
]);
})
);
}
function onActivate(event) {
console.log('[Serviceworker]', "Activating!", event);
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
// Return true if you want to remove this cache,
// but remember that caches are shared across
// the whole origin
return cacheName.indexOf(CACHE_VERSION) !== 0;
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
}
// Borrowed from https://github.com/TalAter/UpUp
function onFetch(event) {
event.respondWith(
// try to return untouched request from network first
fetch(event.request).catch(function() {
// if it fails, try to return request from the cache
return caches.match(event.request).then(function(response) {
if (response) {
return response;
}
// if not found in cache, return default offline content for navigate requests
if (event.request.mode === 'navigate' ||
(event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
console.log('[Serviceworker]', "Fetching offline content", event);
return caches.match('/offline.html');
}
})
})
);
}
self.addEventListener("push", (event) => {
let title = (event.data && event.data.text()) || "Yay a message";
let body = "We have received a push message";
let icon = '/assets/default.png';
event.waitUntil(
self.registration.showNotification(title, { title,body, icon })
)
});
self.addEventListener('install', onInstall);
self.addEventListener('activate', onActivate);
self.addEventListener('fetch', onFetch);
serviceworker-companion.js
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/serviceworker.js', { scope: './' })
.then(function(reg) {
console.log('[Companion]', 'Service worker registered!');
});
}
I implemented push via serviceworker gem, webpush gem, VAPID tutorial.
The event.data object in the self.addEventListener("push", (event)=> {...}), has a method json(), that method convert to json the event.data value.
This will works only if the sended data is in the proper json format.
Here is an example how I'm using web push:
self.addEventListener("push", function onPush(event) {
var data = event.data.json()
event.waitUntil(self.registration.showNotification(data.message.title, {
body: data.message.body,
icon: data.message.icon,
actions: [
{ action: 'Button one', title: "Button one text" },
{ action: 'Button two', title: "Button two text" }
]
}));
});
Here is a method from my user.rb model:
def send_web_push(title: , body: )
# the web_push is a payload with I save to the user record,
# that's allow me to send web push with background worker.
payload = {
endpoint: web_push['endpoint'],
keys: {
auth: web_push['auth'],
p256dh: web_push['p256dh']
}
}
push = WebPush.new(payload)
push.set_vapid_details(
'mailto:support#awesomesupewebapp.com',
Rails.application.secrets.web_push_public_key,
Rails.application.secrets.web_push_private_key
)
# below is a `:message` key in json format
push.send_notification({
message: {
title: title,
body: body,
icon: "/this_is_my_icon.png"
}
}.to_json)
end
I use some other ruby gem for web_push, but its common I assume. From that example you should see how is the body, title and icon keys can be changed.
view
<%= content_tag(:button, "Foo", class: "webpush-button") %>
<script>
$('.webpush-button').on('click', (e) => {
navigator.serviceWorker.ready
.then((serviceWorkerRegistration) => {
serviceWorkerRegistration.pushManager.getSubscription()
.then((subscription) => {
$.post('/push', {
subscription: subscription.toJSON(),
message: "<%= #challenge.name %>",
body: "<%= #challenge.why %>",
icon: "/assets/default.png"
});
});
});
});
</script>
controller
class PushNotificationsController < ApplicationController
def push
Webpush.payload_send(
message: JSON.generate({
title: params[:message],
body: params[:body],
icon: params[:icon]
}),
endpoint: params[:subscription][:endpoint],
p256dh: params[:subscription][:keys][:p256dh],
auth: params[:subscription][:keys][:auth],
vapid: {
subject: "mailto:sender#example.com",
public_key: ENV['VAPID_PUBLIC_KEY'],
private_key: ENV['VAPID_PRIVATE_KEY']
}
)
end
end
js
self.addEventListener("push", (event) => {
var data = event.data.json();
let title = (event.data && data.title) || "Yay a message";
let body = (event.data && data.body) || "We have received a push message";
let icon = (event.data && data.icon) || "/assets/blue-astronaut.png";
event.waitUntil(
self.registration.showNotification(title, { title,body, icon })
)
});

Web Pushnotification 'UnauthorizedRegistration' or 'Gone' or 'Unauthorized'- subscription expires

I have developed a push notification service for my web site. the service worker is:
'use strict';
self.addEventListener('push', function (event) {
var msg = {};
if (event.data) {
msg = event.data.json();
}
let notificationTitle = msg.title;
const notificationOptions = {
body: msg.body,//body
dir:'rtl',//direction
icon: msg.icon,//image
data: {
url: msg.url,//click
},
};
event.waitUntil(
Promise.all([
self.registration.showNotification(
notificationTitle, notificationOptions),
])
);
});
self.addEventListener('notificationclick', function (event) {
event.notification.close();
let clickResponsePromise = Promise.resolve();
if (event.notification.data && event.notification.data.url) {
clickResponsePromise = clients.openWindow(event.notification.data.url);
}
const fetchOptions =
{ method: 'post'};
fetch('http://localhost:5333/usrh.ashx?click=true', fetchOptions).
then(function (response)
{
if (response.status >= 400 && response.status < 500)
{
throw new Error('Failed to send push message via web push protocol');
}
}).catch((err) =>
{
this.showErrorMessage('Ooops Unable to Send a Click', err);
});
});
self.addEventListener('notificationclose', function (event) {
const fetchOptions =
{ method: 'post'};
fetch('http://localhost:5333/usrh.ashx?close=true', fetchOptions).
then(function (response)
{
if (response.status >= 400 && response.status < 500)
{
throw new Error('Failed to send push message via web push protocol');
}
}).catch((err) =>
{
this.showErrorMessage('Ooops Unable to Send a Click', err);
});
});
self.addEventListener('pushsubscriptionchange', function () {
const fetchOptions = {
method: 'post'
,
};
fetch('http://localhost:5333/usru.ashx', fetchOptions)
.then(function (response) {
if (response.status >= 400 && response.status < 500) {
console.log('Failed web push response: ', response, response.status);
throw new Error('Failed to update users.');
}
})
.catch((err) => {
this.showErrorMessage('Ooops Unable to Send a user', err);
});
});
I have subscribed the users successfully using the following code:
registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('http://localhost:5333/service-worker.js')
.catch((err) => {
this.showErrorMessage('Unable to Register SW', 'Sorry this demo requires a service worker to work and it ' + 'failed to install - sorry :(');
console.error(err);
});
} else {
this.showErrorMessage('Service Worker Not Supported', 'Sorry this demo requires service worker support in your browser. ' +
'Please try this demo in Chrome or Firefox Nightly.');
}
}
and
class PushClient {
constructor(subscriptionUpdate, appkeys) {
this._subscriptionUpdate = subscriptionUpdate;
this._publicApplicationKey = appkeys;
if (!('serviceWorker' in navigator)) {
return;
}
if (!('PushManager' in window)) {
return;
}
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
return;
}
navigator.serviceWorker.ready.then(() => {
this.setUpPushPermission();
});
}
setUpPushPermission() {
return navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
return serviceWorkerRegistration.pushManager.getSubscription();
})
.then((subscription) => {
if (!subscription) {
return;
}
this._subscriptionUpdate(subscription);
})
.catch((err) => {
console.log('setUpPushPermission() ', err);
});
}
subscribeDevice() {
return new Promise((resolve, reject) => {
if (Notification.permission === 'denied') {
sc(3);
return reject(new Error('Push messages are blocked.'));
}
if (Notification.permission === 'granted') {
sc(3);
return resolve();
}
if (Notification.permission === 'default') {
Notification.requestPermission((result) => {
if (result === 'denied') {
sc(0);
} else if (result === 'granted') {
sc(1);
} else {
sc(2);
}
if (result !== 'granted') {
reject(new Error('Bad permission result'));
}
resolve();
});
}
})
.then(() => {
return navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
return serviceWorkerRegistration.pushManager.subscribe({
userVisibleOnly: true
, applicationServerKey: this._publicApplicationKey.publicKey
,
});
})
.then((subscription) => {
this._subscriptionUpdate(subscription);
if (subscription) {
this.sendPushMessage(subscription);
}
})
.catch((subscriptionErr) => { });
})
.catch(() => { });
}
toBase64(arrayBuffer, start, end) {
start = start || 0;
end = end || arrayBuffer.byteLength;
const partialBuffer = new Uint8Array(arrayBuffer.slice(start, end));
return btoa(String.fromCharCode.apply(null, partialBuffer));
}
unsubscribeDevice() {
navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
return serviceWorkerRegistration.pushManager.getSubscription();
})
.then((pushSubscription) => {
if (!pushSubscription) {
this._subscriptionUpdate(null);
return;
}
return pushSubscription.unsubscribe()
.then(function (successful) {
if (!successful) {
console.error('We were unable to unregister from push');
}
});
})
.then(() => {
this._subscriptionUpdate(null);
})
.catch((err) => {
console.error('Error thrown while revoking push notifications. ' + 'Most likely because push was never registered', err);
});
}
sendPushMessage(subscription) {
let payloadPromise = Promise.resolve(null);
payloadPromise = JSON.parse(JSON.stringify(subscription));
const vapidPromise = EncryptionHelperFactory.createVapidAuthHeader(this._publicApplicationKey, subscription.endpoint, 'http://localhost:5333/');
return Promise.all([payloadPromise, vapidPromise, ])
.then((results) => {
const payload = results[0];
const vapidHeaders = results[1];
let infoFunction = this.getWebPushInfo;
infoFunction = () => {
return this.getWebPushInfo(subscription, payload, vapidHeaders);
};
const requestInfo = infoFunction();
this.sendRequestToProxyServer(requestInfo);
});
}
getWebPushInfo(subscription, payload, vapidHeaders) {
let body = null;
const headers = {};
headers.TTL = 60;
if (payload) {
headers.Encryption = `auth=${payload.keys.auth}`;
headers['Crypto-Key'] = `p256dh=${payload.keys.p256dh}`;
headers['Content-Encoding'] = 'aesgcm';
} else {
headers['Content-Length'] = 0;
}
if (vapidHeaders) {
headers.Authorization = `WebPush ${vapidHeaders.authorization}`;
if (headers['Crypto-Key']) {
headers['Crypto-Key'] = `${headers['Crypto-Key']}; ` + `p256ecdsa=${vapidHeaders.p256ecdsa}`;
} else {
headers['Crypto-Key'] = `p256ecdsa=${vapidHeaders.p256ecdsa}`;
}
}
const response = {
headers: headers
, endpoint: subscription.endpoint
,
};
if (body) {
response.body = body;
}
return response;
}
sendRequestToProxyServer(requestInfo) {
const fetchOptions = {
method: 'post'
,
};
if (requestInfo.body && requestInfo.body instanceof ArrayBuffer) {
requestInfo.body = this.toBase64(requestInfo.body);
fetchOptions.body = requestInfo;
}
fetchOptions.body = JSON.stringify(requestInfo);
fetch('http://localhost:5333/usrh.ashx', fetchOptions)
.then(function (response) {
if (response.status >= 400 && response.status < 500) {
console.log('Failed web push response: ', response, response.status);
throw new Error('Failed to send push message via web push protocol');
}
})
.catch((err) => {
this.showErrorMessage('Ooops Unable to Send a Push', err);
});
}
}
All these codes are in javascript. I can successfully recieve user subscription infromarion on my server like:
Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwcxxxxx
Crypto-Key: p256dh=BBp90dwDWxxxxc1TfdBjFPqxxxxxwjO9fCip-K_Eebmg=; p256ecdsa=BDd3_hVL9fZi9Yboxxxxxxo
endpoint: https://fcm.googleapis.com/fcm/send/cxxxxxxxxxxxxxxJRorOMHKLQ3gtT7
Encryption: auth=9PzQZ1mut99qxxxxxxxxxxyw==
Content-Encoding: aesgcm
Also I can successfully send a push to this user using the code bellow in C#:
public static async Task<bool> SendNotificationByte(string endpoint, string[] Keys, byte[] userSecret, byte[] data = null,
int ttl = 0, ushort padding = 0, bool randomisePadding = false, string auth="")
{
#region send
HttpRequestMessage Request = new HttpRequestMessage(HttpMethod.Post, endpoint);
Request.Headers.TryAddWithoutValidation("Authorization", auth);
Request.Headers.Add("TTL", ttl.ToString());
if (data != null && Keys[1] != null && userSecret != null)
{
EncryptionResult Package = EncryptMessage(Decode(Keys[1]), userSecret, data, padding, randomisePadding);
Request.Content = new ByteArrayContent(Package.Payload);
Request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
Request.Content.Headers.ContentLength = Package.Payload.Length;
Request.Content.Headers.ContentEncoding.Add("aesgcm");
Request.Headers.Add("Crypto-Key", "dh=" + Encode(Package.PublicKey)+" ;"+Keys[2]+"="+Keys[3]);
Request.Headers.Add("Encryption", "salt=" + Encode(Package.Salt));
}
using (HttpClient HC = new HttpClient())
{
HttpResponseMessage res = await HC.SendAsync(Request).ConfigureAwait(false);
if (res.StatusCode == HttpStatusCode.Created)
return true;
else return false;
}
#endregion
}
The problem is that after a period of time (about 20 hours or even less), when I want to send a push to this user I got the following errors:
firefox subscription:
{StatusCode: 410, ReasonPhrase: 'Gone', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
Access-Control-Allow-Headers: content-encoding,encryption,crypto-key,ttl,encryption-key,content-type,authorization
Access-Control-Allow-Methods: POST
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: location,www-authenticate
Connection: keep-alive
Cache-Control: max-age=86400
Date: Tue, 21 Feb 2017 08:19:03 GMT
Server: nginx
Content-Length: 179
Content-Type: application/json
}}
chrome subscription:
{StatusCode: 400, ReasonPhrase: 'UnauthorizedRegistration', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Alt-Svc: quic=":443"; ma=2592000; v="35,34"
Vary: Accept-Encoding
Transfer-Encoding: chunked
Accept-Ranges: none
Cache-Control: max-age=0, private
Date: Tue, 21 Feb 2017 08:18:35 GMT
Server: GSE
Content-Type: text/html; charset=UTF-8
Expires: Tue, 21 Feb 2017 08:18:35 GMT
}}
I think I missed something, that makes the subscription expires, or have to make the users to resubscribe when their subscription information is changed or expired, but I do not know how?!!
The Problem is solved by sending a push echo notification to the subscribed users to resubscribe them. I have wrote a job in which I send a push echo periodically and resubscribe the users and update their information.
To do so I send an special message called "push echo" using the code bellow:
self.addEventListener('push', function (event) {
lastEventName = 'push';
var msg = {};
if (event.data) {
msg = event.data.json();
if (!!msg.isEcho) {
self.registration.pushManager.getSubscription()
.then(function (subscription) {
if (!subscription) {
} else {
subscription.unsubscribe().then(function () {
self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64UrlToUint8Array('xxxxxxxxxxxxxxxx') })
.then(function (subscription) {
resubscription(subscription);
});
});
}
});
return;
}
}
if (!!msg.isEcho)
return;
let notificationTitle = msg.title;
const notificationOptions = {
body: msg.body,
dir: 'rtl',
icon: msg.icon,
data: {
url: msg.url,
id: msg.id,
key: msg.key
},
};
event.waitUntil(
Promise.all([
self.registration.showNotification(
notificationTitle, notificationOptions),
])
);
const fetchOptions =
{ method: 'post', mode: 'no-cors' };
fetch('http://example.com', fetchOptions).
then(function (response) {
if (response.status >= 400 && response.status < 500) {
throw new Error('Failed to send push message via web push protocol');
}
lastEventName = 'view';
}).catch((err) => {
this.showErrorMessage('Ooops Unable to Send a Click', err);
});
});
In the resubscription method you can unsubscribe and then subscribe the user and update server data.
I think the issue is about how you send your applicationServerKey. I just have done an example of what you want to do and I had to send that key encoded with this function:
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
so you have to create your subscription object in this way :
registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(this._publicApplicationKey.publicKey),
})
Mainly what I did was follow this tutorial.
I have that working example in this github repo. The README file is in Spanish, but I think it can help you.
I think the problem can be solved by resubscribing the users.
Some clues:
The sw needs to be registered and activated, at the time the push event arrives. This mean that you may not clean session, use private browsing mode, clean cache of computer in between.
The push event must come from the same origin.

Categories

Resources