Moved some files around, improved how notifications are being requested, and improved activation and installation-flow of serviceworker
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
90
src/mixins/serviceWorkerMixin.js
Normal file
90
src/mixins/serviceWorkerMixin.js
Normal 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;
|
||||
168
src/service-worker/service-worker.js
Normal file
168
src/service-worker/service-worker.js
Normal 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
100
src/ui/UpdateToast.vue
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user