Trent Larson
1 week 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