notification implemented
authorebelcrom <ebelcrom@gmail.com>
Sun, 19 Jan 2020 22:25:13 +0000 (23:25 +0100)
committerebelcrom <ebelcrom@gmail.com>
Sun, 19 Jan 2020 22:25:13 +0000 (23:25 +0100)
src/lib/lib.js
src/service-worker.js [new file with mode: 0644]
src/views/Settings.vue
vue.config.js

index 9c213c1f3febbe16067aedef6c22a70f81f34582..f0bc4d4154ee87eb84213a727e3b8efd1fd61b24 100644 (file)
@@ -1,3 +1,14 @@
+const url = 'https://binomiant.duckdns.org/mVk7Yr3k/v1';
+const prodStatus = '/status';
+const prodEvents = '/events';
+const prodControl = '/control';
+const prodNotification = '/test/notification';
+const testStatus = '/test/status';
+const testEvents = '/test/events';
+const testControl = '/test/control';
+const base64PubKey = 'BLDSdGasI5sLks30brbIWvlLMFqzoxxkOs7'
+  + 'aW_E9PDBzIO_mDs6-tvtb2U0-BVFDafNd58DJgoXxdK5711FF29c';
+
 function setItem(key, value) {
   if (key === null) {
     return;
@@ -32,17 +43,176 @@ export const storage = {
   hasItem: hasItem,
 };
 
+function urlBase64ToUint8Array(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 register() {
+  const permission = Notification.permission;
+  switch (permission) {
+    case 'granted':
+      return new Promise((resolve, reject) => {
+        navigator.serviceWorker.ready
+        .then(registration => {
+          if (registration) {
+            registration.pushManager.getSubscription()
+            .then(subscription => {
+              if (subscription) {
+                resolve(subscription);
+              } else {
+                const publicKey = urlBase64ToUint8Array(base64PubKey);
+                registration.pushManager.subscribe({
+                  userVisibleOnly: true,
+                  applicationServerKey: publicKey,
+                })
+                .then(subscription => {
+                  if (subscription) {
+                    resolve(subscription);
+                  } else {
+                    reject(null);
+                  }
+                });
+              }
+            })
+            .catch(err => {
+              reject(err);
+            });
+          } else {
+            reject(null);
+          }
+        })
+        .catch(err => {
+          reject(err);
+        });
+      });
+      break;
+    case 'default':
+      return new Promise((resolve, reject) => {
+        Notification.requestPermission()
+        .then(status => {
+          if (status) {
+            switch (status) {
+              case 'granted':
+                navigator.serviceWorker.ready
+                .then(registration => {
+                  if (registration) {
+                    const publicKey = urlBase64ToUint8Array(base64PubKey);
+                    registration.pushManager.subscribe({
+                      userVisibleOnly: true,
+                      applicationServerKey: publicKey,
+                    })
+                    .then(subscription => {
+                      if (subscription) {
+                        resolve(subscription);
+                      } else {
+                        reject(null);
+                      }
+                    });
+                  } else {
+                    reject(null);
+                  }
+                })
+                .catch(err => {
+                  reject(err);
+                });
+                break;
+              case 'denied':
+              case 'default':
+              default:
+                reject(null);
+                break;
+            }
+          } else {
+            reject(null);
+          }
+        })
+        .catch(err => {
+          reject(err);
+        });
+      });
+      break;
+    case 'denied':
+    default:
+      return Promise.reject();
+      break;
+  }
+}
+
+function subscribe() {
+  var apiKey = getItem('apiKey');
+
+  var data = {
+    path: prodNotification,
+    query: '',
+    method: 'POST',
+    apiKey: apiKey,
+    requestContentType: 'json',
+    responseContentType: 'json',
+    body: '',
+  }
+
+  return new Promise((resolve, reject) => {
+    register()
+    .then(subscription => {
+      if (subscription === null) {
+        reject(null);
+      } else {
+        data.body = JSON.stringify(subscription);
+        sendRequest(data)
+        .then(response => {
+          resolve(response);
+        })
+        .catch(err => {
+          reject(err);
+        });
+      }
+    })
+    .catch(err => {
+      reject(err);
+    });
+  });
+}
+
+function unsubscribe() {
+  return new Promise((resolve, reject) => {
+    navigator.serviceWorker.ready
+    .then(registration => {
+      registration.pushManager.getSubscription()
+      .then(subscription => {
+        subscription.unsubscribe()
+        .then(result => {
+          resolve(result);
+        })
+        .catch(err => {
+          reject(err);
+        });
+      })
+      .catch(err => {
+        reject(err);
+      });
+    })
+    .catch(err => {
+      reject(err);
+    });
+  });
+}
+
 export const notification = {
+  subscribe: subscribe,
+  unsubscribe: unsubscribe,
 };
 
-const url = 'https://binomiant.duckdns.org/mVk7Yr3k/v1';
-const prodStatus = '/status';
-const prodEvents = '/events';
-const prodControl = '/control';
-const testStatus = '/test/status';
-const testEvents = '/test/events';
-const testControl = '/test/control';
-
 async function sendRequest(data) {
   var init = {
     method: data.method,
@@ -51,11 +221,14 @@ async function sendRequest(data) {
   if (data.requestContentType != null) {
     init.headers['Content-Type'] = 'application/json';
   }
-  if (data.apiKey == null) {
+  if (data.apiKey === '') {
     init.headers['X-API-Key-Test'] = '2TTqCD4mNNny';
   } else {
     init.headers['X-API-Key'] = data.apiKey;
   }
+  if (data.body) {
+    init.body = data.body;
+  }
 
   const response = await fetch(url + data.path + data.query, init);
   if (response.status >= 200 && response.status <= 399) {
@@ -73,7 +246,7 @@ async function sendRequest(data) {
 function getStatus() {
   const testMode = getItem('testMode');
   var path = '',
-      apiKey = null;
+      apiKey = '';
   var query = new URLSearchParams('');
 
   if (testMode) {
@@ -99,7 +272,7 @@ function getStatus() {
 function getEvents() {
   const testMode = getItem('testMode');
   var path = '',
-      apiKey = null;
+      apiKey = '';
   var query = new URLSearchParams('');
 
   if (testMode) {
@@ -180,7 +353,7 @@ function getEvents() {
 function postControl() {
   const testMode = getItem('testMode');
   var path = '',
-      apiKey = null;
+      apiKey = '';
   var query = new URLSearchParams('');
 
   if (testMode) {
diff --git a/src/service-worker.js b/src/service-worker.js
new file mode 100644 (file)
index 0000000..c093648
--- /dev/null
@@ -0,0 +1,59 @@
+/* Custom part */
+
+workbox.core.setCacheNameDetails({prefix: "garnod-pwa.git"});
+
+self.addEventListener('message', (event) => {
+  if (event.data && event.data.type === 'SKIP_WAITING') {
+    self.skipWaiting();
+  }
+});
+
+self.addEventListener('push', event => {
+  const notificationTitle = 'Garage Node';
+
+  var message = 'Door is open!';
+  var icon = null;
+  if (event.data) {
+    message = event.data.json().message;
+    icon = event.data.json().icon;
+  }
+
+  var notificationOptions = {
+    body: message,
+    tag: 'notification'
+  };
+  if (icon) {
+    notificationOptions.badge = icon;
+    notificationOptions.icon = icon;
+  }
+
+  event.waitUntil(
+    Promise.all([
+      self.registration.showNotification(
+        notificationTitle, notificationOptions)
+    ])
+  );
+});
+
+self.addEventListener('notificationclick', event => {
+  event.notification.close();
+
+  var clickResponsePromise = Promise.resolve();
+  if (event.notification.data && event.notification.data.url) {
+    clickResponsePromise = clients.openWindow(event.notification.data.url);
+  }
+
+  event.waitUntil(
+    Promise.all([
+      clickResponsePromise
+    ])
+  );
+});
+
+/**
+ * The workboxSW.precacheAndRoute() method efficiently caches and responds to
+ * requests for URLs in the manifest.
+ * See https://goo.gl/S9QRab
+ */
+self.__precacheManifest = [].concat(self.__precacheManifest || []);
+workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
index 1a59720f1d984056994a87adc39a43d9d17558f4..ae173fa207908a12051c6ff7f0aca7597163b818 100644 (file)
       :showApiKeyDialog="showApiKeyDialog"
       v-on:apiKeySetEvent="setApiKey"
     />
+
+    <v-snackbar
+      v-model="snackbar.show"
+      :timeout="snackbar.timeout"
+      :color="snackbar.color"
+      bottom
+    >
+      {{snackbar.text}}
+    </v-snackbar>
   </v-content>
 </template>
 
 <script>
 import ApiKeyDialog from '@/components/ApiKey'
-import { storage } from '@/lib/lib.js'
+import { storage, notification } from '@/lib/lib.js'
+
+const messages = {
+  subscribeFailed: {
+    text: 'Subscribe notification failed',
+    color: 'error',
+  },
+};
 
 export default {
   components: {
@@ -115,6 +131,12 @@ export default {
         },
       ],
       showApiKeyDialog: false,
+      snackbar: {
+        show: false,
+        timeout: 5000,
+        color: messages.subscribeFailed.color,
+        text: messages.subscribeFailed.text,
+      },
     };
   },
   created() {
@@ -153,6 +175,40 @@ export default {
       this.listItems[1].value = !this.listItems[1].value;
       this.settings[1].value = this.listItems[1].value;
       storage.setItem('pushNotification', this.settings[1].value);
+
+      if (this.settings[1].value) {
+        notification.subscribe()
+        .then(result => {
+          if (result === null) {
+            this.listItems[1].value = !this.listItems[1].value;
+            this.settings[1].value = this.listItems[1].value;
+            storage.setItem('pushNotification', this.settings[1].value);
+            this.snackbar.show = true;
+          }
+        })
+        .catch(err => {
+          this.listItems[1].value = !this.listItems[1].value;
+          this.settings[1].value = this.listItems[1].value;
+          storage.setItem('pushNotification', this.settings[1].value);
+          this.snackbar.show = true;
+        });
+      } else {
+        notification.unsubscribe()
+        .then(result => {
+          if (result === null) {
+            this.listItems[1].value = !this.listItems[1].value;
+            this.settings[1].value = this.listItems[1].value;
+            storage.setItem('pushNotification', this.settings[1].value);
+            this.snackbar.show = true;
+          }
+        })
+        .catch(err => {
+          this.listItems[1].value = !this.listItems[1].value;
+          this.settings[1].value = this.listItems[1].value;
+          storage.setItem('pushNotification', this.settings[1].value);
+          this.snackbar.show = true;
+        });
+      }
     },
     setPushNotificationInSwitch() {
       this.listItems[1].value = !this.listItems[1].value;
index 9405fcc28b31e66f416792968a69b41cf1cd92cb..285a84b2087122736771f0f2f80c89e3c6cfc455 100644 (file)
@@ -6,8 +6,13 @@ module.exports = {
   pwa: {
     name: 'Garage Node',
     themeColor: '#1976D2',
-    msTileColor: '#424242'
+    msTileColor: '#424242',
+
+    workboxPluginMode: 'InjectManifest',
+    workboxOptions: {
+      swSrc: 'src/service-worker.js'
+    }
   },
 
-  publicPath: ''
+  publicPath: '/mVk7Yr3k'
 }