forked from jsnbuchanan/crowd-funder-for-time-pwa
Merge pull request 'web-push-permissions' (#67) from web-push-permissions into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#67
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ node_modules
|
|||||||
signature.bin
|
signature.bin
|
||||||
*.pem
|
*.pem
|
||||||
verified.txt
|
verified.txt
|
||||||
|
myenv
|
||||||
|
|
||||||
*~
|
*~
|
||||||
# local env files
|
# local env files
|
||||||
|
|||||||
30
additional-scripts.js
Normal file
30
additional-scripts.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
|
||||||
|
self.addEventListener("push", function (event) {
|
||||||
|
let payload;
|
||||||
|
if (event.data) {
|
||||||
|
payload = JSON.parse(event.data.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = payload ? payload.title : "Custom Title";
|
||||||
|
const options = {
|
||||||
|
body: payload ? payload.body : "Custom body text",
|
||||||
|
icon: payload ? payload.icon : "icon.png",
|
||||||
|
badge: payload ? payload.badge : "badge.png",
|
||||||
|
};
|
||||||
|
|
||||||
|
event.waitUntil(self.registration.showNotification(title, options));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
self.addEventListener('message', function(event) {
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
switch (data.command) {
|
||||||
|
case 'account':
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('Unknown command:', data.command);
|
||||||
|
}
|
||||||
|
});
|
||||||
175
src/App.vue
175
src/App.vue
@@ -162,17 +162,22 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
|
@click="
|
||||||
|
close(notification.id);
|
||||||
|
turnOnNotifications();
|
||||||
|
"
|
||||||
>
|
>
|
||||||
Turn on Notifications
|
Turn on Notifications
|
||||||
</button>
|
</button>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
@click="close(notification.id)"
|
@click="maybeLater(notification.id)"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Maybe Later
|
Maybe Later
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
@click="never(notification.id)"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
|
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Never
|
Never
|
||||||
@@ -254,4 +259,170 @@
|
|||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|
||||||
<script lang="ts"></script>
|
<script lang="ts">
|
||||||
|
import { Vue, Component } from "vue-facing-decorator";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class App extends Vue {
|
||||||
|
b64 = "";
|
||||||
|
mounted() {
|
||||||
|
axios
|
||||||
|
.get("https://timesafari-pwa.anomalistlabs.com/web-push/vapid")
|
||||||
|
.then((response) => {
|
||||||
|
this.b64 = response.data.vapidKey;
|
||||||
|
console.log(this.b64);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("API error", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private askPermission(): Promise<NotificationPermission> {
|
||||||
|
// Check if Notifications are supported
|
||||||
|
if (!("Notification" in window)) {
|
||||||
|
alert("This browser does not support notifications.");
|
||||||
|
return Promise.reject("This browser does not support notifications.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check existing permissions
|
||||||
|
if (Notification.permission === "granted") {
|
||||||
|
return Promise.resolve("granted");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request permission
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const permissionResult = Notification.requestPermission((result) => {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (permissionResult) {
|
||||||
|
permissionResult.then(resolve, reject);
|
||||||
|
}
|
||||||
|
}).then((permissionResult) => {
|
||||||
|
console.log("Permission result:", permissionResult);
|
||||||
|
|
||||||
|
if (permissionResult !== "granted") {
|
||||||
|
alert("We need notification permission to provide certain features.");
|
||||||
|
return Promise.reject("We weren't granted permission.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissionResult;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async turnOnNotifications() {
|
||||||
|
return this.askPermission()
|
||||||
|
.then((permission) => {
|
||||||
|
console.log("Permission granted:", permission);
|
||||||
|
|
||||||
|
// Call the function and handle promises
|
||||||
|
this.subscribeToPush()
|
||||||
|
.then(() => {
|
||||||
|
console.log("Subscribed successfully.");
|
||||||
|
// Assuming the subscription object is available
|
||||||
|
return navigator.serviceWorker.ready;
|
||||||
|
})
|
||||||
|
.then((registration) => {
|
||||||
|
// Fetch the existing subscription object from the registration
|
||||||
|
return registration.pushManager.getSubscription();
|
||||||
|
})
|
||||||
|
.then((subscription) => {
|
||||||
|
if (subscription) {
|
||||||
|
console.log(subscription);
|
||||||
|
return this.sendSubscriptionToServer(subscription);
|
||||||
|
} else {
|
||||||
|
throw new Error("Subscription object is not available.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log("Subscription data sent to server.");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(
|
||||||
|
"Subscription or server communication failed:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("An error occurred:", error);
|
||||||
|
// Handle error appropriately here
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to convert URL base64 to Uint8Array
|
||||||
|
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The subscribeToPush method
|
||||||
|
private subscribeToPush(): Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if ("serviceWorker" in navigator && "PushManager" in window) {
|
||||||
|
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
|
||||||
|
const options: PushSubscriptionOptions = {
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: applicationServerKey,
|
||||||
|
};
|
||||||
|
console.log(options);
|
||||||
|
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then((registration) => {
|
||||||
|
return registration.pushManager.subscribe(options);
|
||||||
|
})
|
||||||
|
.then((subscription) => {
|
||||||
|
console.log("Push subscription successful:", subscription);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Push subscription failed:", error, options);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const errorMsg = "Push messaging is not supported";
|
||||||
|
console.warn(errorMsg);
|
||||||
|
reject(new Error(errorMsg));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendSubscriptionToServer(
|
||||||
|
subscription: PushSubscription,
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(subscription);
|
||||||
|
return fetch("/web-push/subscribe", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(subscription),
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to send subscription to server");
|
||||||
|
}
|
||||||
|
console.log("Subscription sent to server successfully.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
never(ID: string) {
|
||||||
|
alert(ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeLater(ID: string) {
|
||||||
|
alert(ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* See also ../libs/veramo/setup.ts
|
* See also ../libs/veramo/setup.ts
|
||||||
*/
|
*/
|
||||||
export enum AppString {
|
export enum AppString {
|
||||||
APP_NAME = "Time Safari",
|
APP_NAME = "TimeSafari",
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||||
|
|||||||
@@ -9,59 +9,39 @@ import {
|
|||||||
} from "./tables/settings";
|
} from "./tables/settings";
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
|
|
||||||
// a separate DB because the seed is super-sensitive data
|
// Define types for tables that hold sensitive and non-sensitive data
|
||||||
type SensitiveTables = {
|
type SensitiveTables = { accounts: Table<Account> };
|
||||||
accounts: Table<Account>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NonsensitiveTables = {
|
type NonsensitiveTables = {
|
||||||
contacts: Table<Contact>;
|
contacts: Table<Contact>;
|
||||||
settings: Table<Settings>;
|
settings: Table<Settings>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
||||||
* In order to make the next line be acceptable, the program needs to have its linter suppress a rule:
|
|
||||||
* https://typescript-eslint.io/rules/no-unnecessary-type-constraint/
|
|
||||||
*
|
|
||||||
* and change *any* to *unknown*
|
|
||||||
*
|
|
||||||
* https://9to5answer.com/how-to-bypass-warning-unexpected-any-specify-a-different-type-typescript-eslint-no-explicit-any
|
|
||||||
*/
|
|
||||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||||
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
|
||||||
const SensitiveSchemas = Object.assign({}, AccountsSchema);
|
|
||||||
|
|
||||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||||
BaseDexie & T;
|
BaseDexie & T;
|
||||||
|
|
||||||
|
// Initialize Dexie databases for sensitive and non-sensitive data
|
||||||
|
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
||||||
|
const SensitiveSchemas = { ...AccountsSchema };
|
||||||
|
|
||||||
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||||
const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
|
const NonsensitiveSchemas = { ...ContactsSchema, ...SettingsSchema };
|
||||||
|
|
||||||
/**
|
// Manage the encryption key. If not present in localStorage, create and store it.
|
||||||
* Needed to enable a special webpack setting to allow *await* below:
|
|
||||||
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create password and place password in localStorage.
|
|
||||||
*
|
|
||||||
* It's good practice to keep the data encrypted at rest, so we'll do that even
|
|
||||||
* if the secret is stored right next to the app.
|
|
||||||
*/
|
|
||||||
const secret =
|
const secret =
|
||||||
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
|
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
|
||||||
|
if (!localStorage.getItem("secret")) localStorage.setItem("secret", secret);
|
||||||
|
|
||||||
if (localStorage.getItem("secret") == null) {
|
// Apply encryption to the sensitive database using the secret key
|
||||||
localStorage.setItem("secret", secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypted(accountsDB, { secretKey: secret });
|
encrypted(accountsDB, { secretKey: secret });
|
||||||
accountsDB.version(1).stores(SensitiveSchemas);
|
|
||||||
|
|
||||||
|
// Define the schema for our databases
|
||||||
|
accountsDB.version(1).stores(SensitiveSchemas);
|
||||||
db.version(1).stores(NonsensitiveSchemas);
|
db.version(1).stores(NonsensitiveSchemas);
|
||||||
|
|
||||||
// initialize, a la https://dexie.org/docs/Tutorial/Design#the-populate-event
|
// Event handler to initialize the non-sensitive database with default settings
|
||||||
db.on("populate", function () {
|
db.on("populate", () => {
|
||||||
// ensure there's an initial entry for settings
|
|
||||||
db.settings.add({
|
db.settings.add({
|
||||||
id: MASTER_SETTINGS_KEY,
|
id: MASTER_SETTINGS_KEY,
|
||||||
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
||||||
|
|||||||
@@ -1,29 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* BoundingBox type describes the geographical bounding box coordinates.
|
||||||
|
*/
|
||||||
export type BoundingBox = {
|
export type BoundingBox = {
|
||||||
eastLong: number;
|
eastLong: number; // Eastern longitude
|
||||||
maxLat: number;
|
maxLat: number; // Maximum (Northernmost) latitude
|
||||||
minLat: number;
|
minLat: number; // Minimum (Southernmost) latitude
|
||||||
westLong: number;
|
westLong: number; // Western longitude
|
||||||
};
|
};
|
||||||
|
|
||||||
// a singleton
|
/**
|
||||||
|
* Settings type encompasses user-specific configuration details.
|
||||||
|
*/
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
id: number; // there's only one entry: MASTER_SETTINGS_KEY
|
id: number; // Only one entry using MASTER_SETTINGS_KEY
|
||||||
|
activeDid?: string; // Active Decentralized ID
|
||||||
|
apiServer?: string; // API server URL
|
||||||
|
firstName?: string; // User's first name
|
||||||
|
lastName?: string; // User's last name
|
||||||
|
lastViewedClaimId?: string; // Last viewed claim ID
|
||||||
|
lastNotifiedClaimId?: string; // Last notified claim ID
|
||||||
|
|
||||||
activeDid?: string;
|
// Array of named search boxes defined by bounding boxes
|
||||||
apiServer?: string;
|
|
||||||
firstName?: string;
|
|
||||||
isRegistered?: boolean;
|
|
||||||
lastName?: string; // deprecated, pre v 0.1.3
|
|
||||||
lastViewedClaimId?: string;
|
|
||||||
searchBoxes?: Array<{
|
searchBoxes?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
bbox: BoundingBox;
|
bbox: BoundingBox;
|
||||||
}>;
|
}>;
|
||||||
showContactGivesInline?: boolean;
|
|
||||||
|
showContactGivesInline?: boolean; // Display contact inline or not
|
||||||
|
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
||||||
|
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
|
||||||
|
reminderOn?: boolean; // Toggle to enable or disable reminders
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema for the Settings table in the database.
|
||||||
|
*/
|
||||||
export const SettingsSchema = {
|
export const SettingsSchema = {
|
||||||
settings: "id",
|
settings: "id",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants.
|
||||||
|
*/
|
||||||
export const MASTER_SETTINGS_KEY = 1;
|
export const MASTER_SETTINGS_KEY = 1;
|
||||||
|
|||||||
@@ -11,5 +11,8 @@ module.exports = defineConfig({
|
|||||||
iconPaths: {
|
iconPaths: {
|
||||||
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
||||||
},
|
},
|
||||||
|
workboxOptions: {
|
||||||
|
importScripts: ["additional-scripts.js"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user