OLD | NEW |
(Empty) | |
| 1 <!-- |
| 2 @license |
| 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.g
ithub.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/CONTRI
BUTORS.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/PATEN
TS.txt |
| 9 --> |
| 10 <link rel="import" href="../polymer/polymer.html"> |
| 11 |
| 12 <script> |
| 13 (function() { |
| 14 'use strict'; |
| 15 // TODO: Doesn't work for IE or Safari, and the usual |
| 16 // document.getElementsByTagName('script') workaround seems to be broken by |
| 17 // HTML imports. Not important for now as neither of those browsers support |
| 18 // service worker yet. |
| 19 var currentScript = document.currentScript.baseURI; |
| 20 |
| 21 var SCOPE = new URL('./$$platinum-push-messaging$$/', currentScript).href; |
| 22 var WORKER_URL = new URL('./service-worker.js', currentScript).href; |
| 23 |
| 24 var BASE_URL = new URL('./', document.location.href).href; |
| 25 |
| 26 var SUPPORTED = 'serviceWorker' in navigator && |
| 27 'PushManager' in window && |
| 28 'Notification' in window; |
| 29 |
| 30 /** |
| 31 * @const {Number} The desired version of the service worker to use. This is |
| 32 * not strictly tied to anything except that it should be changed whenever |
| 33 * a breaking change is made to the service worker code. |
| 34 */ |
| 35 var VERSION = 1; |
| 36 |
| 37 // This allows us to use the PushSubscription attribute type in browsers |
| 38 // where it is not defined. |
| 39 if (!('PushSubscription' in window)) { |
| 40 window.PushSubscription = {}; |
| 41 } |
| 42 |
| 43 /** |
| 44 * `<platinum-push-messaging>` sets up a [push messaging][1] subscription |
| 45 * and allows you to define what happens when a push message is received. |
| 46 * |
| 47 * The element can be placed anywhere, but should only be used once in a |
| 48 * page. If there are multiple occurrences, only one will be active. |
| 49 * |
| 50 * # Requirements |
| 51 * Push messaging is currently only available in Google Chrome, which |
| 52 * requires you to configure Google Cloud Messaging. Chrome will check that |
| 53 * your page links to a manifest file that contains a `gcm_sender_id` field. |
| 54 * You can find full details of how to set all of this up in the [HTML5 |
| 55 * Rocks guide to push notifications][1]. |
| 56 * |
| 57 * # Notifcation details |
| 58 * The data for how a notification should be displayed can come from one of |
| 59 * three places. |
| 60 * |
| 61 * Firstly, you can specify a URL from which to fetch the message data. |
| 62 * ``` |
| 63 * <platinum-push-messaging |
| 64 * message-url="notification-data.json"> |
| 65 * </platinum-push-messaging> |
| 66 * ``` |
| 67 * |
| 68 * The second way is to send the message data in the body of |
| 69 * the push message from your server. In this case you do not need to |
| 70 * configure anything in your page: |
| 71 * ``` |
| 72 * <platinum-push-messaging></platinum-push-messaging> |
| 73 * ``` |
| 74 * **Note that this method is not currently supported by any browser**. It |
| 75 * is, however, defined in the |
| 76 * [draft W3C specification](http://w3c.github.io/push-api/#the-push-event) |
| 77 * and this element should use that data when it is implemented in the |
| 78 * future. |
| 79 * |
| 80 * If a message-url is provided then the message body will be ignored in |
| 81 * favor of the first method. |
| 82 * |
| 83 * Thirdly, you can manually define the attributes on the element: |
| 84 * ``` |
| 85 * <platinum-push-messaging |
| 86 * title="Application updated" |
| 87 * message="The application was updated in the background" |
| 88 * icon-url="icon.png" |
| 89 * click-url="notification.html"> |
| 90 * </platinum-push-messaging> |
| 91 * ``` |
| 92 * These values will also be used as defaults if one of the other methods |
| 93 * does not provide a value for that property. |
| 94 * |
| 95 * # Testing |
| 96 * If you have set up Google Cloud Messaging then you can send push messages |
| 97 * to your browser by following the guide in the [GCM documentation][2]. |
| 98 * |
| 99 * However, for quick client testing there are two options. You can use the |
| 100 * `testPush` method, which allows you to simulate a push message that |
| 101 * includes a payload. |
| 102 * |
| 103 * Or, at a lower level, you can open up chrome://serviceworker-internals in |
| 104 * Chrome and use the 'Push' button for the service worker corresponding to |
| 105 * your app. |
| 106 * |
| 107 * [1]: http://updates.html5rocks.com/2015/03/push-notificatons-on-the-open-
web |
| 108 * [2]: https://developer.android.com/google/gcm/http.html |
| 109 * |
| 110 * @demo demo/ |
| 111 */ |
| 112 Polymer({ |
| 113 is: 'platinum-push-messaging', |
| 114 |
| 115 properties: { |
| 116 |
| 117 /** |
| 118 * Indicates whether the Push and Notification APIs are supported by |
| 119 * this browser. |
| 120 */ |
| 121 supported: { |
| 122 readOnly: true, |
| 123 type: Boolean, |
| 124 value: SUPPORTED |
| 125 }, |
| 126 |
| 127 /** |
| 128 * The details of the current push subscription, if any. |
| 129 */ |
| 130 subscription: { |
| 131 readOnly: true, |
| 132 type: PushSubscription, |
| 133 notify: true, |
| 134 }, |
| 135 |
| 136 /** |
| 137 * Indicates the status of the element. If true, push messages will be |
| 138 * received. |
| 139 */ |
| 140 enabled: { |
| 141 readOnly: true, |
| 142 type: Boolean, |
| 143 notify: true, |
| 144 value: false |
| 145 }, |
| 146 |
| 147 |
| 148 /** |
| 149 * A URL from which message information can be retrieved. |
| 150 * |
| 151 * When a push event happens that does not contain a message body this |
| 152 * URL will be fetched. The URL is expected to be for a JSON document in |
| 153 * the format: |
| 154 * ``` |
| 155 * { |
| 156 * "title": "The title for the notification", |
| 157 * "body": "The message to display in the notification", |
| 158 * "url": "The URL to display when the notification is clicked", |
| 159 * "icon": "The URL of an icon to display with the notification", |
| 160 * "tag": "An identifier that determines which notifications can be di
splayed at the same time" |
| 161 * } |
| 162 * ``` |
| 163 */ |
| 164 messageUrl: String, |
| 165 |
| 166 /** |
| 167 * A default notification title. |
| 168 */ |
| 169 title: String, |
| 170 |
| 171 /** |
| 172 * A default notification message. |
| 173 */ |
| 174 message: String, |
| 175 |
| 176 /** |
| 177 * A default icon for notifications. |
| 178 */ |
| 179 iconUrl: String, |
| 180 |
| 181 /** |
| 182 * A default URL to display when a notification is clicked. |
| 183 */ |
| 184 clickUrl: { |
| 185 type: String, |
| 186 value: document.location.href |
| 187 }, |
| 188 |
| 189 /** |
| 190 * A default tag for the notifications that will be generated by |
| 191 * this element. Notifications with the same tag will overwrite one |
| 192 * another, so that only one will be shown at once. |
| 193 */ |
| 194 tag: String |
| 195 }, |
| 196 |
| 197 /** |
| 198 * Fired when a notification is clicked that had the current page as the |
| 199 * click URL. |
| 200 * |
| 201 * @event platinum-push-messaging-click |
| 202 * @param {Object} The push message data used to create the notification |
| 203 */ |
| 204 |
| 205 /** |
| 206 * Fired when a push message is received but no notification is shown. |
| 207 * This happens when the click URL is for this page and the page is |
| 208 * visible to the user on the screen. |
| 209 * |
| 210 * @event platinum-push-messaging-push |
| 211 * @param {Object} The push message data that was received |
| 212 */ |
| 213 |
| 214 /** |
| 215 * Fired when an error occurs while enabling or disabling notifications |
| 216 * |
| 217 * @event platinum-push-messaging-error |
| 218 * @param {String} The error message |
| 219 */ |
| 220 |
| 221 /** |
| 222 * Returns a promise which will resolve to the registration object |
| 223 * associated with our current service worker. |
| 224 * |
| 225 * @return {Promise<ServiceWorkerRegistration>} |
| 226 */ |
| 227 _getRegistration: function() { |
| 228 return navigator.serviceWorker.getRegistration(SCOPE); |
| 229 }, |
| 230 |
| 231 /** |
| 232 * Returns a promise that will resolve when the given registration becomes |
| 233 * active. |
| 234 * |
| 235 * @param registration {ServiceWorkerRegistration} |
| 236 * @return {Promise<undefined>} |
| 237 */ |
| 238 _registrationReady: function(registration) { |
| 239 if (registration.active) { |
| 240 return Promise.resolve(); |
| 241 } |
| 242 |
| 243 var serviceWorker = registration.installing || registration.waiting; |
| 244 |
| 245 return new Promise(function(resolve, reject) { |
| 246 // Because the Promise function is called on next tick there is a |
| 247 // small chance that the worker became active already. |
| 248 if (serviceWorker.state === 'activated') { |
| 249 resolve(); |
| 250 } |
| 251 var listener = function(event) { |
| 252 if (serviceWorker.state === 'activated') { |
| 253 resolve(); |
| 254 } else if (serviceWorker.state === 'redundant') { |
| 255 reject(new Error('Worker became redundant')); |
| 256 } else { |
| 257 return; |
| 258 } |
| 259 serviceWorker.removeEventListener('statechange', listener); |
| 260 }; |
| 261 serviceWorker.addEventListener('statechange', listener); |
| 262 }); |
| 263 }, |
| 264 |
| 265 /** |
| 266 * Event handler for the `message` event. |
| 267 * |
| 268 * @param event {MessageEvent} |
| 269 */ |
| 270 _messageHandler: function(event) { |
| 271 if (event.data && event.data.source === SCOPE) { |
| 272 switch(event.data.type) { |
| 273 case 'push': |
| 274 this.fire('platinum-push-messaging-push', event.data); |
| 275 break; |
| 276 case 'click': |
| 277 this.fire('platinum-push-messaging-click', event.data); |
| 278 break; |
| 279 } |
| 280 } |
| 281 }, |
| 282 |
| 283 /** |
| 284 * Takes an options object and creates a stable JSON serialization of it. |
| 285 * This naive algorithm will only work if the object contains only |
| 286 * non-nested properties. |
| 287 * |
| 288 * @param options {Object.<String, ?(String|Number|Boolean)>} |
| 289 * @return String |
| 290 */ |
| 291 _serializeOptions: function(options) { |
| 292 var props = Object.keys(options); |
| 293 props.sort(); |
| 294 var parts = props.filter(function(propName) { |
| 295 return !!options[propName]; |
| 296 }).map(function(propName) { |
| 297 return JSON.stringify(propName) + ':' + JSON.stringify(options[propNam
e]); |
| 298 }); |
| 299 return '{' + parts.join(',') + '}'; |
| 300 }, |
| 301 |
| 302 /** |
| 303 * Determine the URL of the worker based on the currently set parameters |
| 304 * |
| 305 * @return String the URL |
| 306 */ |
| 307 _getWorkerURL: function() { |
| 308 var options = this._serializeOptions({ |
| 309 tag: this.tag, |
| 310 messageUrl: this.messageUrl, |
| 311 title: this.title, |
| 312 message: this.message, |
| 313 iconUrl: this.iconUrl, |
| 314 clickUrl: this.clickUrl, |
| 315 version: VERSION, |
| 316 baseUrl: BASE_URL |
| 317 }); |
| 318 |
| 319 return WORKER_URL + '?' + options; |
| 320 }, |
| 321 |
| 322 /** |
| 323 * Update the subscription property, but only if the value has changed. |
| 324 * This prevents triggering the subscription-changed event twice on page |
| 325 * load. |
| 326 */ |
| 327 _updateSubscription: function(subscription) { |
| 328 if (JSON.stringify(subscription) !== JSON.stringify(this.subscription))
{ |
| 329 this._setSubscription(subscription); |
| 330 } |
| 331 }, |
| 332 |
| 333 /** |
| 334 * Programmatically trigger a push message |
| 335 * |
| 336 * @param message {Object} the message payload |
| 337 */ |
| 338 testPush: function(message) { |
| 339 this._getRegistration().then(function(registration) { |
| 340 registration.active.postMessage({ |
| 341 type: 'test-push', |
| 342 message: message |
| 343 }); |
| 344 }); |
| 345 }, |
| 346 |
| 347 /** |
| 348 * Request push messaging to be enabled. |
| 349 * |
| 350 * @return {Promise<undefined>} |
| 351 */ |
| 352 enable: function() { |
| 353 if (!this.supported) { |
| 354 this.fire('platinum-push-messaging-error', 'Your browser does not supp
ort push notifications'); |
| 355 return Promise.resolve(); |
| 356 } |
| 357 |
| 358 return navigator.serviceWorker.register(this._getWorkerURL(), {scope: SC
OPE}).then(function(registration) { |
| 359 return this._registrationReady(registration).then(function() { |
| 360 return registration.pushManager.subscribe({userVisibleOnly: true}); |
| 361 }); |
| 362 }.bind(this)).then(function(subscription) { |
| 363 this._updateSubscription(subscription); |
| 364 this._setEnabled(true); |
| 365 }.bind(this)).catch(function(error) { |
| 366 this.fire('platinum-push-messaging-error', error.message || error); |
| 367 }.bind(this)); |
| 368 }, |
| 369 |
| 370 /** |
| 371 * Request push messaging to be disabled. |
| 372 * |
| 373 * @return {Promise<undefined>} |
| 374 */ |
| 375 disable: function() { |
| 376 if (!this.supported) { |
| 377 return Promise.resolve(); |
| 378 } |
| 379 |
| 380 return this._getRegistration().then(function(registration) { |
| 381 if (!registration) { |
| 382 return; |
| 383 } |
| 384 return registration.pushManager.getSubscription().then(function(subscr
iption) { |
| 385 if (subscription) { |
| 386 return subscription.unsubscribe(); |
| 387 } |
| 388 }).then(function() { |
| 389 return registration.unregister(); |
| 390 }).then(function() { |
| 391 this._updateSubscription(); |
| 392 this._setEnabled(false); |
| 393 }.bind(this)).catch(function(error) { |
| 394 this.fire('platinum-push-messaging-error', error.message || error); |
| 395 }.bind(this)); |
| 396 }.bind(this)); |
| 397 }, |
| 398 |
| 399 ready: function() { |
| 400 if (this.supported) { |
| 401 var handler = this._messageHandler.bind(this); |
| 402 // NOTE: We add the event listener twice because the specced and |
| 403 // implemented behaviors do not match. In Chrome 42, messages are |
| 404 // received on window. In the current spec they are supposed to be |
| 405 // received on navigator.serviceWorker. |
| 406 // TODO: Remove the non-spec code in the future. |
| 407 window.addEventListener('message', handler); |
| 408 navigator.serviceWorker.addEventListener('message', handler); |
| 409 |
| 410 this._getRegistration().then(function(registration) { |
| 411 if (!registration) { |
| 412 return; |
| 413 } |
| 414 if (registration.active && registration.active.scriptURL !== this._g
etWorkerURL()) { |
| 415 // We have an existing worker in this scope, but it is out of date |
| 416 return this.enable(); |
| 417 } |
| 418 return registration.pushManager.getSubscription().then(function(subs
cription) { |
| 419 this._updateSubscription(subscription); |
| 420 this._setEnabled(true); |
| 421 }.bind(this)); |
| 422 }.bind(this)); |
| 423 } |
| 424 } |
| 425 }); |
| 426 })(); |
| 427 </script> |
OLD | NEW |