Trent Larson
2 weeks ago
5 changed files with 491 additions and 506 deletions
@ -0,0 +1,481 @@ |
|||||
|
<template> |
||||
|
<transition |
||||
|
enter-active-class="transform ease-out duration-300 transition" |
||||
|
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4" |
||||
|
enter-to-class="translate-y-0 opacity-100 sm:translate-y-0" |
||||
|
leave-active-class="transition ease-in duration-500" |
||||
|
leave-from-class="opacity-100" |
||||
|
leave-to-class="opacity-0" |
||||
|
> |
||||
|
<div |
||||
|
v-if="isVisible" |
||||
|
class="fixed z-[100] top-0 inset-x-0 w-full absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50" |
||||
|
> |
||||
|
<div |
||||
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg" |
||||
|
> |
||||
|
<div class="w-full px-6 py-6 text-slate-900 text-center"> |
||||
|
<p v-if="serviceWorkerReady && vapidKey" class="text-lg mb-4"> |
||||
|
Would you like to be notified of new activity once a day? |
||||
|
</p> |
||||
|
<p v-else class="text-lg mb-4"> |
||||
|
Waiting for system initialization, which may take up to 10 |
||||
|
seconds... |
||||
|
<fa icon="spinner" spin /> |
||||
|
</p> |
||||
|
|
||||
|
<div v-if="serviceWorkerReady && vapidKey"> |
||||
|
<span class="flex flex-row justify-center"> |
||||
|
<span class="mt-2">Yes, tell me at: </span> |
||||
|
<input |
||||
|
type="number" |
||||
|
@change="checkHourInput" |
||||
|
class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20" |
||||
|
v-model="hourInput" |
||||
|
/> |
||||
|
<input |
||||
|
type="number" |
||||
|
@change="checkMinuteInput" |
||||
|
class="border border-slate-400 mt-2 px-2 py-2 text-center w-20" |
||||
|
v-model="minuteInput" |
||||
|
/> |
||||
|
<span |
||||
|
class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20" |
||||
|
@click="hourAm = !hourAm" |
||||
|
> |
||||
|
<span v-if="hourAm"> AM <fa icon="chevron-down" /> </span> |
||||
|
<span v-else> PM <fa icon="chevron-up" /> </span> |
||||
|
</span> |
||||
|
</span> |
||||
|
<button |
||||
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white mt-2 px-2 py-2 rounded-md" |
||||
|
@click=" |
||||
|
close(); |
||||
|
turnOnNotifications(); |
||||
|
" |
||||
|
> |
||||
|
Turn on Daily Message |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<button |
||||
|
@click="close()" |
||||
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md" |
||||
|
> |
||||
|
No, Not Now |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</transition> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from "vue-facing-decorator"; |
||||
|
|
||||
|
import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app"; |
||||
|
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "@/db/index"; |
||||
|
import { urlBase64ToUint8Array } from "@/libs/crypto/vc/util"; |
||||
|
import * as libsUtil from "@/libs/util"; |
||||
|
|
||||
|
// Example interface for error |
||||
|
interface ErrorResponse { |
||||
|
message: string; |
||||
|
} |
||||
|
|
||||
|
// PushSubscriptionJSON is defined in the Push API https://www.w3.org/TR/push-api/#dom-pushsubscriptionjson |
||||
|
interface PushSubscriptionWithTime extends PushSubscriptionJSON { |
||||
|
notifyTime: { utcHour: number; minute: number }; |
||||
|
notifyType: string; |
||||
|
} |
||||
|
|
||||
|
interface ServiceWorkerMessage { |
||||
|
type: string; |
||||
|
data: string; |
||||
|
} |
||||
|
|
||||
|
interface ServiceWorkerResponse { |
||||
|
// Define the properties and their types |
||||
|
success: boolean; |
||||
|
message?: string; |
||||
|
} |
||||
|
|
||||
|
interface VapidResponse { |
||||
|
data: { |
||||
|
vapidKey: string; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
@Component |
||||
|
export default class PushNotificationPermission extends Vue { |
||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void; |
||||
|
|
||||
|
hourAm = true; |
||||
|
hourInput = "8"; |
||||
|
isVisible = false; |
||||
|
minuteInput = "00"; |
||||
|
serviceWorkerReady = false; |
||||
|
vapidKey = ""; |
||||
|
|
||||
|
async open() { |
||||
|
this.isVisible = true; |
||||
|
try { |
||||
|
const settings = await retrieveSettingsForActiveAccount(); |
||||
|
let pushUrl = DEFAULT_PUSH_SERVER; |
||||
|
if (settings?.webPushServer) { |
||||
|
pushUrl = settings.webPushServer; |
||||
|
} |
||||
|
|
||||
|
if (pushUrl.startsWith("http://localhost")) { |
||||
|
logConsoleAndDb("Not checking for VAPID in this local environment."); |
||||
|
} else { |
||||
|
let responseData = ""; |
||||
|
await this.axios |
||||
|
.get(pushUrl + "/web-push/vapid") |
||||
|
.then((response: VapidResponse) => { |
||||
|
this.vapidKey = response.data?.vapidKey || ""; |
||||
|
logConsoleAndDb("Got vapid key: " + this.vapidKey); |
||||
|
responseData = JSON.stringify(response.data); |
||||
|
navigator.serviceWorker?.addEventListener( |
||||
|
"controllerchange", |
||||
|
() => { |
||||
|
logConsoleAndDb( |
||||
|
"New service worker is now controlling the page", |
||||
|
); |
||||
|
}, |
||||
|
); |
||||
|
}); |
||||
|
if (!this.vapidKey) { |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error Setting Notifications", |
||||
|
text: "Could not set notifications.", |
||||
|
}, |
||||
|
5000, |
||||
|
); |
||||
|
logConsoleAndDb( |
||||
|
"Error Setting Notifications: web push server response didn't have vapidKey: " + |
||||
|
responseData, |
||||
|
true, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} catch (error) { |
||||
|
if (window.location.host.startsWith("localhost")) { |
||||
|
logConsoleAndDb( |
||||
|
"Ignoring the error getting VAPID for local development.", |
||||
|
); |
||||
|
} else { |
||||
|
logConsoleAndDb( |
||||
|
"Got an error initializing notifications: " + JSON.stringify(error), |
||||
|
true, |
||||
|
); |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error Setting Notifications", |
||||
|
text: "Got an error setting notifications.", |
||||
|
}, |
||||
|
5000, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
// there may be a long pause here on first initialization |
||||
|
navigator.serviceWorker?.ready.then(() => { |
||||
|
this.serviceWorkerReady = true; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private close() { |
||||
|
this.isVisible = false; |
||||
|
} |
||||
|
|
||||
|
private sendMessageToServiceWorker( |
||||
|
message: ServiceWorkerMessage, |
||||
|
): Promise<unknown> { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
if (navigator.serviceWorker?.controller) { |
||||
|
const messageChannel = new MessageChannel(); |
||||
|
|
||||
|
messageChannel.port1.onmessage = (event: MessageEvent) => { |
||||
|
if (event.data.error) { |
||||
|
reject(event.data.error as ErrorResponse); |
||||
|
} else { |
||||
|
resolve(event.data as ServiceWorkerResponse); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
navigator.serviceWorker?.controller.postMessage(message, [ |
||||
|
messageChannel.port2, |
||||
|
]); |
||||
|
} else { |
||||
|
reject("Service worker controller not available"); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private askPermission(): Promise<NotificationPermission> { |
||||
|
logConsoleAndDb( |
||||
|
"Requesting permission for notifications: " + JSON.stringify(navigator), |
||||
|
); |
||||
|
if ( |
||||
|
!("serviceWorker" in navigator && navigator.serviceWorker?.controller) |
||||
|
) { |
||||
|
return Promise.reject("Service worker not available."); |
||||
|
} |
||||
|
|
||||
|
const secret = localStorage.getItem("secret"); |
||||
|
if (!secret) { |
||||
|
return Promise.reject("No secret found."); |
||||
|
} |
||||
|
|
||||
|
return this.sendSecretToServiceWorker(secret) |
||||
|
.then(() => this.checkNotificationSupport()) |
||||
|
.then(() => this.requestNotificationPermission()) |
||||
|
.catch((error) => Promise.reject(error)); |
||||
|
} |
||||
|
|
||||
|
private sendSecretToServiceWorker(secret: string): Promise<void> { |
||||
|
const message: ServiceWorkerMessage = { |
||||
|
type: "SEND_LOCAL_DATA", |
||||
|
data: secret, |
||||
|
}; |
||||
|
|
||||
|
return this.sendMessageToServiceWorker(message).then((response) => { |
||||
|
logConsoleAndDb( |
||||
|
"Response from service worker: " + JSON.stringify(response), |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private checkNotificationSupport(): Promise<void> { |
||||
|
if (!("Notification" in window)) { |
||||
|
alert("This browser does not support notifications."); |
||||
|
return Promise.reject("This browser does not support notifications."); |
||||
|
} |
||||
|
if (window.Notification.permission === "granted") { |
||||
|
return Promise.resolve(); |
||||
|
} |
||||
|
return Promise.resolve(); |
||||
|
} |
||||
|
|
||||
|
private requestNotificationPermission(): Promise<NotificationPermission> { |
||||
|
return window.Notification.requestPermission().then( |
||||
|
(permission: string) => { |
||||
|
if (permission !== "granted") { |
||||
|
alert( |
||||
|
"Allow this app permission to make notifications for personal reminders." + |
||||
|
" You can adjust them at any time in your settings.", |
||||
|
); |
||||
|
throw new Error("We weren't granted permission."); |
||||
|
} |
||||
|
return permission; |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private checkHourInput() { |
||||
|
const hourNum = parseInt(this.hourInput); |
||||
|
if (isNaN(hourNum)) { |
||||
|
this.hourInput = "12"; |
||||
|
} else if (hourNum < 1) { |
||||
|
this.hourInput = "12"; |
||||
|
this.hourAm = !this.hourAm; |
||||
|
} else if (hourNum > 12) { |
||||
|
this.hourInput = "1"; |
||||
|
this.hourAm = !this.hourAm; |
||||
|
} else { |
||||
|
this.hourInput = hourNum.toString(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private checkMinuteInput() { |
||||
|
const minuteNum = parseInt(this.minuteInput); |
||||
|
if (isNaN(minuteNum)) { |
||||
|
this.minuteInput = "00"; |
||||
|
} else if (minuteNum < 0) { |
||||
|
this.minuteInput = "59"; |
||||
|
} else if (minuteNum < 10) { |
||||
|
this.minuteInput = "0" + minuteNum; |
||||
|
} else if (minuteNum > 59) { |
||||
|
this.minuteInput = "00"; |
||||
|
} else { |
||||
|
this.minuteInput = minuteNum.toString(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async turnOnNotifications() { |
||||
|
return this.askPermission() |
||||
|
.then((permission) => { |
||||
|
logConsoleAndDb("Permission granted: " + JSON.stringify(permission)); |
||||
|
|
||||
|
// Call the function and handle promises |
||||
|
return this.subscribeToPush(); |
||||
|
}) |
||||
|
.then(() => { |
||||
|
logConsoleAndDb("Subscribed successfully."); |
||||
|
return navigator.serviceWorker?.ready; |
||||
|
}) |
||||
|
.then((registration) => { |
||||
|
return registration.pushManager.getSubscription(); |
||||
|
}) |
||||
|
.then(async (subscription) => { |
||||
|
if (subscription) { |
||||
|
await this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "info", |
||||
|
title: "Notification Setup Underway", |
||||
|
text: "Setting up notifications for interesting activity, which takes about 10 seconds. If you don't see a final confirmation, check the 'Troubleshoot' page.", |
||||
|
}, |
||||
|
-1, |
||||
|
); |
||||
|
// we already checked that this is a valid hour number |
||||
|
const rawHourNum = libsUtil.numberOrZero(this.hourInput); |
||||
|
const adjHourNum = this.hourAm |
||||
|
? // If it's AM, then we'll change it to 0 for 12 AM but otherwise use rawHourNum |
||||
|
rawHourNum === 12 |
||||
|
? 0 |
||||
|
: rawHourNum |
||||
|
: // Otherwise it's PM, so keep a 12 but otherwise add 12 |
||||
|
rawHourNum === 12 |
||||
|
? 12 |
||||
|
: rawHourNum + 12; |
||||
|
const hourNum = adjHourNum % 24; // probably unnecessary now |
||||
|
const utcHour = |
||||
|
hourNum + Math.round(new Date().getTimezoneOffset() / 60); |
||||
|
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24; |
||||
|
const minuteNum = libsUtil.numberOrZero(this.minuteInput); |
||||
|
const utcMinute = |
||||
|
minuteNum + Math.round(new Date().getTimezoneOffset() % 60); |
||||
|
const finalUtcMinute = (utcMinute + (utcMinute < 0 ? 60 : 0)) % 60; |
||||
|
|
||||
|
const subscriptionWithTime: PushSubscriptionWithTime = { |
||||
|
notifyTime: { utcHour: finalUtcHour, minute: finalUtcMinute }, |
||||
|
notifyType: "DAILY_CHECK", |
||||
|
...subscription.toJSON(), |
||||
|
}; |
||||
|
await this.sendSubscriptionToServer(subscriptionWithTime); |
||||
|
// To help investigate potential issues with this: https://firebase.google.com/docs/cloud-messaging/migrate-v1 |
||||
|
logConsoleAndDb( |
||||
|
"Subscription data sent to server with endpoint: " + |
||||
|
subscription.endpoint, |
||||
|
); |
||||
|
return subscriptionWithTime; |
||||
|
} else { |
||||
|
throw new Error("Subscription object is not available."); |
||||
|
} |
||||
|
}) |
||||
|
.then(async (subscription: PushSubscriptionWithTime) => { |
||||
|
logConsoleAndDb( |
||||
|
"Subscription data sent to server and all finished successfully.", |
||||
|
); |
||||
|
await libsUtil.sendTestThroughPushServer(subscription, true); |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "success", |
||||
|
title: "Notifications Turned On", |
||||
|
text: "Notifications are on. You should see at least one on your device; if not, check the 'Troubleshoot' page.", |
||||
|
}, |
||||
|
-1, |
||||
|
); |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
logConsoleAndDb( |
||||
|
"Got an error setting notification permissions: " + |
||||
|
" string " + |
||||
|
error.toString() + |
||||
|
" JSON " + |
||||
|
JSON.stringify(error), |
||||
|
true, |
||||
|
); |
||||
|
alert("Some error occurred setting notification permissions."); |
||||
|
// unsubscribe just in case we failed after getting a subscription |
||||
|
navigator.serviceWorker?.ready |
||||
|
.then((registration) => registration.pushManager.getSubscription()) |
||||
|
.then((subscription) => { |
||||
|
if (subscription) { |
||||
|
subscription.unsubscribe(); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private subscribeToPush(): Promise<void> { |
||||
|
return new Promise<void>((resolve, reject) => { |
||||
|
if (!("serviceWorker" in navigator && "PushManager" in window)) { |
||||
|
const errorMsg = "Push messaging is not supported"; |
||||
|
console.warn(errorMsg); |
||||
|
return reject(new Error(errorMsg)); |
||||
|
} |
||||
|
|
||||
|
if (window.Notification.permission !== "granted") { |
||||
|
const errorMsg = "Notification permission not granted"; |
||||
|
console.warn(errorMsg); |
||||
|
return reject(new Error(errorMsg)); |
||||
|
} |
||||
|
|
||||
|
const applicationServerKey = urlBase64ToUint8Array(this.vapidKey); |
||||
|
const options: PushSubscriptionOptions = { |
||||
|
userVisibleOnly: true, |
||||
|
applicationServerKey: applicationServerKey, |
||||
|
}; |
||||
|
|
||||
|
navigator.serviceWorker?.ready |
||||
|
.then((registration) => { |
||||
|
return registration.pushManager.subscribe(options); |
||||
|
}) |
||||
|
.then((subscription) => { |
||||
|
logConsoleAndDb( |
||||
|
"Push subscription successful: " + JSON.stringify(subscription), |
||||
|
); |
||||
|
resolve(); |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
logConsoleAndDb( |
||||
|
"Push subscription failed: " + |
||||
|
JSON.stringify(error) + |
||||
|
" - " + |
||||
|
JSON.stringify(options), |
||||
|
true, |
||||
|
); |
||||
|
|
||||
|
// Inform the user about the issue |
||||
|
alert( |
||||
|
"We encountered an issue setting up push notifications. " + |
||||
|
"If you wish to revoke notification permissions, please do so in your browser settings.", |
||||
|
); |
||||
|
|
||||
|
reject(error); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private sendSubscriptionToServer( |
||||
|
subscription: PushSubscriptionWithTime, |
||||
|
): Promise<void> { |
||||
|
logConsoleAndDb("About to send subscription... " + subscription); |
||||
|
return fetch("/web-push/subscribe", { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
}, |
||||
|
body: JSON.stringify(subscription), |
||||
|
}).then((response) => { |
||||
|
if (!response.ok) { |
||||
|
console.error("Bad response subscribing to web push: ", response); |
||||
|
throw new Error("Failed to send push subscription to server"); |
||||
|
} |
||||
|
logConsoleAndDb("Push subscription sent to server successfully."); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Add any specific styles for this component here */ |
||||
|
</style> |
Loading…
Reference in new issue