This repository has been archived by the owner on Mar 13, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathplatinum-push-messaging.html
538 lines (494 loc) · 18.1 KB
/
platinum-push-messaging.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
<!--
@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/index.html
*/
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,
/**
* If true then the fetch "credentials" of the request to "messageUrl" will
* be set to "include", allowing the browser to request the URL as the
* current user.
*
* This is helpful when the server will reject a message from an
* unauthenticated user.
*/
useCredentials: {
type: Boolean,
value: false
},
/**
* 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,
useCredentials: this.useCredentials,
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>