UNPKG

npm-polymer-elements

Version:

Polymer Elements package for npm

525 lines (481 loc) 18 kB
<!-- @license Copyright (c) 2015 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt --> <link rel="import" href="../polymer/polymer.html"> <link rel="import" href="../promise-polyfill/promise-polyfill.html"> <script> (function() { 'use strict'; // TODO: Doesn't work for IE or Safari, and the usual // document.getElementsByTagName('script') workaround seems to be broken by // HTML imports. Not important for now as neither of those browsers support // service worker yet. var currentScript = (document.currentScript || {}).baseURI; var SCOPE = new URL('./$$platinum-push-messaging$$/', currentScript).href; var BASE_URL = new URL('./', document.location.href).href; var SUPPORTED = 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window; /** * @const {Number} The desired version of the service worker to use. This is * not strictly tied to anything except that it should be changed whenever * a breaking change is made to the service worker code. */ var VERSION = 1; // This allows us to use the PushSubscription attribute type in browsers // where it is not defined. if (!('PushSubscription' in window)) { window.PushSubscription = {}; } /** * `<platinum-push-messaging>` sets up a [push messaging][1] subscription * and allows you to define what happens when a push message is received. * * The element can be placed anywhere, but should only be used once in a * page. If there are multiple occurrences, only one will be active. * * # Sample * * For a complete sample that uses the element, see the [Cat Push * Notifications][3] project. * * # Requirements * Push messaging is currently only available in Google Chrome, which * requires you to configure Google Cloud Messaging. Chrome will check that * your page links to a manifest file that contains a `gcm_sender_id` field. * You can find full details of how to set all of this up in the [HTML5 * Rocks guide to push notifications][1]. * * # Notification details * The data for how a notification should be displayed can come from one of * three places. * * Firstly, you can specify a URL from which to fetch the message data. * ``` * <platinum-push-messaging * message-url="notification-data.json"> * </platinum-push-messaging> * ``` * * The second way is to send the message data in the body of * the push message from your server. In this case you do not need to * configure anything in your page: * ``` * <platinum-push-messaging></platinum-push-messaging> * ``` * **Note that this method is not currently supported by any browser**. It * is, however, defined in the * [draft W3C specification](http://w3c.github.io/push-api/#the-push-event) * and this element should use that data when it is implemented in the * future. * * If a message-url is provided then the message body will be ignored in * favor of the first method. * * Thirdly, you can manually define the attributes on the element: * ``` * <platinum-push-messaging * title="Application updated" * message="The application was updated in the background" * icon-url="icon.png" * click-url="notification.html"> * </platinum-push-messaging> * ``` * These values will also be used as defaults if one of the other methods * does not provide a value for that property. * * # Testing * If you have set up Google Cloud Messaging then you can send push messages * to your browser by following the guide in the [GCM documentation][2]. * * However, for quick client testing there are two options. You can use the * `testPush` method, which allows you to simulate a push message that * includes a payload. * * Or, at a lower level, you can open up chrome://serviceworker-internals in * Chrome and use the 'Push' button for the service worker corresponding to * your app. * * [1]: http://updates.html5rocks.com/2015/03/push-notificatons-on-the-open-web * [2]: https://developer.android.com/google/gcm/http.html * [3]: https://github.com/notwaldorf/caturday-post * * @demo demo/ */ Polymer({ is: 'platinum-push-messaging', properties: { /** * Indicates whether the Push and Notification APIs are supported by * this browser. */ supported: { readOnly: true, type: Boolean, value: function() { return SUPPORTED; } }, /** * The details of the current push subscription, if any. */ subscription: { readOnly: true, type: PushSubscription, notify: true, }, /** * Indicates the status of the element. If true, push messages will be * received. */ enabled: { readOnly: true, type: Boolean, notify: true, value: false }, /** * The location of the service worker script required by the element. * The script is distributed alongside the main HTML import file for the * element, so the location can normally be determined automatically. * However, if you vulcanize your project you will need to include the * script in your built project manually and use this property to let * the element know how to load it. */ workerUrl: { type: String, value: function() { return new URL('./service-worker.js', currentScript).href; } }, /** * A URL from which message information can be retrieved. * * When a push event happens that does not contain a message body this * URL will be fetched. The document will be parsed as JSON, and should * result in an object. * * The valid keys for the object are `title`, `message`, `url`, `icon`, * `tag`, `dir`, `lang`, `noscreen`, `renotify`, `silent`, `sound`, * `sticky` and `vibrate`. For documentation of these values see the * attributes of the same names, except that these values override the * element attributes. */ messageUrl: String, /** * The default notification title. */ title: String, /** * The default notification message. */ message: String, /** * A default tag for the notifications that will be generated by * this element. Notifications with the same tag will overwrite one * another, so that only one will be shown at once. */ tag: String, /** * The URL of a default icon for notifications. */ iconUrl: String, /** * The default text direction for the title and body of the * notification. Can be `auto`, `ltr` or `rtl`. */ dir: { type: String, value: 'auto' }, /** * The default language to assume for the title and body of the * notification. If set this must be a valid * [BCP 47](https://tools.ietf.org/html/bcp47) language tag. */ lang: String, /** * If true then displaying the notification should not turn the device's * screen on. */ noscreen: { type: Boolean, value: false }, /** * When a notification is displayed that has the same `tag` as an * existing notification, the existing one will be replaced. If this * flag is true then such a replacement will cause the user to be * alerted as though it were a new notification, by vibration or sound * as appropriate. */ renotify: { type: Boolean, value: false }, /** * If true then displaying the notification should not cause any * vibration or sound to be played. */ silent: { type: Boolean, value: false }, /** * If true then the notification should be sticky, meaning that it is * not directly dismissable. */ sticky: { type: Boolean, value: false }, /** * The pattern of vibration that should be used by default when a * notification is displayed. See */ vibrate: Array, /** * The URL of a default sound file to play when a notification is shown. */ sound: String, /** * The default URL to display when a notification is clicked. */ clickUrl: { type: String, value: document.location.href }, }, /** * Fired when a notification is clicked that had the current page as the * click URL. * * @event platinum-push-messaging-click * @param {Object} The push message data used to create the notification */ /** * Fired when a push message is received but no notification is shown. * This happens when the click URL is for this page and the page is * visible to the user on the screen. * * @event platinum-push-messaging-push * @param {Object} The push message data that was received */ /** * Fired when an error occurs while enabling or disabling notifications * * @event platinum-push-messaging-error * @param {String} The error message */ /** * Returns a promise which will resolve to the registration object * associated with our current service worker. * * @return {Promise<ServiceWorkerRegistration>} */ _getRegistration: function() { return navigator.serviceWorker.getRegistration(SCOPE).then(function(registration) { // If we have a service worker whose scope is a superset of the push // scope then getRegistration will return that if our own worker is // not registered. Check that the registration we have is really ours if (registration && registration.scope === SCOPE) { return registration; } return; }); }, /** * Returns a promise that will resolve when the given registration becomes * active. * * @param registration {ServiceWorkerRegistration} * @return {Promise<undefined>} */ _registrationReady: function(registration) { if (registration.active) { return Promise.resolve(); } var serviceWorker = registration.installing || registration.waiting; return new Promise(function(resolve, reject) { // Because the Promise function is called on next tick there is a // small chance that the worker became active already. if (serviceWorker.state === 'activated') { resolve(); } var listener = function(event) { if (serviceWorker.state === 'activated') { resolve(); } else if (serviceWorker.state === 'redundant') { reject(new Error('Worker became redundant')); } else { return; } serviceWorker.removeEventListener('statechange', listener); }; serviceWorker.addEventListener('statechange', listener); }); }, /** * Event handler for the `message` event. * * @param event {MessageEvent} */ _messageHandler: function(event) { if (event.data && event.data.source === SCOPE) { switch(event.data.type) { case 'push': this.fire('platinum-push-messaging-push', event.data); break; case 'click': this.fire('platinum-push-messaging-click', event.data); break; } } }, /** * Takes an options object and creates a stable JSON serialization of it. * This naive algorithm will only work if the object contains only * non-nested properties. * * @param options {Object.<String, ?(String|Number|Boolean)>} * @return String */ _serializeOptions: function(options) { var props = Object.keys(options); props.sort(); var parts = props.filter(function(propName) { return !!options[propName]; }).map(function(propName) { return JSON.stringify(propName) + ':' + JSON.stringify(options[propName]); }); return encodeURIComponent('{' + parts.join(',') + '}'); }, /** * Determine the URL of the worker based on the currently set parameters * * @return String the URL */ _getWorkerURL: function() { var options = this._serializeOptions({ tag: this.tag, messageUrl: this.messageUrl, title: this.title, message: this.message, iconUrl: this.iconUrl, clickUrl: this.clickUrl, dir: this.dir, lang: this.lang, noscreen: this.noscreen, renotify: this.renotify, silent: this.silent, sound: this.sound, sticky: this.sticky, vibrate: this.vibrate, version: VERSION, baseUrl: BASE_URL }); return this.workerUrl + '?' + options; }, /** * Update the subscription property, but only if the value has changed. * This prevents triggering the subscription-changed event twice on page * load. */ _updateSubscription: function(subscription) { if (JSON.stringify(subscription) !== JSON.stringify(this.subscription)) { this._setSubscription(subscription); } }, /** * Programmatically trigger a push message * * @param message {Object} the message payload */ testPush: function(message) { this._getRegistration().then(function(registration) { registration.active.postMessage({ type: 'test-push', message: message }); }); }, /** * Request push messaging to be enabled. * * @return {Promise<undefined>} */ enable: function() { if (!this.supported) { this.fire('platinum-push-messaging-error', 'Your browser does not support push notifications'); return Promise.resolve(); } return navigator.serviceWorker.register(this._getWorkerURL(), {scope: SCOPE}).then(function(registration) { return this._registrationReady(registration).then(function() { return registration.pushManager.subscribe({userVisibleOnly: true}); }); }.bind(this)).then(function(subscription) { this._updateSubscription(subscription); this._setEnabled(true); }.bind(this)).catch(function(error) { this.fire('platinum-push-messaging-error', error.message || error); }.bind(this)); }, /** * Request push messaging to be disabled. * * @return {Promise<undefined>} */ disable: function() { if (!this.supported) { return Promise.resolve(); } return this._getRegistration().then(function(registration) { if (!registration) { return; } return registration.pushManager.getSubscription().then(function(subscription) { if (subscription) { return subscription.unsubscribe(); } }).then(function() { return registration.unregister(); }).then(function() { this._updateSubscription(); this._setEnabled(false); }.bind(this)).catch(function(error) { this.fire('platinum-push-messaging-error', error.message || error); }.bind(this)); }.bind(this)); }, ready: function() { if (this.supported) { var handler = this._messageHandler.bind(this); // NOTE: We add the event listener twice because the specced and // implemented behaviors do not match. In Chrome 42, messages are // received on window. In the current spec they are supposed to be // received on navigator.serviceWorker. // TODO: Remove the non-spec code in the future. window.addEventListener('message', handler); navigator.serviceWorker.addEventListener('message', handler); this._getRegistration().then(function(registration) { if (!registration) { return; } if (registration.active && registration.active.scriptURL !== this._getWorkerURL()) { // We have an existing worker in this scope, but it is out of date return this.enable(); } return registration.pushManager.getSubscription().then(function(subscription) { this._updateSubscription(subscription); this._setEnabled(true); }.bind(this)); }.bind(this)); } } }); })(); </script>