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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With