Moved some files around, improved how notifications are being requested, and improved activation and installation-flow of serviceworker

This commit is contained in:
Kasper Rynning-Tønnesen
2020-02-21 14:36:27 +01:00
parent ac1e73ee09
commit 8b1d86bd9b
8 changed files with 291 additions and 113 deletions

View File

@@ -2,103 +2,49 @@
<div class="app-container">
<banner />
<router-view />
<UpdateToast
v-if="showToast"
:text="toastText"
:refreshButton="refreshToast"
v-on:closeToast="closeToast"
/>
</div>
</template>
<script>
import ServiceWorkerMixin from "@/mixins/ServiceWorkerMixin";
import banner from "@/ui/Banner";
import UpdateToast from "@/ui/UpdateToast";
export default {
name: "vinlottis",
components: { banner },
components: { banner, UpdateToast },
props: {},
data() {
return {};
return {
showToast: false,
toastText: null,
refreshToast: false
};
},
mounted() {
console.log("SNEAKY PETE!");
if ("serviceWorker" in navigator) {
const channel = new BroadcastChannel("updatePush");
channel.addEventListener("message", event => {
if (event.data.success) {
localStorage.setItem("push", true);
}
});
navigator.serviceWorker
.register("/service-worker.js")
.then(serviceWorker => {
console.log(
"Arbeids arbeideren din er installert. Du kan nå gå offline frem til neste trekning."
);
// From your client pages:
serviceWorker.onupdatefound = () => {
const installingWorker = serviceWorker.installing;
installingWorker.onstatechange = () => {
if (
installingWorker.state === "installed" &&
navigator.serviceWorker.controller
) {
// Preferably, display a message asking the user to reload...
location.reload();
}
};
};
if (!("PushManager" in window)) {
throw new Error("No Push API Support!");
}
window.Notification.requestPermission().then(permission => {
if (permission !== "granted") {
console.log(
"Du valgte å ikke ha arbeids-arbeideren til å sende deg dytte-meldinger :'('"
);
return;
}
if (localStorage.getItem("push") == null) {
this.sendMessage("updatePush");
}
});
})
.catch(error => {
console.error("Arbeids arbeideren klarer ikke arbeide.", error);
});
}
this.$on("service-worker-updated", () => {
this.toastText = "Det er ny oppdatering av siden, vil du oppdatere?";
this.showToast = true;
this.refreshToast = true;
});
this.$on("push-allowed", () => {
this.toastText = "Push-notifications er skrudd på!";
this.refreshToast = false;
this.showToast = true;
});
},
computed: {},
mixins: [ServiceWorkerMixin],
methods: {
sendMessage: function(message) {
// This wraps the message posting/response in a promise, which will
// resolve if the response doesn't contain an error, and reject with
// the error if it does. If you'd prefer, it's possible to call
// controller.postMessage() and set up the onmessage handler
// independently of a promise, but this is a convenient wrapper.
return new Promise(function(resolve, reject) {
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
if (event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
// This sends the message data as well as transferring
// messageChannel.port2 to the service worker.
// The service worker can then use the transferred port to reply
// via postMessage(), which will in turn trigger the onmessage
// handler on messageChannel.port1.
// See
// https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage
if (navigator.serviceWorker.controller == null) {
resolve();
}
navigator.serviceWorker.controller.postMessage(message, [
messageChannel.port2
]);
});
closeToast: function() {
this.showToast = false;
}
}
};

View File

@@ -1,7 +1,17 @@
<template>
<div class="outer">
<div class="container">
<h1 class="title" @click="startCountdown">Vinlotteri</h1>
<div class="header-top">
<h1 class="title" @click="startCountdown">Vinlotteri</h1>
<img
src="/public/assets/images/notification.svg"
alt="Notification-bell"
@click="requestNotificationAccess"
class="notification-request-button"
role="button"
v-if="notificationAllowed"
/>
</div>
<router-link to="generate" class="generate-link">
Klarer du ikke velge lodd-farger?
<span class="subtext generator-link">Prøv loddgeneratoren</span>
@@ -57,10 +67,23 @@ export default {
data() {
return {
hardStart: false,
todayExists: false
todayExists: false,
pushAllowed: false
};
},
computed: {
notificationAllowed: function() {
return (
Notification.permission !== "granted" ||
!this.pushAllowed ||
localStorage.getItem("push") == null
);
}
},
mounted() {
this.$on("push-allowed", () => {
this.pushAllowed = true;
});
fetch("/api/wines/prelottery")
.then(wines => wines.json())
.then(wines => {
@@ -76,6 +99,9 @@ export default {
this.track();
},
methods: {
requestNotificationAccess() {
this.$root.$children[0].registerServiceWorkerPushNotification();
},
changeEnabled(way) {
this.hardStart = way;
},
@@ -93,6 +119,11 @@ export default {
@import "../styles/global.scss";
@import "../styles/media-queries.scss";
.notification-request-button {
cursor: pointer;
margin-left: 15px;
}
.bottom-container {
display: flex;
flex-direction: row;
@@ -107,8 +138,22 @@ export default {
}
}
.title {
cursor: pointer;
.header-top {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-bottom: 2rem;
margin-top: 3.8rem;
@include mobile {
margin-top: 1.5rem;
}
.title {
cursor: pointer;
margin: auto 0;
}
}
.left-bottom {

View File

@@ -0,0 +1,90 @@
var serviceWorkerRegistrationMixin = {
created: function() {
if (!("serviceWorker" in navigator)) {
console.log("Nettleseren din støtter ikke service-workers.");
return;
}
if (Notification.permission !== "granted") {
localStorage.removeItem("push");
}
this.registerPushListener();
this.registerServiceWorker();
},
methods: {
registerPushListener: function() {
const channel = new BroadcastChannel("updatePush");
channel.addEventListener("message", event => {
if (event.data.success) {
localStorage.setItem("push", true);
this.$emit("push-allowed");
}
});
},
sendMessage: function(message) {
return new Promise(function(resolve, reject) {
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
if (event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
if (navigator.serviceWorker.controller == null) {
resolve();
} else {
navigator.serviceWorker.controller.postMessage(message, [
messageChannel.port2
]);
}
});
},
serviceWorkerUpdateFoundListener: function(serviceWorker) {
const installingWorker = serviceWorker.installing;
installingWorker.onstatechange = () => {
if (
installingWorker.state === "installed" &&
navigator.serviceWorker.controller
) {
this.$emit("service-worker-updated");
}
};
},
registerServiceWorkerPushNotification: function() {
if (!("PushManager" in window)) {
throw new Error("No Push API Support!");
}
window.Notification.requestPermission().then(permission => {
if (permission !== "granted") {
console.log(
"Du valgte å ikke ha arbeids-arbeideren til å sende deg dytte-meldinger :'('"
);
return;
}
if (localStorage.getItem("push") == null) {
this.sendMessage("updatePush");
}
});
},
registerServiceWorker: function() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/service-worker.js")
.then(serviceWorker => {
console.log(
"Arbeids arbeideren din er installert. Du kan nå gå offline frem til neste trekning."
);
serviceWorker.onupdatefound = () => {
this.serviceWorkerUpdateFoundListener(serviceWorker);
};
//this.registerServiceWorkerPushNotification();
})
.catch(error => {
console.error("Arbeids arbeideren klarer ikke arbeide.", error);
});
}
}
}
};
module.exports = serviceWorkerRegistrationMixin;

View File

@@ -0,0 +1,168 @@
var version = "v1.0" + __DATE__;
var cacheName = "vinlottis";
var CACHE_NAME = cacheName;
var CACHE_NAME_API = cacheName + "::api";
var STATIC_CACHE_URLS = ["/"];
console.log("Nåværende versjon:", version);
self.addEventListener("activate", event => {
console.log("Aktiverer");
event.waitUntil(self.clients.claim());
event.waitUntil(removeCache(CACHE_NAME));
event.waitUntil(removeCache(CACHE_NAME_API));
event.waitUntil(addCache(CACHE_NAME, STATIC_CACHE_URLS));
});
self.addEventListener("message", event => {
if (event.data === "updatePush") {
event.waitUntil(
new Promise((resolve, reject) => {
const applicationServerKey = urlB64ToUint8Array(__PUBLICKEY__);
const options = { applicationServerKey, userVisibleOnly: true };
self.registration.pushManager
.subscribe(options)
.then(subscription =>
saveSubscription(subscription)
.then(() => {
const channel = new BroadcastChannel("updatePush");
channel.postMessage({ success: true });
resolve();
})
.catch(() => {
resolve();
})
)
.catch(() => {
console.log("Kunne ikke legge til pushnotifications");
reject();
});
})
);
}
});
self.addEventListener("push", function(event) {
if (event.data) {
var message = JSON.parse(event.data.text());
showLocalNotification(message.title, message.message, self.registration);
} else {
}
});
self.addEventListener("install", event => {
console.log("Arbeids arbeideren installerer seg.");
self.skipWaiting();
event.waitUntil(addCache(CACHE_NAME, STATIC_CACHE_URLS));
});
self.addEventListener("fetch", event => {
if (
event.request.url.includes("/login") ||
event.request.url.includes("/update") ||
event.request.url.includes("/register") ||
event.request.method == "POST" ||
event.request.url.includes("/api/wines/prelottery")
) {
event.respondWith(fetch(event.request));
return;
}
if (event.request.url.includes("/api/")) {
event.respondWith(
fetch(event.request)
.then(response => cache(event.request, response))
.catch(function() {
return caches.match(event.request);
})
);
} else {
event.respondWith(
caches
.match(event.request) // check if the request has already been cached
.then(cached => cached || fetch(event.request)) // otherwise request network
.then(
response =>
staticCache(event.request, response) // put response in cache
.then(() => response) // resolve promise with the network response
)
);
}
});
function showLocalNotification(title, body, swRegistration) {
const options = {
body,
icon: "https://lottis.vin/public/assets/images/favicon.png",
image: "https://lottis.vin/public/assets/images/favicon.png",
vibrate: [300]
};
swRegistration.showNotification(title, options);
}
async function saveSubscription(subscription) {
const SERVER_URL = "/subscription/save-subscription";
const response = await fetch(SERVER_URL, {
method: "post",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(subscription)
});
return response.json();
}
const urlB64ToUint8Array = base64String => {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, "+")
.replace(/_/g, "/");
const rawData = atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
function addCache(cacheKey, cacheUrls) {
return caches.open(cacheKey).then(cache => {
console.log("Legger til cache", cache);
return cache.addAll(cacheUrls);
});
}
function removeCache(cacheKey) {
return caches
.keys()
.then(keys => keys.filter(key => key !== cacheKey))
.then(keys =>
Promise.all(
keys.map(key => {
console.log(`Sletter mellom-lager på nøkkel ${key}`);
return caches.delete(key);
})
)
);
}
function staticCache(request, response) {
if (response.type === "error" || response.type === "opaque") {
return Promise.resolve(); // do not put in cache network errors
}
return caches
.open(CACHE_NAME)
.then(cache => cache.put(request, response.clone()));
}
function cache(request, response) {
if (response.type === "error" || response.type === "opaque") {
return response;
}
return caches.open(CACHE_NAME_API).then(cache => {
cache.put(request, response.clone());
return response;
});
}

100
src/ui/UpdateToast.vue Normal file
View File

@@ -0,0 +1,100 @@
<template>
<div class="update-toast" :class="showClass">
<span>{{text}}</span>
<div class="button-container">
<button v-if="refreshButton" @click="refresh">Refresh</button>
<button @click="closeToast">Lukk</button>
</div>
</div>
</template>
<script>
export default {
props: {
text: { type: String, required: true },
refreshButton: { type: Boolean, required: false }
},
data() {
return { showClass: null };
},
created() {
this.showClass = "show";
},
mounted() {
if (this.refreshButton) {
return;
}
setTimeout(() => {
this.$emit("closeToast");
}, 5000);
},
methods: {
refresh: function() {
location.reload();
},
closeToast: function() {
this.$emit("closeToast");
}
}
};
</script>
<style lang="scss" scoped>
@import "../styles/media-queries.scss";
.update-toast {
position: fixed;
bottom: 10px;
left: 0;
right: 0;
margin: auto;
background: #2d2d2d;
border-radius: 5px;
padding: 15px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 50vw;
opacity: 0;
pointer-events: none;
&.show {
pointer-events: all;
opacity: 1;
}
-webkit-transition: opacity 0.5s ease-in-out;
-moz-transition: opacity 0.5s ease-in-out;
-ms-transition: opacity 0.5s ease-in-out;
-o-transition: opacity 0.5s ease-in-out;
transition: opacity 0.5s ease-in-out;
@include mobile {
width: 85vw;
bottom: 0px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
& span {
color: white;
}
& .button-container {
& button {
color: #2d2d2d;
background-color: white;
border-radius: 5px;
padding: 10px;
margin: 0 3px;
font-size: 0.8rem;
&:active {
background: #2d2d2d;
color: white;
}
}
}
}
</style>

View File

@@ -1,7 +1,12 @@
<template>
<div class="inner-wine-container">
<div class="inner-wine-container" :class="{ 'big': fullscreen }">
<div class="left">
<img :src="wine.image" class="wine-image" :class="{ 'fullscreen': fullscreen }"/>
<!-- <img :src="wine.image" class="wine-image" :class="{ 'fullscreen': fullscreen }"/> -->
<img
src="https://images.vivino.com/thumbs/QRhTyEmKR8Wi_C1N2uqBWg_pb_x960.png"
class="wine-image"
:class="{ 'fullscreen': fullscreen }"
/>
</div>
<div class="right">
<h2>{{ wine.name }}</h2>
@@ -12,7 +17,7 @@
Vunnet av:
{{wine.winners.join(", ")}}
</span>
<div class="color-wins">
<div class="color-wins" :class="{ 'big': fullscreen }">
<span class="color-win blue">{{wine.blue == undefined ? 0 : wine.blue}}</span>
<span class="color-win red">{{wine.red == undefined ? 0 : wine.red}}</span>
<span class="color-win green">{{wine.green == undefined ? 0 : wine.green}}</span>
@@ -45,6 +50,7 @@ export default {
&.fullscreen {
@include desktop {
height: unset;
max-height: 65vh;
}
}
}
@@ -56,6 +62,10 @@ export default {
flex-wrap: wrap;
}
.color-wins.big {
width: unset;
}
span.color-win {
border: 2px solid transparent;
color: #333;
@@ -111,6 +121,10 @@ h3 {
font-family: Arial;
margin-bottom: 30px;
&.big {
align-items: center;
}
@include desktop {
justify-content: center;
}