How to get registration id in push event in ServiceWorker? - service-worker

I have the registration id when subscribing, but I can't figure out how to get it in the service worker file when I get the push notification?
client.js
pushManager.subscribe({ userVisibleOnly: true }).then(({ endpoint }) => {
const registrationId = endpoint.split('https://android.googleapis.com/gcm/send/')[1];
// How do I get this registrationId in service-worker.js below?
});
service-worker.js
self.addEventListener('push', event => {
// I'd like to make a request to server to get the notification data, but I need the registrationId for that. How do I get registration id here?
});

Inside the ServiceWorkerGlobalScope, you can get the effective ServiceWorkerRegistration, which in turns exposes the PushManager, which then gives you a getSubscription() method, which returns a Promise that resolves with the current subscription.
There are a lot of layers to follow, but the actual code is fairly straightforward:
self.registration.pushManager.getSubscription().then(subscription => {
// Do something with subscription.endpoint
});

Related

Fetch of the service worker doesn't seem to get triggered

When a browser requests an image from the server, the call is getting picked up by an API controller in the back end. There, a authorization check must be done before returning the image in order to check if the request is allowed or not.
So I need to add the authorization header and when searching for the best solution, I found this article: https://www.twelve21.io/how-to-access-images-securely-with-oauth-2-0/ and I was mostly intereseted in the solution number 4 which uses a Service Worker.
I made my own implementation, I registered a serviceWorker:
if ('serviceWorker' in navigator) {
console.log("serviceWorker active");
window.addEventListener('load', onLoad);
}
else {
console.log("serviceWorker not active");
}
function onLoad() {
console.log("onLoad is called");
var scope = {
scope: '/api/imagesgateway/'
};
navigator.serviceWorker.register('/Scripts/ServiceWorker/imageInterceptor.js', scope)
.then(registration => console.log("ServiceWorker registration successful with scope: ", registration.scope))
.catch(error => console.error("ServiceWorker registration failed: ", error));
}
and this is in my imageInterceptor:
self.addEventListener('fetch', event => {
console.log("fetch event triggered");
event.respondWith(
fetch(event.request, {
mode: 'cors',
credentials: 'include',
header: {
'Authorization': 'Bearer ...'
}
})
)
});
When I run my application, I see in my console that the registration seems to be successfully executed as I see the console.logs printed (ServiceWorker active, onLoad is called and successful registration with correct scope: https://localhost:44332/api/imagesgateway/
But when I load an image (https://localhost:44332/api/imagesgateway/...) via the gateway, I still get a 400 and when put a breakpoint on the backend I see that the authentication header is still null. Also, I don't see "fetch event triggered" message in my console. In another article it is stated that I can see the registered service workers via this setting: chrome://inspect/#service-workers but I don't see my worker there either.
My question is: Why isn't the authorization header added? Is it because, although the registration seems to go successfully, this isn't actually the case and therefore I don't see the worker in inspect#service-workers either?
You're not seeing fetch event triggered in the browser console because your Service Worker script isn't allowed to intercept the image requests. This is because your Service Worker script is located in a directory outside the scope of the requests you're interested in.
In order to intercept requests that handle resources at
/api/imagesgateway/
the SW script needs to be located in either
/, /api/, or /api/imagesgateway/. It cannot be located in /some/other/directory/service-worker.js.
This is the reason that your Service Worker registers successfully! There is no probelm in registering the SW. The problem lies in what it can do.
More info: Understanding Service Worker scope

Save a user's pushSubscription info in Rails database?

I have followed this tutorial: https://dzone.com/articles/how-to-add-real-web-push-notifications-to-your-web to enable push notifications on my rails app. I am using the webpush gem to send the notifications.
So far, all I have managed to do is get the browser to ask for permission to send notifications, and when I try to call the method send_web_push_notification (shown below) line 2 is throwing up an error.
I think it is because I am not saving the user's pushSubscription info to the database, but I don't know how to do this. In the tutorial, there is this line at the end: 'We use a database JSON field called web_push_subscription to save the pushSubscription info on our users.'
Would someone be able to show me how to do this?
Any help would be appreciated. Thanks.
send_web_push_notification method:
def send_web_push_notification(user_id)
subscription = User.find(user_id).web_push_subscription
message = {
title: "You have a message!",
body: "This is the message body",
tag: "new-message"
}
unless subscription.nil?
Webpush.payload_send(
message: JSON.generate(message),
endpoint: subscription["endpoint"],
p256dh: subscription["keys"]["p256dh"],
auth: subscription["keys"]["auth"],
ttl: 15,
vapid: {
subject: 'mailto:admin#example.com',
public_key: Rails.application.config.webpush_keys[:public_key],
private_key: Rails.application.config.webpush_keys[:private_key]
}
)
end
end
serviceworker.js.erb:
function showNotification(event) {
return new Promise(resolve => {
const { body, title, tag } = JSON.parse(event.data.text());
self.registration
.getNotifications({ tag })
.then(existingNotifications => { // close? ignore? })
.then(() => {
const icon = `/path/to/icon`;
return self.registration
.showNotification(title, { body, tag, icon })
})
.then(resolve)
})
}
self.addEventListener("push", event => {
event.waitUntil(
showNotification(event)
);
}
});
self.addEventListener("notificationclick", event => {
event.waitUntil(clients.openWindow("/"));
});
application.js:
const permission = Notification.requestPermission();
if (permission !== 'granted') {
// no notifications
}else{
// yay notifications
}
function subscribeToPushNotifications(registration) {
return registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: window.vapidPublicKey
})
.then(pushSubscription => {
console.log(
"Received PushSubscription:",
JSON.stringify(pushSubscription)
);
return pushSubscription;
});
}
If you look closely codes you will notice that you have to create a Json field in database to save subscription. If there is subscription available than push notification will be sent. Actually there many more scenarios it is not necessary the you want to save one browser for user notification, if you plan multiple browser than you have to create separate table, but if you want to add one browser for push notification, than you can add this information in user table too. Create new migration to update your user table and add following column
t.json "web_push_subscription"
Run migration, Now you have Json column if you notice code clearly following are information you require in your user database, you will save this information when user subscribe for push notification
user. web_push_subscription[:endpoint] = what_ever_value_received
user.web_push_subscription[:auth] = what_ever_value_received
Unfortunately it is just idea as I have not implement it, but I should check JSON.stringify(pushSubscription) object recived, and there are chances all data would be in this response which you received you may need to save it as it is to your subscription.
You also need to save permission, that user really allowed you to send notification, if yes than one field in user as boolean notification = true, so you can check if user allow you to send notification, than you can send, otherwise don't send. You should also have way to remove these keys for specific user when they unsubscribe notifications.
You basically need to update a model, which is backend, but you do not want the user to go through all that process. This is where ajax comes in handy. I am not very comfortable with ajax but it is one of the best things provided by JS.
With the code in ajax function, you will hit the controller update action with the changed attribute and the update will change the model as necessary and update it. then your html will change accordingly without page refresh.
TLDR: I think you are looking for this.

Service worker remote/foreign installation [duplicate]

I have two subdomains: https://abc.xxxx.com and https://xyz.xxxx.com. So my questions:
1). is it possible to register a service worker for
https://xyz.xxxx.com from https://abc.xxxx.com ? if yes then how?
2). if http://abc.xxxx.com (http insecure) then anyway to register
a service worker for https://xyz.xxxx.com from http://abc.xxxx.com like in iframe or something....
This is a real situation, I am facing for my multiple subdomain. Any help appreciated. Thanks in advance.
Here are some general answers that I think should address the various points you raise in your question:
Each registered service worker has an associated scope, which dictates the set of web pages that the service worker can control. The scope of a service worker is a URL, and that URL must have the same origin as the page that registers the service worker, and must be either a URL that corresponds to the same path level as the page or a path that's one or more levels down. The default scope corresponds to the same path level as location of the service worker script. Because of this restriction, it's not possible to call navigator.serviceWorker.register(...) from a page on one (sub-)domain and end up with a service worker that controls pages on another (sub-)domain.
There are restrictions in place to prevent you from throwing an https: <iframe> on an http: page and using that to register a service worker. See DOMException when registering service worker inside an https iframe
Though I don't know that it's directly related to your question, explicitly calling fetch() for an http: resource within your service worker code will result in a failure in current versions of Chrome, since mixed-content fetch()s are not allowed within a service worker. I don't know if things are 100% settled on that front, and this open bug is still relevant.
If you have pages that live on both abc.123.com and xyz.123.com and you want both sets of pages to be controlled by a service worker, then you need to have two separate service worker registrations. Each registration needs to be for a copy of your service worker JS file that's hosted on the respective domain corresponding to the top-level page, and all pages and service worker scripts need to be accessed via https:.
That being said, you can kick off a service worker registration for a different domain by including a cross-domain <iframe> on a page, but both the host page and the <iframe> need to be served via https:. The normal service worker scoping restrictions apply, so if, for example, you want to register a service worker for the other domain that will cover the entire https://other-domain.com/ scope, you need to make sure that the location of the service worker script being registered is at the top-level, e.g. https://other-domain.com/service-worker.js, not at https://other-domain.com/path/to/service-worker.js. This is the approach used by, for example, the AMP project via the <amp-install-serviceworker> element.
Service Worker scripts must be hosted at the same origin (Protocol + Domain name + Port). Each sub-domain is considered a different origin, So, you will need to register a service worker for each one. Each of these workers will have its own cache and scope.
Try use Ngnix proxy_pass. This work for me.
My bad, I misunderstood a bit. Well, here's the code
if('serviceWorker' in navigator){
if(window.location.pathname != '/'){
//register with API
if(!navigator.serviceWorker.controller) navigator.serviceWorker.register('/service-worker', { scope: '/' });
//once registration is complete
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration){
//get subscription
serviceWorkerRegistration.pushManager.getSubscription().then(function(subscription){
//enable the user to alter the subscription
//jquery selector for enabling whatever you use to subscribe.removeAttr("disabled");
//set it to allready subscribed if it is so
if(subscription){
//code for showing the user that they're allready subscribed
}
});
});
}
}else{
console.warn('Service workers aren\'t supported in this browser.');
}
then here's the event -ish for your subscribe / unsubscribe
// subscribe or unsubscribe to the ServiceWorker
$(document.body).on('change', /*selector*/, function(){
//new state is checked so we subscribe
if($(this).prop('checked')){
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration){
serviceWorkerRegistration.pushManager.subscribe()
.then(function(subscription){
// The subscription was successful
console.log('subscription successful'); //subscription.subscriptionId
//save in DB - this is important because
$.post($('#basePath').val() + 'settings/ajax-SW-sub/', {id:subscription.subscriptionId}, function(data){
//console.log(data);
}, 'json');
}).catch(function(e) {
if (Notification.permission === 'denied') {
// The user denied the notification permission which
// means we failed to subscribe and the user will need
// to manually change the notification permission to
// subscribe to push messages
console.warn('Permission for Notifications was denied');
} else {
// A problem occurred with the subscription; common reasons
// include network errors, and lacking gcm_sender_id and/or
// gcm_user_visible_only in the manifest.
console.error('Unable to subscribe to push.', e);
}
});
});//*/
//new state us unchecked so we unsubscribe
}else{
$('.js-enable-sub-test').parent().removeClass('checked');
//get subscription
navigator.serviceWorker.ready.then(function(reg) {
reg.pushManager.getSubscription().then(function(subscription) {
//unregister in db
$.post($('#basePath').val() + 'settings/ajax-SW-unsub/', {id:subscription.subscriptionId}, function(data){
//console.log(data);
}, 'json');
//remove subscription from google servers
subscription.unsubscribe().then(function(successful) {
// You've successfully unsubscribed
console.log('unsubscribe successful');
}).catch(function(e) {
// Unsubscription failed
console.log('unsubscribe failed', e);
})
})
});//*/
}
});
after that you need to register an account on the google developer console and register a project for something like *.xxxx.com . Then you need to get a proper manifest json with gcm_sender_id and gcm_user_visible_only
You need to create a key for both server and browser applications, there's more info on that on this page.
https://developers.google.com/web/updates/2015/03/push-notificatons-on-the-open-web?hl=en
The one for browser applications goes in your manifest json.
Then to send out push notifications you'll be using something like this:
function addSWmessage($args){
$output = false;
if(!isset($args['expiration']) || $args['expiration'] == ''){
$args['expiration'] = date('Y-m-d H:i:s', strtotime('+7 days', time()));
}
$sql = sprintf("INSERT INTO `serviceworker_messages` SET title = '%s', body = '%s', imageurl = '%s', linkurl = '%s', hash = '%s', expiration = '%s'",
parent::escape_string($args['title']),
parent::escape_string($args['text']),
parent::escape_string($args['imageurl']),
parent::escape_string($args['linkurl']),
parent::escape_string(md5(uniqid('******************', true))),
parent::escape_string($args['expiration']));
if($id = parent::insert($sql)){
$output = $id;
}
return $output;
}
function pushSWmessage($args){
//$args['messageid'] $args['userids'][]
foreach($args['userids'] as $val){
$sql = sprintf("SELECT messages_mobile, messages FROM `users_serviceworker_hash` WHERE users_id = '%s'",
parent::escape_string($val));
if($row = parent::queryOne($sql)){
$m1 = json_decode($row['messages'], true);
$m1[] = $args['messageid'];
$m2 = json_decode($row['messages_mobile'], true);
$m2[] = $args['messageid'];
$sql = sprintf("UPDATE `users_serviceworker_hash` SET messages = '%s', messages_mobile = '%s' WHERE users_id = '%s'",
parent::escape_string(json_encode($m1)),
parent::escape_string(json_encode($m2)),
parent::escape_string($val['users_id']));
parent::insert($sql);
}
}
$sql = sprintf("SELECT subscriptionID, users_id FROM `users_serviceworker_subscriptions`");
if($rows = parent::query($sql)){
foreach($rows as $val){
if(in_array($val['users_id'], $args['userids'])){
$registrationIds[] = $val['subscriptionID'];
}
}
if(isset($registrationIds) && !empty($registrationIds)){
// prep the bundle
$msg = array
(
'message' => '!',
'title' => '!',
'subtitle' => '!',
'tickerText' => '!',
'vibrate' => 1,
'sound' => 1,
'largeIcon' => '!',
'smallIcon' => '!'
);
$headers = array
(
'Authorization: key='.SW_API_ACCESS_KEY,
'Content-Type: application/json'
);
$fields = array
(
'registration_ids' => $registrationIds,
'data' => $msg
);
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL, 'https://android.googleapis.com/gcm/send');
curl_setopt($ch,CURLOPT_POST, true);
curl_setopt($ch,CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch,CURLOPT_POSTFIELDS, json_encode($fields));
curl_exec($ch);
curl_close($ch);
}
}
}
And no, I don't know what issue you've been having but this works for me with multiple sub domains. :)

React Native Network request Failed on fetch - Works Local

I am using some code in a React Native component that is a simple fetch passing a parameter to OMDB API. This could be a CORS issue since if I run it in the format below going directly to omdbapi.com it fails always with Network request Failed. This request however works in the Android emulator on the same network.
// The fetchData function makes an AJAX call to the OMDB API.
fetchData(movieinput) {
console.log("In fetch");
// We pass the movie the user entered in into the URL for the API call.
fetch('http://www.omdbapi.com/?t='+movieinput+'&y=&plot=short&r=json')
.then((response) => response.json())
.then((responseData) => {
// After the data is recieved, we set this.state.movie to the result of the API call.
this.setState({
movie: responseData,
});
})
.done();
}
If however I run the same code going to a local URL that wraps the remote request into a localhost request, it works correctly.
fetchData(movieinput) {
console.log("In fetch");
// We pass the movie the user entered in into the URL for the API call.
fetch('http://localhost:3000/movie/'+movieinput)
.then((response) => response.json())
.then((responseData) => {
// After the data is recieved, we set this.state.movie to the result of the API call.
this.setState({
movie: JSON.parse(responseData),
});
})
.done();
}
Any ideas?
The problem is probably App Transport Security, an iOS specific restriction that (by default) forces apps to use https for all network communication. Here is a video tutorial explaining how to tweak ATS so that your react native app will work: http://codecookbook.co/post/how-to-make-network-requests-in-react-native/

APNS (Apple Push Notification Service) with Node JS

I am looking to create APNS (Apple Push Notification Service), where the server will be sending notifications to the iOS devices.
I am able to make the push notifications work via PHP using the SAME device token and the SAME certificate, however, I would like to send notifications via Node JS instead of PHP.
I have the following valid files/certificates to help me get started:
cert.pem
key.pem
aps_development.cer
cert.p12
key.p12,
ck.pem
I've been looking through several resources/links such as:
https://github.com/argon/node-apn
How to implement APNS notifications through nodejs?
After doing so, I was able to come up with the following sample code, where PASSWORD stands for the password of the key.pem and TOKEN stands for my device's token:
var apn = require("apn");
var path = require('path');
try {
var options = {
cert: path.join(__dirname, 'cert.pem'), // Certificate file path
key: path.join(__dirname, 'key.pem'), // Key file path
passphrase: '<PASSWORD>', // A passphrase for the Key file
ca: path.join(__dirname, 'aps_development.cer'),// String or Buffer of CA data to use for the TLS connection
production:false,
gateway: 'gateway.sandbox.push.apple.com', // gateway address
port: 2195, // gateway port
enhanced: true // enable enhanced format
};
var apnConnection = new apn.Connection(options);
var myDevice = new apn.Device("<TOKEN>");
var note = new apn.Notification();
note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
note.badge = 3;
note.sound = "ping.aiff";
note.alert = "You have a new message";
note.payload = {'msgFrom': 'Alex'};
note.device = myDevice;
apnConnection.pushNotification(note);
process.stdout.write("******* EXECUTED WITHOUT ERRORS************ :");
} catch (ex) {
process.stdout.write("ERROR :"+ex);
}
I get no errors when executing this code, but The problem is that no notification is received on my iOS device. I have also tried setting the ca:null & debug:true (in options var). But same thing happens.
Again, when I use the ck.pem & device token that I have and use it with PHP, it works, but i'm not able to make it work in Node JS. PLEASE HELP!!
Thank you so much!
You are probably running into the asynchronous nature of NodeJS itself. I use the same node-apn module with great success. But you don't just call it directly like you're used to in PHP - that's a synchronous model that doesn't map from PHP->Node. Your process is exiting before anything can actually happen - the apnConnection.pushNotification(note); is an asynchronous call that just barely gets started before your script returns/exits.
As noted in the node-apn docs you probably want to "listen for" additional events on apnConnection. Here's an excerpt of code that I use to log out various events that are occurring on the connection after it's created:
// We were unable to initialize the APN layer - most likely a cert issue.
connection.on('error', function(error) {
console.error('APNS: Initialization error', error);
});
// A submission action has completed. This just means the message was submitted, not actually delivered.
connection.on('completed', function(a) {
console.log('APNS: Completed sending', a);
});
// A message has been transmitted.
connection.on('transmitted', function(notification, device) {
console.log('APNS: Successfully transmitted message');
});
// There was a problem sending a message.
connection.on('transmissionError', function(errorCode, notification, device) {
var deviceToken = device.toString('hex').toUpperCase();
if (errorCode === 8) {
console.log('APNS: Transmission error -- invalid token', errorCode, deviceToken);
// Do something with deviceToken here - delete it from the database?
} else {
console.error('APNS: Transmission error', errorCode, deviceToken);
}
});
connection.on('connected', function() {
console.log('APNS: Connected');
});
connection.on('timeout', function() {
console.error('APNS: Connection timeout');
});
connection.on('disconnected', function() {
console.error('APNS: Lost connection');
});
connection.on('socketError', console.log);
Equally important, you need to make sure your script STAYS RUNNING while the async requests are being processed. Most of the time, as you build a bigger and bigger service, you're going to end up with some kind of event loop that does this, and frameworks like ActionHero, ExpressJS, Sails, etc. will do this for you.
In the meantime, you can confirm it with this super-crude loop, which just forces the process to stay running until you hit CTRL+C:
setInterval(function() {
console.log('Waiting for events...');
}, 5000);
I will explain it with simple code
First install apn module using this command npm install apn .
Require that module in code
var apn = require('apn');
let service = new apn.Provider({
cert: "apns.pem",
key: "p12Cert.pem",
passphrase:"123456",
production: true //use this when you are using your application in production.For development it doesn't need
});
Here is the main heart of notification
let note = new apn.Notification({
payload:{
"staffid":admins[j]._id,
"schoolid":admins[j].schoolid,
"prgmid":resultt.programid
},
category:"Billing",
alert:"Fee payment is pending for your approval",
sound:"ping.aiff",
topic:"com.xxx.yyy",//this is the bundle name of your application.This key is needed for production
contentAvailable: 1//this key is also needed for production
});
console.log(`Sending: ${note.compile()} to ${ios}`);
services.send(note, ios).then( result => {//ios key is holding array of device ID's to which notification has to be sent
console.log("sent:", result.sent.length);
console.log("failed:", result.failed.length);
console.log(result.failed);
});
services.shutdown();
In Payload you can send data with custom keys.I hope it helps

Resources