diff --git a/.gitignore b/.gitignore index 00cbbdf..dbe3e1a 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ typings/ # dotenv environment variables file .env +.vscode/ diff --git a/LICENSES.txt b/LICENSES.txt new file mode 100644 index 0000000..5388ac7 --- /dev/null +++ b/LICENSES.txt @@ -0,0 +1,3 @@ +Background Image +https://www.pexels.com/photo/air-atmosphere-blue-blue-sky-96622/ +CC0 License \ No newline at end of file diff --git a/README.md b/README.md index 70c03e9..6308d09 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ -# serviceworker-demo -Service Worker demos +# Service Worker App Demo + +This is a demo for service workers and web push notifications that work in modern web browsers. + +## How to use + +First, install all Node.js dependencies via `npm install`. + +This demo requires access to a mongodb instance for storing push subscription info to send push updates at some other point in time. It also requires specifying a public and private key for identifying your server to the push service's server. These keys, known as VAPID public/private keys, can be generated and printed to the console when first executing the site. The site can be executed by running `node index.js` which will start a server on port `2020`. You'll need to populate those keys as environment variables and execute `node index.js` again to ensure that push messages can be configured from your server. + +If you are using VS Code you can set the environment variables mentioned above in your `launch.json` file as follows: + +```js +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch demo", + "program": "${workspaceFolder}/serve.js", + "env": { + "DATABASE_CONNECTION_URI": "YOUR CONNECTION STRING", + "VAPID_PUBLIC_KEY": "YOUR PUBLIC KEY", + "VAPID_PRIVATE_KEY": "YOUR PRIVATE KEY", + } + }, + { + "type": "node", + "request": "launch", + "name": "Job", + "program": "${workspaceFolder}/notify.js", + "env": { + "DATABASE_CONNECTION_URI": "YOUR CONNECTION STRING", + "VAPID_PUBLIC_KEY": "YOUR PUBLIC KEY", + "VAPID_PRIVATE_KEY": "YOUR PRIVATE KEY", + } + } + ] +} +``` + +Alternatively, you can modify `configured-web-push.js` and `db.js` and set the values there explicitly: + +```js +const databaseConnectionURI = process.env.DATABASE_CONNECTION_URI || ''; +const vapidPublicKey = process.env.VAPID_PUBLIC_KEY || ''; +const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY || ''; +``` \ No newline at end of file diff --git a/configured-web-push.js b/configured-web-push.js new file mode 100644 index 0000000..9ed3e25 --- /dev/null +++ b/configured-web-push.js @@ -0,0 +1,21 @@ +const webPush = require('web-push'); + +const vapidPublicKey = process.env.VAPID_PUBLIC_KEY || ''; +const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY || ''; + +if (vapidPublicKey === '' || vapidPrivateKey === '') { + console.log("You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + + "environment variables. You can use the following ones:"); + console.log(webPush.generateVAPIDKeys()); +} else { + webPush.setVapidDetails( +    'mailto:email@outlook.com', +   vapidPublicKey, +    vapidPrivateKey + ); +} + +module.exports = { + webPush: webPush, + vapidPublicKey: vapidPublicKey +} diff --git a/db.js b/db.js new file mode 100644 index 0000000..60453f9 --- /dev/null +++ b/db.js @@ -0,0 +1,35 @@ +const databaseConnectionURI = process.env.DATABASE_CONNECTION_URI || ''; + +if (databaseConnectionURI === '') { + console.error('Database connection URI not defined.'); +} + +const mongoose = require('mongoose'); +mongoose.Promise = global.Promise; + +const { Schema } = mongoose; + +const SubscriptionSchema = new Schema({ + endpoint: { type: String, index: true }, + keys: { + auth: { type: String }, + p256dh: { type: String } + }, + created: {type: Date, default: Date.now } +}, { collection: 'serviceworkerapp' }); + +const Subscription = mongoose.model('Subscription', SubscriptionSchema); + +const connect = async function() { + try { + await mongoose.connect(databaseConnectionURI); + } catch (e) { + console.error('Connection the database failed.'); + } +} + +connect(); + +module.exports = { + Subscription +}; diff --git a/notify.js b/notify.js new file mode 100644 index 0000000..a7b7a9b --- /dev/null +++ b/notify.js @@ -0,0 +1,44 @@ +const { Subscription } = require('./db'); +const configuredWebPush = require('./configured-web-push'); + +const init = async function() { + let pushMessage = "Extreme weather! Come check it out!"; + + try { + const cursor = Subscription.find().cursor(); + await cursor.eachAsync(function(sub) { + return configuredWebPush.webPush.sendNotification({ + endpoint: sub.endpoint, + keys: { + auth: sub.keys.auth, + p256dh: sub.keys.p256dh + } + }, pushMessage, {contentEncoding: 'aes128gcm'}) + .then(function(push) { + console.log(push); + }) + .catch(function(e) { + // 404 for FCM AES128GCM + if (e.statusCode === 410 || e.statusCode === 404) { + // delete invalid registration + return Subscription.remove({endpoint: sub.endpoint}).exec() + .then(function(sub) { + console.log('Deleted: ' + sub.endpoint); + }) + .catch(function(sub) { + console.error('Failed to delete: ' + sub.endpoint); + }); + } + }); + }); + } catch (e) { + console.log(e); + } + + console.log('Job executed correctly'); +}; + +init().catch(function(err) { + console.error(err); + process.exit(1); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..d014fa9 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "serviceworker-demo", + "version": "1.0.0", + "description": "Service Worker demos", + "main": "serve.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/MicrosoftEdge/serviceworker-demo.git" + }, + "author": "aliams", + "license": "ISC", + "bugs": { + "url": "https://github.com/MicrosoftEdge/serviceworker-demo/issues" + }, + "homepage": "https://github.com/MicrosoftEdge/serviceworker-demo#readme", + "dependencies": { + "express": "^4.16.3", + "mongoose": "^5.0.17", + "web-push": "^3.3.0" + } +} diff --git a/public-common/bg.jpg b/public-common/bg.jpg new file mode 100644 index 0000000..e48d36f Binary files /dev/null and b/public-common/bg.jpg differ diff --git a/public-common/common.js b/public-common/common.js new file mode 100644 index 0000000..e0a4fe8 --- /dev/null +++ b/public-common/common.js @@ -0,0 +1,44 @@ +function updatePage(details) { + document.querySelector('.current').innerText = details.temp; + document.querySelector('.weather-info .description').innerText = details.description; + + document.querySelector('.weather-info .details').innerHTML = ''; + for (var i = 0; i < details.details.length; i++) { + var li = document.createElement('li'); + var span = document.createElement('span'); + span.innerText = details.details[i].label; + var text = document.createTextNode(' ' + details.details[i].value); + li.appendChild(span); + li.appendChild(text); + document.querySelector('.weather-info .details').appendChild(li); + } + + var time = new Date(details.time); + var hours = time.getHours(); + var ampm = hours >= 12 ? 'PM' : 'AM'; + var minutes = time.getMinutes(); + hours = hours % 12; + hours = hours ? hours : 12; + minutes = (minutes < 10 ? '0' : '') + minutes; + var lastUpdated = hours + ':' + minutes + ' ' + ampm; + + document.querySelector('.weather-info .last-updated .time').innerText = lastUpdated; + + document.querySelector('.wrapper').style.opacity = 1; +} + +function displayNetworkError() { + var wrapperDiv = document.createElement('div'); + wrapperDiv.className = 'wrapper'; + var infoDiv = document.createElement('div'); + infoDiv.className = 'weather-info'; + var textSpan = document.createElement('span'); + textSpan.className = 'last-updated'; + textSpan.style.textAlign = 'center'; + textSpan.style.paddingTop = '18rem'; + textSpan.innerText = 'Please connect to the internet and retry.'; + infoDiv.appendChild(textSpan); + wrapperDiv.appendChild(infoDiv); + document.querySelector('.wrapper').style.display = 'none'; + document.querySelector('.bg').appendChild(wrapperDiv); +} \ No newline at end of file diff --git a/public-common/fallback.html b/public-common/fallback.html new file mode 100644 index 0000000..99cd750 --- /dev/null +++ b/public-common/fallback.html @@ -0,0 +1,19 @@ + + + + Weather + + + + +
+
+
+ + Please connect to the internet and retry. + +
+
+
+ + \ No newline at end of file diff --git a/public-common/fonts/segoeui.ttf b/public-common/fonts/segoeui.ttf new file mode 100644 index 0000000..edf6731 Binary files /dev/null and b/public-common/fonts/segoeui.ttf differ diff --git a/public-common/fonts/segoeui.woff b/public-common/fonts/segoeui.woff new file mode 100644 index 0000000..cf15299 Binary files /dev/null and b/public-common/fonts/segoeui.woff differ diff --git a/public-common/fonts/segoeui.woff2 b/public-common/fonts/segoeui.woff2 new file mode 100644 index 0000000..3bab420 Binary files /dev/null and b/public-common/fonts/segoeui.woff2 differ diff --git a/public-common/fonts/segoeuilight.ttf b/public-common/fonts/segoeuilight.ttf new file mode 100644 index 0000000..08bb6dd Binary files /dev/null and b/public-common/fonts/segoeuilight.ttf differ diff --git a/public-common/fonts/segoeuilight.woff b/public-common/fonts/segoeuilight.woff new file mode 100644 index 0000000..e80798d Binary files /dev/null and b/public-common/fonts/segoeuilight.woff differ diff --git a/public-common/fonts/segoeuilight.woff2 b/public-common/fonts/segoeuilight.woff2 new file mode 100644 index 0000000..94c86be Binary files /dev/null and b/public-common/fonts/segoeuilight.woff2 differ diff --git a/public-common/fonts/segoeuisemibold.ttf b/public-common/fonts/segoeuisemibold.ttf new file mode 100644 index 0000000..0fcc0f7 Binary files /dev/null and b/public-common/fonts/segoeuisemibold.ttf differ diff --git a/public-common/fonts/segoeuisemibold.woff b/public-common/fonts/segoeuisemibold.woff new file mode 100644 index 0000000..9ee8e4c Binary files /dev/null and b/public-common/fonts/segoeuisemibold.woff differ diff --git a/public-common/fonts/segoeuisemibold.woff2 b/public-common/fonts/segoeuisemibold.woff2 new file mode 100644 index 0000000..6b6fab2 Binary files /dev/null and b/public-common/fonts/segoeuisemibold.woff2 differ diff --git a/public-common/index.html b/public-common/index.html new file mode 100644 index 0000000..9dfc31e --- /dev/null +++ b/public-common/index.html @@ -0,0 +1,28 @@ + + + + Weather + + + + +
+ +
+ + + + + + diff --git a/public-common/styles.css b/public-common/styles.css new file mode 100644 index 0000000..2affd8c --- /dev/null +++ b/public-common/styles.css @@ -0,0 +1,142 @@ +body { + background-color: #01579b; +} + +body.desaturate { + background-color: #4a4a4a; +} + +*, ::after, ::before { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +ul { + list-style: none; +} + +a.refresh { + text-decoration: none; + color: #fff; +} + +li a { + color: #fff; + text-decoration: none; +} + +li a:hover { + text-decoration: underline; +} + +.desaturate .bg { + filter: grayscale(100%); /* Current draft standard */ + -webkit-filter: grayscale(100%); /* New WebKit */ + -moz-filter: grayscale(100%); + -ms-filter: grayscale(100%); + -o-filter: grayscale(100%); /* Not yet supported in Gecko, Opera or IE */ + filter: url(resources.svg#desaturate); /* Gecko */ + filter: gray; /* IE */ + -webkit-filter: grayscale(1); /* Old WebKit */ +} + +.bg { + background: #01579b 0 0 no-repeat, url(bg.jpg); + background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(1, 87, 155, 1) 100%), url(bg.jpg); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255, 255, 255, 0)), color-stop(100%,rgba(1, 87, 155, 1))), url(bg.jpg); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%,rgba(1, 87, 155, 1) 100%), url(bg.jpg); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, rgba(255, 255, 255, 0) 0%,rgba(1, 87, 155, 1) 100%), url(bg.jpg); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, rgba(255, 255, 255, 0) 0%,rgba(1, 87, 155, 1) 100%), url(bg.jpg); /* IE10+ */ + background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%,rgba(1, 87, 155, 1) 100%), url(bg.jpg); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#01579b',GradientType=0 ); /* IE6-9 */ + height: 640px; +} + +.wrapper { + color: #fff; + width: 24rem; + margin: 0 auto; + padding-top: .5rem; +} + +.wrapper .current-info, +.wrapper .current, +.wrapper .type { + display: inline-block; +} + +.wrapper .current-info .current { + font-family: "Segoe UI Light", "Segoe UI", Arial, Sans-Serif; + font-size: 16rem; + line-height: 1; + margin-left: -1.2rem; +} + +.wrapper .current-info .type, +.wrapper .current-info .location { + font-family: "Segoe UI Light", "Segoe UI", Arial, Sans-Serif; + font-size: 6rem; + line-height: 1.167; + vertical-align: top; + margin-top: 1.5rem; +} + +.wrapper .current-info .location { + font-size: 3rem; + display: block; + margin-bottom: -1.4rem; +} + +.wrapper .weather-info>span { + font-family: "Segoe UI Light", "Segoe UI", Arial, Sans-Serif; + font-size: 3.2rem; + line-height: 1.167; + display: block; + margin-bottom: .5rem; +} + +.wrapper .weather-info .last-updated { + margin-bottom: 1.5rem; +} + +.wrapper .weather-info ul, +.wrapper .weather-info .last-updated { + font-family: "Segoe UI", "Segoe UI", Arial, Sans-Serif; + font-size: 1.3rem; + line-height: 1.384; + color: #fff; +} + +.wrapper .weather-info ul li { + display: inline-block; + width: 100%; +} + +.wrapper .weather-info ul span, +.wrapper .weather-info .last-updated { + font-family: "Segoe UI Light", "Segoe UI", Arial, Sans-Serif; + opacity: .8; + padding-right: .3rem; +} + +@font-face { + font-family: "Segoe UI Light"; + font-style: normal; + font-weight: normal; + src: local('Segoe UI Light'), url(fonts/segoeuilight.woff2) format("woff2"), url(fonts/segoeuilight.woff) format("woff"), url(fonts/segoeuilight.ttf) format("truetype"); +} + +@font-face { + font-family: "Segoe UI"; + font-style: normal; + font-weight: normal; + src: local('Segoe UI'), url(fonts/segoeui.woff2) format("woff2"), url(fonts/segoeui.woff) format("woff"), url(fonts/segoeui.ttf) format("truetype"); +} + +@font-face { + font-family: "Segoe UI Semibold"; + font-style: normal; + font-weight: normal; + src: local('Segoe UI Semibold'), url(fonts/segoeuisemibold.woff2) format("woff2"), url(fonts/segoeuisemibold.woff) format("woff"), url(fonts/segoeuisemibold.ttf) format("truetype"); +} diff --git a/public-common/util.js b/public-common/util.js new file mode 100644 index 0000000..085784c --- /dev/null +++ b/public-common/util.js @@ -0,0 +1,53 @@ +function urlB64ToUint8Array(base64String) { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + + return outputArray; +} + +function subscribePush(registration) { + return getPublicKey().then(function(key) { + return registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: key + }); + }); +} + +function getPublicKey() { + return fetch('/push/api/key') + .then(function(response) { + return response.json(); + }) + .then(function(data) { + return urlB64ToUint8Array(data.key); + }); +} + +function publishSubscription(subscription, remove) { + return fetch('/push/api/' + (remove ? 'un' : '') + 'subscribe', { + method: 'post', + headers: { + 'Content-type': 'application/json' + }, + body: JSON.stringify({ + subscription: subscription + }) + }); +} + +function saveSubscription(subscription) { + return publishSubscription(subscription); +} + +function deleteSubscription(subscription) { + return publishSubscription(subscription, true); +} diff --git a/public/appcache/appcache.manifest b/public/appcache/appcache.manifest new file mode 100644 index 0000000..6de6f2a --- /dev/null +++ b/public/appcache/appcache.manifest @@ -0,0 +1,15 @@ +CACHE MANIFEST +# 2018-05-08 v2 + +CACHE: +./styles.css +./bg.jpg +./weather.json +./common.js +./main.js + +NETWORK: +* + +FALLBACK: +./contact-us.html ./fallback.html diff --git a/public/appcache/index.html b/public/appcache/index.html new file mode 100644 index 0000000..e66a962 --- /dev/null +++ b/public/appcache/index.html @@ -0,0 +1,27 @@ + + + + Weather + + + + +
+ +
+ + + + + \ No newline at end of file diff --git a/public/appcache/main.js b/public/appcache/main.js new file mode 100644 index 0000000..d6370d2 --- /dev/null +++ b/public/appcache/main.js @@ -0,0 +1,11 @@ +document.addEventListener('DOMContentLoaded', function() { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + updatePage(JSON.parse(this.responseText)); + document.querySelector('.wrapper').style.visibility = 'visible'; + } + }; + xhr.open('GET', './weather.json', true); + xhr.send(); +}); diff --git a/public/basic/main.js b/public/basic/main.js new file mode 100644 index 0000000..9043f99 --- /dev/null +++ b/public/basic/main.js @@ -0,0 +1,36 @@ +if (navigator.serviceWorker) { + navigator.serviceWorker.register('sw.js').then(function() { console.log('SW registered!'); }); +} + +document.addEventListener('DOMContentLoaded', function() { + document.querySelector('.refresh').addEventListener('click', function(event) { + event.preventDefault(); + document.querySelector('.wrapper').style.opacity = .5; + + fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.body.className = ''; + }) + .catch(function() { + document.querySelector('.wrapper').style.opacity = 1; + document.body.className = 'desaturate'; + }); + }); + + fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.body.className = ''; + document.querySelector('.wrapper').style.visibility = 'visible'; + }) + .catch(function() { + displayNetworkError(); + }); +}); diff --git a/public/basic/sw.js b/public/basic/sw.js new file mode 100644 index 0000000..0bc6344 --- /dev/null +++ b/public/basic/sw.js @@ -0,0 +1,5 @@ +self.onfetch = function(event) { + event.respondWith( + fetch(event.request) + ); +} diff --git a/public/basic1/main.js b/public/basic1/main.js new file mode 100644 index 0000000..9043f99 --- /dev/null +++ b/public/basic1/main.js @@ -0,0 +1,36 @@ +if (navigator.serviceWorker) { + navigator.serviceWorker.register('sw.js').then(function() { console.log('SW registered!'); }); +} + +document.addEventListener('DOMContentLoaded', function() { + document.querySelector('.refresh').addEventListener('click', function(event) { + event.preventDefault(); + document.querySelector('.wrapper').style.opacity = .5; + + fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.body.className = ''; + }) + .catch(function() { + document.querySelector('.wrapper').style.opacity = 1; + document.body.className = 'desaturate'; + }); + }); + + fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.body.className = ''; + document.querySelector('.wrapper').style.visibility = 'visible'; + }) + .catch(function() { + displayNetworkError(); + }); +}); diff --git a/public/basic1/sw.js b/public/basic1/sw.js new file mode 100644 index 0000000..6f20dc7 --- /dev/null +++ b/public/basic1/sw.js @@ -0,0 +1,40 @@ +self.oninstall = function(event) { + event.waitUntil( + caches.open('assets-v1').then(function(cache) { + return cache.addAll([ + './', + './index.html', + './styles.css', + './bg.jpg', + './common.js', + './util.js', + './main.js', + './fallback.html' + ]); + }) + ); +} + +self.onactivate = function(event) { + var keepList = ['assets-v1']; + + event.waitUntil( + caches.keys().then(function(cacheNameList) { + return Promise.all(cacheNameList.map(function(cacheName) { + if (keepList.indexOf(cacheName) === -1) { + return caches.delete(cacheName); + } + })); + }) + ); +} + +self.onfetch = function(event) { + event.respondWith( + caches.match(event.request).then(function(response) { + return response || fetch(event.request).catch(function() { + return caches.match('./fallback.html'); + }); + }) + ); +} diff --git a/public/basic2/main.js b/public/basic2/main.js new file mode 100644 index 0000000..551a53d --- /dev/null +++ b/public/basic2/main.js @@ -0,0 +1,35 @@ +if (navigator.serviceWorker) { + navigator.serviceWorker.register('sw.js').then(function() { console.log('SW registered!'); }); +} + +document.addEventListener('DOMContentLoaded', function() { + document.querySelector('.refresh').addEventListener('click', function(event) { + event.preventDefault(); + document.querySelector('.wrapper').style.opacity = .5; + + fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.body.className = ''; + }) + .catch(function() { + document.querySelector('.wrapper').style.opacity = 1; + document.body.className = 'desaturate'; + }); + }); + + fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.querySelector('.wrapper').style.visibility = 'visible'; + }) + .catch(function() { + displayNetworkError(); + }); +}); diff --git a/public/basic2/sw.js b/public/basic2/sw.js new file mode 100644 index 0000000..a7407f1 --- /dev/null +++ b/public/basic2/sw.js @@ -0,0 +1,43 @@ +self.oninstall = function(event) { + event.waitUntil( + caches.open('assets-v1').then(function(cache) { + return cache.addAll([ + './', + './index.html', + './styles.css', + './bg.jpg', + './common.js', + './util.js', + './main.js', + './fallback.html' + ]); + }) + ); +} + +self.onactivate = function(event) { + var keepList = ['assets-v1']; + + event.waitUntil( + caches.keys().then(function(cacheNameList) { + return Promise.all(cacheNameList.map(function(cacheName) { + if (keepList.indexOf(cacheName) === -1) { + return caches.delete(cacheName); + } + })); + }) + ); +} + +self.onfetch = function(event) { + event.respondWith( + caches.match(event.request).then(function(response) { + return response || fetch(event.request).then(function(response) { + caches.open('dynamic').then(function(cache) { + cache.put(event.request, response); + }); + return response.clone(); + }); + }) + ); +} diff --git a/public/basic3/main.js b/public/basic3/main.js new file mode 100644 index 0000000..3c02041 --- /dev/null +++ b/public/basic3/main.js @@ -0,0 +1,46 @@ +if (navigator.serviceWorker) { + navigator.serviceWorker.register('sw.js').then(function() { console.log('SW registered!') }); +} + +document.addEventListener('DOMContentLoaded', function() { + document.querySelector('.refresh').addEventListener('click', function(event) { + event.preventDefault(); + document.querySelector('.wrapper').style.opacity = .5; + + fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.body.className = ''; + }) + .catch(function() { + document.querySelector('.wrapper').style.opacity = 1; + document.body.className = 'desaturate'; + }); + }); + + fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.body.className = ''; + document.querySelector('.wrapper').style.visibility = 'visible'; + }) + .catch(function() { + return caches.match('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.querySelector('.wrapper').style.visibility = 'visible'; + }); + }) + .catch(function() { + displayNetworkError(); + }); +}); diff --git a/public/basic3/sw.js b/public/basic3/sw.js new file mode 100644 index 0000000..4cebac4 --- /dev/null +++ b/public/basic3/sw.js @@ -0,0 +1,51 @@ +self.oninstall = function(event) { + event.waitUntil( + caches.open('assets-v1').then(function(cache) { + return cache.addAll([ + './', + './index.html', + './styles.css', + './bg.jpg', + './common.js', + './util.js', + './main.js', + './fallback.html' + ]); + }) + ); +} + +self.onactivate = function(event) { + var keepList = ['assets-v1']; + + event.waitUntil( + caches.keys().then(function(cacheNameList) { + return Promise.all(cacheNameList.map(function(cacheName) { + if (keepList.indexOf(cacheName) === -1) { + return caches.delete(cacheName); + } + })); + }) + ); +} + +self.onfetch = function(event) { + if (event.request.url.indexOf('weather.json') !== -1) { + event.respondWith(fetchAndCache(event.request)); + } else { + event.respondWith( + caches.match(event.request).then(function(response) { + return response || fetchAndCache(event.request); + }) + ); + } +} + +function fetchAndCache(request) { + return fetch(request).then(function(response) { + caches.open('dynamic').then(function(cache) { + cache.put(request, response); + }); + return response.clone(); + }); +} diff --git a/public/improved/main.js b/public/improved/main.js new file mode 100644 index 0000000..e8007d6 --- /dev/null +++ b/public/improved/main.js @@ -0,0 +1,62 @@ +if (navigator.serviceWorker) { + navigator.serviceWorker.register('sw.js').then(function() { console.log('SW registered!') }); +} + +document.addEventListener('DOMContentLoaded', function() { + document.querySelector('.refresh').addEventListener('click', function(event) { + event.preventDefault(); + document.querySelector('.wrapper').style.opacity = .5; + + fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.body.className = ''; + }) + .catch(function() { + document.querySelector('.wrapper').style.opacity = 1; + document.body.className = 'desaturate'; + }); + }); + + function showLoading() { + + } + + function hideLoading() { + document.querySelector('.wrapper').style.visibility = 'visible'; + } + + showLoading(); + + var networkDone = false; + + var networkRequest = fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + networkDone = true; + updatePage(json); + document.body.className = ''; + }); + + caches.match('./weather.json') + .then(function(response) { + if (!response) { + throw Error('No data'); + } + + return response.json(); + }) + .then(function(json) { + if (!networkDone) updatePage(json); + }) + .catch(function() {return networkRequest;}) + .catch(function() { + displayNetworkError(); + }) + .then(hideLoading); +}); diff --git a/public/improved/sw.js b/public/improved/sw.js new file mode 100644 index 0000000..4cebac4 --- /dev/null +++ b/public/improved/sw.js @@ -0,0 +1,51 @@ +self.oninstall = function(event) { + event.waitUntil( + caches.open('assets-v1').then(function(cache) { + return cache.addAll([ + './', + './index.html', + './styles.css', + './bg.jpg', + './common.js', + './util.js', + './main.js', + './fallback.html' + ]); + }) + ); +} + +self.onactivate = function(event) { + var keepList = ['assets-v1']; + + event.waitUntil( + caches.keys().then(function(cacheNameList) { + return Promise.all(cacheNameList.map(function(cacheName) { + if (keepList.indexOf(cacheName) === -1) { + return caches.delete(cacheName); + } + })); + }) + ); +} + +self.onfetch = function(event) { + if (event.request.url.indexOf('weather.json') !== -1) { + event.respondWith(fetchAndCache(event.request)); + } else { + event.respondWith( + caches.match(event.request).then(function(response) { + return response || fetchAndCache(event.request); + }) + ); + } +} + +function fetchAndCache(request) { + return fetch(request).then(function(response) { + caches.open('dynamic').then(function(cache) { + cache.put(request, response); + }); + return response.clone(); + }); +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..ab924dd --- /dev/null +++ b/public/index.html @@ -0,0 +1,32 @@ + + + + Service Worker Demos + + + + +
+
+
+
+ Service Worker + Weather App Demos + +

+ +
+
+
+ + \ No newline at end of file diff --git a/public/push/main.js b/public/push/main.js new file mode 100644 index 0000000..0be0afa --- /dev/null +++ b/public/push/main.js @@ -0,0 +1,126 @@ +if (navigator.serviceWorker) { + registerServiceWorker().then(function() { + registerPush().then(function(sub) { + sendMessage(sub, 'Extreme weather! Come check it out!'); + }); + }); +} else { + // service worker is not supported, so it won't work! + console.log('SW & Push are Not Supported'); +} + +document.addEventListener('DOMContentLoaded', function() { + document.querySelector('.refresh').addEventListener('click', function(event) { + event.preventDefault(); + document.querySelector('.wrapper').style.opacity = .5; + + fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + updatePage(json); + document.body.className = ''; + }) + .catch(function() { + document.querySelector('.wrapper').style.opacity = 1; + document.body.className = 'desaturate'; + }); + }); + + function showLoading() { + + } + + function hideLoading() { + document.querySelector('.wrapper').style.visibility = 'visible'; + } + + showLoading(); + + var networkDone = false; + + var networkRequest = fetch('./weather.json') + .then(function(response) { + return response.json(); + }) + .then(function(json) { + networkDone = true; + updatePage(json); + document.body.className = ''; + }); + + caches.match('./weather.json') + .then(function(response) { + if (!response) { + throw Error('No data'); + } + + return response.json(); + }) + .then(function(json) { + if (!networkDone) updatePage(json); + }) + .catch(function() {return networkRequest;}) + .catch(function() { + displayNetworkError(); + }) + .then(hideLoading); +}); + +function registerServiceWorker() { + return navigator.serviceWorker.register('sw.js'); +} + +function resetServiceWorkerAndPush() { + return navigator.serviceWorker.getRegistration() + .then(function(registration) { + if (registration) { + return registration.unregister(); + } + }) + .then(function() { + return registerServiceWorker().then(function(registration) { + return registerPush(); + }); + }); +} + +function registerPush() { + return navigator.serviceWorker.ready + .then(function(registration) { + return registration.pushManager.getSubscription().then(function(subscription) { + if (subscription) { + // renew subscription if we're within 5 days of expiration + if (subscription.expirationTime && Date.now() > subscription.expirationTime - 432000000) { + return subscription.unsubscribe().then(function() { + return subscribePush(registration); + }); + } + + return subscription; + } + + return subscribePush(registration); + }); + }) + .then(function(subscription) { + saveSubscription(subscription); + return subscription; + }); +} + +function sendMessage(sub, message) { + const data = { + subscription: sub, + payload: message + } + + return fetch('./api/notify', { + method: 'post', + headers: { + 'Content-type': 'application/json' + }, + body: JSON.stringify(data) + }); +} diff --git a/public/push/sw.js b/public/push/sw.js new file mode 100644 index 0000000..f201193 --- /dev/null +++ b/public/push/sw.js @@ -0,0 +1,94 @@ +importScripts('./util.js'); + +self.oninstall = function(event) { + event.waitUntil( + caches.open('assets-v1').then(function(cache) { + return cache.addAll([ + './', + './index.html', + './styles.css', + './bg.jpg', + './common.js', + './util.js', + './main.js' + ]); + }) + ); +} + +self.onactivate = function(event) { + var keepList = ['assets-v1']; + + event.waitUntil( + caches.keys().then(function(cacheNameList) { + return Promise.all(cacheNameList.map(function(cacheName) { + if (keepList.indexOf(cacheName) === -1) { + return caches.delete(cacheName); + } + })); + }) + ); +} + +function fetchAndCache(request) { + return fetch(request).then(function(response) { + caches.open('dynamic').then(function(cache) { + cache.put(request, response); + }); + return response.clone(); + }); +} + +self.onfetch = function(event) { + if (event.request.url.indexOf('weather.json') !== -1) { + event.respondWith(fetchAndCache(event.request)); + } else { + event.respondWith( + caches.match(event.request).then(function(response) { + return response || fetchAndCache(event.request); + }) + ); + } +} + +self.onpush = function(event) { + const payload = event.data ? event.data.text() : 'no payload'; + event.waitUntil( + registration.showNotification('Weather Advisory', { + body: payload, + icon: 'icon.png' + }) + ); +} + +self.onnotificationclick = function(event) { + event.notification.close(); + + event.waitUntil( + clients.matchAll({ type: 'window', includeUncontrolled: true }) + .then(function(clientList) { + if (clientList.length > 0) { + let client = clientList[0]; + for (let i = 0; i < clientList.length; i++) { + if (clientList[i].focused) { client = clientList[i]; } + } + return client.focus(); + } + + return clients.openWindow('/push/'); + }) + ); +} + +self.onpushsubscriptionchange = function(event) { + event.waitUntil( + Promise.all([ + Promise.resolve(event.oldSubscription ? + deleteSubscription(event.oldSubscription) : true), + Promise.resolve(event.newSubscription ? + event.newSubscription : + subscribePush(registration)) + .then(function(sub) { return saveSubscription(sub); }) + ]) + ); +} diff --git a/serve-push.js b/serve-push.js new file mode 100644 index 0000000..4b575a6 --- /dev/null +++ b/serve-push.js @@ -0,0 +1,74 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const configuredWebPush = require('./configured-web-push'); +const { Subscription } = require('./db'); +const router = express.Router(); +router.use(bodyParser.json()); + +// Push Logic +router.get('/api/key', function(req, res) { + if (configuredWebPush.vapidPublicKey !== '') { + res.send({ + key: configuredWebPush.vapidPublicKey + }); + } else { + res.status(500).send({ + key: 'VAPID KEYS ARE NOT SET' + }); + } +}); + +router.post('/api/subscribe', async function(req, res) { + try { + const sub = req.body.subscription; + + // Find if user is already subscribed searching by `endpoint` + const exists = await Subscription.findOne({ endpoint: sub.endpoint }); + + if (exists) { + res.status(400).send('Subscription already exists'); + + return; + } + + await (new Subscription(sub)).save(); + + res.status(200).send('Success'); + } catch (e) { + res.status(500).send(e.message); + } +}); + +router.post('/api/unsubscribe', async function(req, res) { + try { + const sub = req.body.subscription; + + await Subscription.remove({endpoint: sub.endpoint}); + console.log('Deleted: ' + sub.endpoint); + + res.status(200).send('Success'); + } catch (e) { + res.status(500).send(e.message); + } +}); + +router.post('/api/notify', async function(req, res) { + try { + const data = req.body; + + await configuredWebPush.webPush.sendNotification(data.subscription, data.payload, { contentEncoding: data.encoding }) + .then(function (response) { + console.log('Response: ' + JSON.stringify(response, null, 4)); + res.status(201).send(response); + }) + .catch(function (e) { + console.log('Error: ' + JSON.stringify(e, null, 4)); + res.status(201).send(e); + }); + } catch (e) { + res.status(500) + .send(e.message); + } +}); + +module.exports = router; diff --git a/serve.js b/serve.js new file mode 100644 index 0000000..2fb799f --- /dev/null +++ b/serve.js @@ -0,0 +1,91 @@ +const express = require('express'); + +const app = express(); +const port = process.env.PORT || 2020; + +app.use(express.static('public')); +app.use('/push', require('./serve-push')); +app.disable('x-powered-by'); + +app.get('/appcache/appcache.manifest', function(req, res) { + res.set('Content-type', 'text/cache-manifest').status(200).send(); +}); + +const { lstatSync, readdirSync } = require('fs'); +const { join } = require('path'); + +const isDirectory = source => lstatSync(source).isDirectory(); +const getDirectories = source => + readdirSync(source).map(name => name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'))//.filter(isDirectory).map(name => name); + +const dirs = getDirectories('public').join('|'); + +// const files = getDirectories('public-common'); + +// for (file of files) { +// app.get(getRegExp(file), function (req, res) { +// res.sendFile() +// }); +// } + +function getRegExp(path) { + return new RegExp(`/(${dirs})/${path}`); +} + +app.get(getRegExp('weather.json'), async function(req, res) { + let temp; + let description; + let details; + + function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + function pad(num) { + return (num < 10 ? '0' : '') + num; + } + + weatherTypes = [ + 'Mostly Sunny', + 'Partly Sunny', + 'Mostly Cloudy', + 'Partly Cloudy', + 'Rain', + 'Snow' + ]; + + temp = getRandomInt(0, 99); + description = weatherTypes[getRandomInt(0, weatherTypes.length - 1)]; + details = [ + {label: 'Feels Like', value: getRandomInt(temp - 10, temp + 10) + '°'}, + {label: 'Wind', value: getRandomInt(0, 35) + ' mph'}, + {label: 'Barometer', value: getRandomInt(29, 31) + '.' + pad(getRandomInt(0, 99)) + ' in'}, + {label: 'Visibility', value: getRandomInt(1, 50) + ' mi'}, + {label: 'Humidity', value: getRandomInt(20, 90) + '%'}, + {label: 'Dew Point', value: getRandomInt(temp - 20, temp - 5) + '°'} + ]; + + setTimeout(function() { + res.status(200).send({ + temp: temp, + description: description, + details: details, + time: Date.now() + }); + }, 2000); +}); + +app.get('/appcache(/index.html)?', function(req, res) { + res.sendFile(__dirname + '/public/' + req.params[1] + '/index.html'); +}); + +app.get(getRegExp('fonts/(.*)'), function(req, res) { + res.sendFile(__dirname + '/public-common/fonts/' + req.params[1]); +}); +app.get(getRegExp('(index.html|bg.jpg|fallback.html|styles.css|common.js|util.js)?'), function(req, res) { + res.sendFile(__dirname + '/public-common/' + (req.params[1] === undefined ? 'index.html' : req.params[1])); +}); + +app.listen(port, function() { + console.log('Server listening on port ' + port); +}); diff --git a/web.config b/web.config new file mode 100644 index 0000000..ba8f527 --- /dev/null +++ b/web.config @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +