Skip to content
This repository has been archived by the owner on Sep 4, 2020. It is now read-only.

Browser webpush #2661

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ All available option attributes are described bellow. Currently, there are no Wi

#### Browser

| Attribute | Type | Default | Description |
| ------------------------------ | -------- | ------------------------------------------------------------ | ------------------------------------------------------- |
| `browser.pushServiceURL` | `string` | `http://push.api.phonegap.com/v1/push` | Optional. URL for the push server you want to use. |
| `browser.applicationServerKey` | `string` | | Optional. Your GCM API key if you are using VAPID keys. |
| Attribute | Type | Default | Description |
| ------------------------------ | -------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| `browser.applicationServerKey` | `string` | | Optional. Your VAPID public key / Firebase Cloud Messaging Web Push Certificate |

#### iOS

Expand Down Expand Up @@ -140,7 +139,7 @@ The "finish" method has not use too when the VoIP notifications are enabled.
const push = PushNotification.init({
android: {},
browser: {
pushServiceURL: 'http://push.api.phonegap.com/v1/push'
applicationServerKey: '...'
},
ios: {
alert: 'true',
Expand Down Expand Up @@ -294,17 +293,19 @@ The event `registration` will be triggered on each successful registration with

### Callback parameters

| Parameter | Type | Description |
| ----------------------- | -------- | ------------------------------------------------------------------------------- |
| `data.registrationId` | `string` | The registration ID provided by the 3rd party remote push service. |
| `data.registrationType` | `string` | The registration type of the 3rd party remote push service. Either FCM or APNS. |
| Parameter | Type | Description |
| ----------------------- | -------- | ----------------------------------------------------------------------------------------- |
| `data.registrationId` | `string` | The registration ID provided by the 3rd party remote push service. |
| `data.registrationType` | `string` | The registration type of the 3rd party remote push service. Either FCM, APNS or WEB_PUSH. |
| `data.subscription` | `string` | Browser only. A JSON object provided by the browser containing subscription details. |

### Example

```javascript
push.on('registration', data => {
console.log(data.registrationId);
console.log(data.registrationType);
console.log(data.subscription);
});
```

Expand Down
127 changes: 123 additions & 4 deletions docs/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ phonegap create my-app --template phonegap-template-push
const push = PushNotification.init({
android: {
},
browser: {
pushServiceURL: 'http://push.api.phonegap.com/v1/push'
},
browser: {
applicationServerKey: "..."
},
ios: {
alert: "true",
badge: "true",
Expand All @@ -24,7 +24,9 @@ const push = PushNotification.init({
});

push.on('registration', (data) => {
// data.registrationId
// data.registrationType,
// data.registrationId,
// data.subscription (on browser platform)
});

push.on('notification', (data) => {
Expand All @@ -40,3 +42,120 @@ push.on('error', (e) => {
// e.message
});
```
## Browser Web Push

Some users may wish to use `phonegap-plugin-push` to deliver push notifications to browsers via a
[WebPush](https://tools.ietf.org/html/rfc8030) compliant service, such as:

* [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/) (FCM) for Chrome and Samsung browser
* [Mozilla Cloud Services](https://support.mozilla.org/en-US/kb/push-notifications-firefox) (MCS) for Mozilla Firefox
* [Windows Push Notification Services](https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/windows-push-notification-services--wns--overview) (WNS) for Microsoft Edge

The plumbing for WebPush is quite complex, so we'll walk through an example of how to set it up. We'll create the
following baseline artifacts to spin up a basic WebPush service:

* **A VAPID public key:** to register our browser with the browser vendor's push platform (i.e. FCM, MCS, WNS)
* **A `PushNotification` `registration` event handler** to pass the vendor endpoint details to our web app after our browser has registered with the vendor's push platform
* **A service worker** to process incoming notifications from the vendor's push platform

We'll be using FCM to generate our VAPID key, but you could also get
one elsewhere or generate your own - the approach is vendor neutral.

#### VAPID public key

VAPID stands for
[_Voluntary Application Server Identification for Web Push_](https://datatracker.ietf.org/doc/draft-ietf-webpush-vapid/).
A _VAPID public key_ is a Base64 string containing an encryption key that the vendor push platform (FCM, MCS, WNS) will use to
authenticate your application server.

If you're using FCM you can generate a VAPID public key by browsing to your project home and
selecting _Settings_ > _Cloud Messaging_ > _Web Configuration_ > _Web Push certificates_ and generating a key pair.

The Base64 string under the heading 'key pair' is what needs to go into your `PushNotification.init` configuration as
the browser platform's `applicationServerKey`:
```
{
...
browser: {
applicationServerKey: '<VAPID PUBLIC KEY GOES HERE>'
},
...
}
```

#### Registration event handler

Calling `PushNotification.init` from a browser when an `applicationServerKey` is provided will trigger an attempt to
subscribe the browser instance to its vendor's push service (FCM for Chrome, MCS for Firefox, WNS for Edge).

If our attempt to register with the vendor's push platform is successful, it triggers a `registration` event from
`PushNotification`. The payload for the `registration` event is a dict that describes our subscription endpoint on the vendor's push
platform. It will look similar to the dicts you receive for iOS and Android, but with an additional `subscription`
element. For example:
```
{
"registrationType": "WEB_PUSH",
"registrationId": "blahblahblah",
"subscription": {
"endpoint": "https://fcm.googleapis.com/fcm/send/blahblah:blahblahblah",
"expirationTime":null,
"keys": {
"p256dh": "blahblahblah"
}
}
}
```
Our server-side code needs this data to send a notification to that user. We can post it to our server API when the
`registration` event fires:
```
const push = PushNotification.init({
// Your push configuration
});

push.on('registration', regData => {
...
axios('https://api.myservice.com/webpush/subscriptions', {
headers: {' content-type': 'application/json' },
method: 'POST',
data: regData,
})
.then(resp => ...)
.catch(err => ...);
...
}));
```
We can then store the user's subscription data on our server along with the user's ID, and retrieve it when we want to push
a notification to that user.

Pushing a notification from our server to the user becomes straightforward, especially
using a WebPush library like [web-push](https://github.com/web-push-libs/web-push).

For example, using Node.js:
```
const webPush = require('web-push');

const data = <data object posted from client>

await webPush.sendNotification(data.subscription, "This is a test");
```
The above code pushes a simple string notification to a specific browser subscription. However, the browser does not
yet know what to do with our notifications, so we need to create a service worker that can handle them.

#### Service Worker

To process notifications, we must provide an event handler for `push` events in our app's
[service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) (see how to create a service
worker [here](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers)).

Here is an example handler that assumes your push payload is a simple string (as sent in the earlier server-side example) and displays it as a standard notification:
```
...
self.addEventListener('push', function (event) {
console.log(event.data);
event.waitUntil(self.registration.showNotification(event.data));
});
...
```
See your browser's `push` documentation for more details and more complex examples.

Once you've implemented the above patterns you should be able to receive notifications in Chrome-based browsers, Mozilla Firefox and Microsoft Edge.
4 changes: 2 additions & 2 deletions docs/PAYLOAD.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ By default the icon displayed in your push notification will be your apps icon.
const push = PushNotification.init({
android: {},
browser: {
pushServiceURL: 'http://push.api.phonegap.com/v1/push'
applicationServerKey: '...'
},
ios: {
alert: 'true',
Expand Down Expand Up @@ -388,7 +388,7 @@ const push = PushNotification.init({
iconColor: 'blue'
},
browser: {
pushServiceURL: 'http://push.api.phonegap.com/v1/push'
applicationServerKey: '...'
},
ios: {
alert: 'true',
Expand Down
3 changes: 0 additions & 3 deletions plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@
<js-module src="www/browser/push.js" name="BrowserPush">
<clobbers target="PushNotification"/>
</js-module>
<asset src="src/browser/ServiceWorker.js" target="ServiceWorker.js"/>
<asset src="src/browser/manifest.json" target="manifest.json"/>
<hook type="after_prepare" src="hooks/browser/updateManifest.js"/>
</platform>
<platform name="ios">
<config-file target="config.xml" parent="/*">
Expand Down
18 changes: 13 additions & 5 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,14 @@ declare namespace PhonegapPluginPush {
*/
browser?: {
/**
* URL for the push server you want to use. Default is 'http://push.api.phonegap.com/v1/push'.
* Your VAPID public key / Firebase Cloud Messaging Web Push Certificate.
*/
pushServiceURL?: string
applicationServerKey: string
/**
* Your GCM API key if you are using VAPID keys.
* Endpoint URL
*/
applicationServerKey?: string
}
pushServiceURL: string
}

/**
* iOS specific initialization options.
Expand Down Expand Up @@ -260,6 +260,14 @@ declare namespace PhonegapPluginPush {
* The registration ID provided by the 3rd party remote push service.
*/
registrationId: string
/**
* The type of registration, provided by some 3rd party push services. E.g. WEB_PUSH
*/
registrationType?: string
/**
* The PushSubscription object associated with our registration if we're using web push
*/
subscription?: any
}

interface NotificationEventResponse {
Expand Down
78 changes: 25 additions & 53 deletions www/browser/push.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,52 +38,39 @@ var PushNotification = function(options) {
// triggered on registration and notification
var that = this;

// Add manifest.json to main HTML file
var linkElement = document.createElement('link');
linkElement.rel = 'manifest';
linkElement.href = 'manifest.json';
document.getElementsByTagName('head')[0].appendChild(linkElement);

if ('serviceWorker' in navigator && 'MessageChannel' in window) {
var result;
var channel = new MessageChannel();
channel.port1.onmessage = function(event) {
that.emit('notification', event.data);
};

navigator.serviceWorker.register('ServiceWorker.js').then(function() {
return navigator.serviceWorker.ready;
})
.then(function(reg) {
serviceWorker = reg;
reg.pushManager.subscribe(subOptions).then(function(sub) {
subscription = sub;
result = { 'registrationId': sub.endpoint.substring(sub.endpoint.lastIndexOf('/') + 1) };
that.emit('registration', result);

// send encryption keys to push server
var xmlHttp = new XMLHttpRequest();
var xmlURL = (options.browser.pushServiceURL || 'http://push.api.phonegap.com/v1/push') + '/keys';
xmlHttp.open('POST', xmlURL, true);

var formData = new FormData();
formData.append('subscription', JSON.stringify(sub));

xmlHttp.send(formData);

navigator.serviceWorker.controller.postMessage(result, [channel.port2]);
}).catch(function(error) {
if (navigator.serviceWorker.controller === null) {
// When you first register a SW, need a page reload to handle network operations
window.location.reload();
return;
}

throw new Error('Error subscribing for Push notifications.');
});
navigator.serviceWorker.ready.then(function(reg) {
if (!reg.pushManager) {
console.log('No pushManager in service worker - notifications disabled');
} else {
reg.pushManager.subscribe(subOptions)
.then(function (sub) {
result = {
'registrationType': 'WEB_PUSH',
'registrationId': sub.endpoint.substring(sub.endpoint.lastIndexOf('/') + 1),
'subscription': sub.toJSON()
};
that.emit('registration', result);
navigator.serviceWorker.controller.postMessage(result, [channel.port2]);
})
.catch(function (error) {
if (navigator.serviceWorker.controller === null) {
// When you first register a SW, need a page reload to handle network operations
window.location.reload();
return;
}
throw error;
});
}
}).catch(function(error) {
console.log(error);
throw new Error('Error registering Service Worker');
throw error;
});
} else {
throw new Error('Service Workers are not supported on your browser.');
Expand Down Expand Up @@ -116,22 +103,7 @@ PushNotification.prototype.unregister = function(successCallback, errorCallback,
};
}

if (serviceWorker) {
serviceWorker.unregister().then(function(isSuccess) {
if (isSuccess) {
var deviceID = subscription.endpoint.substring(subscription.endpoint.lastIndexOf('/') + 1);
var xmlHttp = new XMLHttpRequest();
var xmlURL = (that.options.browser.pushServiceURL || 'http://push.api.phonegap.com/v1/push')
+ '/keys/' + deviceID;
xmlHttp.open('DELETE', xmlURL, true);
xmlHttp.send();

successCallback();
} else {
errorCallback();
}
});
}
successCallback();
};

/**
Expand Down