1/* 2 * Adds a browser push subscription when a visitor presses Subscribe in the 3 * listing footer (city) or the Alerts form (listing). The VAPID public key is 4 * read from this script tag's data-vapid-key; an empty key disables the feature. 5 * 6 * MultiSiteWebPush.subscribe() must be called directly from the submit handler: 7 * the permission prompt is only allowed during the user's click. 8 */ 9(function (window, document) { 10 'use strict'; 11 12 var script = document.currentScript; 13 var vapidKey = script ? (script.getAttribute('data-vapid-key') || '') : ''; 14 // Translation file code (en, eb, pb, de, ...) used for notification text. 15 var languageCode = script ? (script.getAttribute('data-lang') || '') : ''; 16 var enabledText = script ? (script.getAttribute('data-enabled-text') || '') : ''; 17 // Site notification icon (/img/<site>/...), handed to the service worker in its URL. 18 var icon = script ? (script.getAttribute('data-icon') || '') : ''; 19 var SW_URL = /^\/img\/[A-Za-z0-9_\-]+\/[A-Za-z0-9_.\-]+$/.test(icon) 20 ? '/push-sw.js?icon=' + encodeURIComponent(icon) 21 : '/push-sw.js'; 22 var SW_SCOPE = '/push-notifications/'; 23 24 function isSupported() { 25 return vapidKey !== '' 26 && window.isSecureContext 27 && 'serviceWorker' in navigator 28 && 'PushManager' in window 29 && 'Notification' in window; 30 } 31 32 function keyToBytes(base64Url) { 33 var padded = (base64Url + '===='.slice((base64Url.length % 4) || 4)).replace(/-/g, '+').replace(/_/g, '/'); 34 var raw = window.atob(padded); 35 var bytes = new Uint8Array(raw.length); 36 for (var i = 0; i < raw.length; i++) { 37 bytes[i] = raw.charCodeAt(i); 38 } 39 return bytes; 40 } 41 42 function requestPermission() { 43 if (Notification.permission !== 'default') { 44 return Promise.resolve(Notification.permission); 45 } 46 return new Promise(function (resolve) { 47 // Older Safari only supports the callback form. 48 var result = Notification.requestPermission(resolve); 49 if (result && typeof result.then === 'function') { 50 result.then(resolve); 51 } 52 }); 53 } 54 55 function activeRegistration() { 56 return navigator.serviceWorker.register(SW_URL, { scope: SW_SCOPE }).then(function (registration) { 57 if (registration.active) { 58 return registration; 59 } 60 var worker = registration.installing || registration.waiting; 61 return new Promise(function (resolve, reject) { 62 if (!worker) { 63 reject(new Error('Push service worker did not install')); 64 return; 65 } 66 worker.addEventListener('statechange', function () { 67 if (worker.state === 'activated') { 68 resolve(registration); 69 } else if (worker.state === 'redundant') { 70 reject(new Error('Push service worker became redundant')); 71 } 72 }); 73 }); 74 }); 75 } 76 77 function pushSubscription(registration) { 78 var options = { userVisibleOnly: true, applicationServerKey: keyToBytes(vapidKey) }; 79 return registration.pushManager.getSubscription().then(function (existing) { 80 if (!existing) { 81 return registration.pushManager.subscribe(options); 82 } 83 return registration.pushManager.subscribe(options).catch(function (error) { 84 // Only a different (rotated) VAPID key justifies replacing a working subscription. 85 if (!error || error.name !== 'InvalidStateError') { 86 throw error; 87 } 88 return existing.unsubscribe().then(function () { 89 return registration.pushManager.subscribe(options); 90 }); 91 }); 92 }); 93 } 94 95 function save(subscription, topic) { 96 return fetch('/push/subscribe', { 97 method: 'POST', 98 credentials: 'same-origin', 99 headers: { 'Content-Type': 'application/json' }, 100 body: JSON.stringify({ 101 subscription: subscription.toJSON(), 102 city: topic.city || null, 103 page: topic.page || null, 104 email: topic.email || null, 105 lang: languageCode || null 106 }) 107 }); 108 } 109 110 window.MultiSiteWebPush = { 111 isSupported: isSupported, 112 113 // Localized "notifications are on" confirmation for the subscribe forms. 114 enabledText: enabledText, 115 116 /** 117 * @param {{city: ?number, page: ?number, email: ?string}} topic 118 * @returns {Promise<string>} subscribed | denied | unsupported | error 119 */ 120 subscribe: function (topic) { 121 topic = topic || {}; 122 if (!isSupported() || (!topic.city && !topic.page)) { 123 return Promise.resolve('unsupported'); 124 } 125 return requestPermission().then(function (permission) { 126 if (permission !== 'granted') { 127 return 'denied'; 128 } 129 return activeRegistration() 130 .then(pushSubscription) 131 .then(function (subscription) { 132 return save(subscription, topic); 133 }) 134 .then(function (response) { 135 return response.ok ? 'subscribed' : 'error'; 136 }); 137 }).catch(function (error) { 138 if (window.console) { 139 window.console.log('Push subscription failed', error); 140 } 141 return 'error'; 142 }); 143 } 144 }; 145})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.