3
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7
Code distributed by Google as part of the polymer project is also
8
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
10
<link rel="import" href="../polymer/polymer.html">
11
<link rel="import" href="../promise-polyfill/promise-polyfill.html">
16
// TODO: Doesn't work for IE or Safari, and the usual
17
// document.getElementsByTagName('script') workaround seems to be broken by
18
// HTML imports. Not important for now as neither of those browsers support
19
// service worker yet.
20
var currentScript = (document.currentScript || {}).baseURI;
21
var SCOPE = new URL('./$$platinum-push-messaging$$/', currentScript).href;
22
var BASE_URL = new URL('./', document.location.href).href;
23
var SUPPORTED = 'serviceWorker' in navigator &&
24
'PushManager' in window &&
25
'Notification' in window;
28
* @const {Number} The desired version of the service worker to use. This is
29
* not strictly tied to anything except that it should be changed whenever
30
* a breaking change is made to the service worker code.
34
// This allows us to use the PushSubscription attribute type in browsers
35
// where it is not defined.
36
if (!('PushSubscription' in window)) {
37
window.PushSubscription = {};
41
* `<platinum-push-messaging>` sets up a [push messaging][1] subscription
42
* and allows you to define what happens when a push message is received.
44
* The element can be placed anywhere, but should only be used once in a
45
* page. If there are multiple occurrences, only one will be active.
49
* For a complete sample that uses the element, see the [Cat Push
50
* Notifications][3] project.
53
* Push messaging is currently only available in Google Chrome, which
54
* requires you to configure Google Cloud Messaging. Chrome will check that
55
* your page links to a manifest file that contains a `gcm_sender_id` field.
56
* You can find full details of how to set all of this up in the [HTML5
57
* Rocks guide to push notifications][1].
59
* # Notification details
60
* The data for how a notification should be displayed can come from one of
63
* Firstly, you can specify a URL from which to fetch the message data.
65
* <platinum-push-messaging
66
* message-url="notification-data.json">
67
* </platinum-push-messaging>
70
* The second way is to send the message data in the body of
71
* the push message from your server. In this case you do not need to
72
* configure anything in your page:
74
* <platinum-push-messaging></platinum-push-messaging>
76
* **Note that this method is not currently supported by any browser**. It
77
* is, however, defined in the
78
* [draft W3C specification](http://w3c.github.io/push-api/#the-push-event)
79
* and this element should use that data when it is implemented in the
82
* If a message-url is provided then the message body will be ignored in
83
* favor of the first method.
85
* Thirdly, you can manually define the attributes on the element:
87
* <platinum-push-messaging
88
* title="Application updated"
89
* message="The application was updated in the background"
91
* click-url="notification.html">
92
* </platinum-push-messaging>
94
* These values will also be used as defaults if one of the other methods
95
* does not provide a value for that property.
98
* If you have set up Google Cloud Messaging then you can send push messages
99
* to your browser by following the guide in the [GCM documentation][2].
101
* However, for quick client testing there are two options. You can use the
102
* `testPush` method, which allows you to simulate a push message that
103
* includes a payload.
105
* Or, at a lower level, you can open up chrome://serviceworker-internals in
106
* Chrome and use the 'Push' button for the service worker corresponding to
109
* [1]: http://updates.html5rocks.com/2015/03/push-notificatons-on-the-open-web
110
* [2]: https://developer.android.com/google/gcm/http.html
111
* [3]: https://github.com/notwaldorf/caturday-post
116
is: 'platinum-push-messaging',
121
* Indicates whether the Push and Notification APIs are supported by
127
value: function() { return SUPPORTED; }
131
* The details of the current push subscription, if any.
135
type: PushSubscription,
140
* Indicates the status of the element. If true, push messages will be
151
* The location of the service worker script required by the element.
152
* The script is distributed alongside the main HTML import file for the
153
* element, so the location can normally be determined automatically.
154
* However, if you vulcanize your project you will need to include the
155
* script in your built project manually and use this property to let
156
* the element know how to load it.
161
return new URL('./service-worker.js', currentScript).href;
166
* A URL from which message information can be retrieved.
168
* When a push event happens that does not contain a message body this
169
* URL will be fetched. The document will be parsed as JSON, and should
170
* result in an object.
172
* The valid keys for the object are `title`, `message`, `url`, `icon`,
173
* `tag`, `dir`, `lang`, `noscreen`, `renotify`, `silent`, `sound`,
174
* `sticky` and `vibrate`. For documentation of these values see the
175
* attributes of the same names, except that these values override the
176
* element attributes.
181
* The default notification title.
186
* The default notification message.
191
* A default tag for the notifications that will be generated by
192
* this element. Notifications with the same tag will overwrite one
193
* another, so that only one will be shown at once.
198
* The URL of a default icon for notifications.
203
* The default text direction for the title and body of the
204
* notification. Can be `auto`, `ltr` or `rtl`.
212
* The default language to assume for the title and body of the
213
* notification. If set this must be a valid
214
* [BCP 47](https://tools.ietf.org/html/bcp47) language tag.
219
* If true then displaying the notification should not turn the device's
228
* When a notification is displayed that has the same `tag` as an
229
* existing notification, the existing one will be replaced. If this
230
* flag is true then such a replacement will cause the user to be
231
* alerted as though it were a new notification, by vibration or sound
240
* If true then displaying the notification should not cause any
241
* vibration or sound to be played.
249
* If true then the notification should be sticky, meaning that it is
250
* not directly dismissable.
258
* The pattern of vibration that should be used by default when a
259
* notification is displayed. See
264
* The URL of a default sound file to play when a notification is shown.
269
* The default URL to display when a notification is clicked.
273
value: document.location.href
278
* Fired when a notification is clicked that had the current page as the
281
* @event platinum-push-messaging-click
282
* @param {Object} The push message data used to create the notification
286
* Fired when a push message is received but no notification is shown.
287
* This happens when the click URL is for this page and the page is
288
* visible to the user on the screen.
290
* @event platinum-push-messaging-push
291
* @param {Object} The push message data that was received
295
* Fired when an error occurs while enabling or disabling notifications
297
* @event platinum-push-messaging-error
298
* @param {String} The error message
302
* Returns a promise which will resolve to the registration object
303
* associated with our current service worker.
305
* @return {Promise<ServiceWorkerRegistration>}
307
_getRegistration: function() {
308
return navigator.serviceWorker.getRegistration(SCOPE).then(function(registration) {
309
// If we have a service worker whose scope is a superset of the push
310
// scope then getRegistration will return that if our own worker is
311
// not registered. Check that the registration we have is really ours
312
if (registration && registration.scope === SCOPE) {
321
* Returns a promise that will resolve when the given registration becomes
324
* @param registration {ServiceWorkerRegistration}
325
* @return {Promise<undefined>}
327
_registrationReady: function(registration) {
328
if (registration.active) {
329
return Promise.resolve();
332
var serviceWorker = registration.installing || registration.waiting;
334
return new Promise(function(resolve, reject) {
335
// Because the Promise function is called on next tick there is a
336
// small chance that the worker became active already.
337
if (serviceWorker.state === 'activated') {
340
var listener = function(event) {
341
if (serviceWorker.state === 'activated') {
343
} else if (serviceWorker.state === 'redundant') {
344
reject(new Error('Worker became redundant'));
348
serviceWorker.removeEventListener('statechange', listener);
350
serviceWorker.addEventListener('statechange', listener);
355
* Event handler for the `message` event.
357
* @param event {MessageEvent}
359
_messageHandler: function(event) {
360
if (event.data && event.data.source === SCOPE) {
361
switch(event.data.type) {
363
this.fire('platinum-push-messaging-push', event.data);
366
this.fire('platinum-push-messaging-click', event.data);
373
* Takes an options object and creates a stable JSON serialization of it.
374
* This naive algorithm will only work if the object contains only
375
* non-nested properties.
377
* @param options {Object.<String, ?(String|Number|Boolean)>}
380
_serializeOptions: function(options) {
381
var props = Object.keys(options);
383
var parts = props.filter(function(propName) {
384
return !!options[propName];
385
}).map(function(propName) {
386
return JSON.stringify(propName) + ':' + JSON.stringify(options[propName]);
388
return encodeURIComponent('{' + parts.join(',') + '}');
392
* Determine the URL of the worker based on the currently set parameters
394
* @return String the URL
396
_getWorkerURL: function() {
397
var options = this._serializeOptions({
399
messageUrl: this.messageUrl,
401
message: this.message,
402
iconUrl: this.iconUrl,
403
clickUrl: this.clickUrl,
406
noscreen: this.noscreen,
407
renotify: this.renotify,
411
vibrate: this.vibrate,
416
return this.workerUrl + '?' + options;
420
* Update the subscription property, but only if the value has changed.
421
* This prevents triggering the subscription-changed event twice on page
424
_updateSubscription: function(subscription) {
425
if (JSON.stringify(subscription) !== JSON.stringify(this.subscription)) {
426
this._setSubscription(subscription);
431
* Programmatically trigger a push message
433
* @param message {Object} the message payload
435
testPush: function(message) {
436
this._getRegistration().then(function(registration) {
437
registration.active.postMessage({
445
* Request push messaging to be enabled.
447
* @return {Promise<undefined>}
450
if (!this.supported) {
451
this.fire('platinum-push-messaging-error', 'Your browser does not support push notifications');
452
return Promise.resolve();
455
return navigator.serviceWorker.register(this._getWorkerURL(), {scope: SCOPE}).then(function(registration) {
456
return this._registrationReady(registration).then(function() {
457
return registration.pushManager.subscribe({userVisibleOnly: true});
459
}.bind(this)).then(function(subscription) {
460
this._updateSubscription(subscription);
461
this._setEnabled(true);
462
}.bind(this)).catch(function(error) {
463
this.fire('platinum-push-messaging-error', error.message || error);
468
* Request push messaging to be disabled.
470
* @return {Promise<undefined>}
472
disable: function() {
473
if (!this.supported) {
474
return Promise.resolve();
477
return this._getRegistration().then(function(registration) {
481
return registration.pushManager.getSubscription().then(function(subscription) {
483
return subscription.unsubscribe();
486
return registration.unregister();
488
this._updateSubscription();
489
this._setEnabled(false);
490
}.bind(this)).catch(function(error) {
491
this.fire('platinum-push-messaging-error', error.message || error);
497
if (this.supported) {
498
var handler = this._messageHandler.bind(this);
499
// NOTE: We add the event listener twice because the specced and
500
// implemented behaviors do not match. In Chrome 42, messages are
501
// received on window. In the current spec they are supposed to be
502
// received on navigator.serviceWorker.
503
// TODO: Remove the non-spec code in the future.
504
window.addEventListener('message', handler);
505
navigator.serviceWorker.addEventListener('message', handler);
507
this._getRegistration().then(function(registration) {
511
if (registration.active && registration.active.scriptURL !== this._getWorkerURL()) {
512
// We have an existing worker in this scope, but it is out of date
513
return this.enable();
515
return registration.pushManager.getSubscription().then(function(subscription) {
516
this._updateSubscription(subscription);
517
this._setEnabled(true);