forked from trent_larson/crowd-funder-for-time-pwa
Compare commits
2 Commits
web-push-p
...
why-migrat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa6cf0c9f6 | ||
| 99db5deb77 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,7 +4,6 @@ node_modules
|
|||||||
signature.bin
|
signature.bin
|
||||||
*.pem
|
*.pem
|
||||||
verified.txt
|
verified.txt
|
||||||
myenv
|
|
||||||
|
|
||||||
*~
|
*~
|
||||||
# local env files
|
# local env files
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
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,22 +162,17 @@
|
|||||||
|
|
||||||
<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="maybeLater(notification.id)"
|
@click="close(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
|
||||||
@@ -259,170 +254,4 @@
|
|||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts"></script>
|
||||||
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>
|
|
||||||
|
|||||||
@@ -6,42 +6,87 @@ import {
|
|||||||
MASTER_SETTINGS_KEY,
|
MASTER_SETTINGS_KEY,
|
||||||
Settings,
|
Settings,
|
||||||
SettingsSchema,
|
SettingsSchema,
|
||||||
|
SettingsSchemaV1,
|
||||||
} from "./tables/settings";
|
} from "./tables/settings";
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
|
|
||||||
// Define types for tables that hold sensitive and non-sensitive data
|
// a separate DB because the seed is super-sensitive data
|
||||||
type SensitiveTables = { accounts: Table<Account> };
|
type SensitiveTables = {
|
||||||
|
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 = { ...ContactsSchema, ...SettingsSchema };
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
const NonsensitiveSchemasV1 = Object.assign({}, ContactsSchema, SettingsSchemaV1);
|
||||||
|
const NonsensitiveSchemas = Object.assign({}, 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);
|
|
||||||
|
|
||||||
// Apply encryption to the sensitive database using the secret key
|
if (localStorage.getItem("secret") == null) {
|
||||||
|
localStorage.setItem("secret", secret);
|
||||||
|
}
|
||||||
|
|
||||||
encrypted(accountsDB, { secretKey: secret });
|
encrypted(accountsDB, { secretKey: secret });
|
||||||
|
|
||||||
// Define the schema for our databases
|
|
||||||
accountsDB.version(1).stores(SensitiveSchemas);
|
accountsDB.version(1).stores(SensitiveSchemas);
|
||||||
db.version(1).stores(NonsensitiveSchemas);
|
|
||||||
|
|
||||||
// Event handler to initialize the non-sensitive database with default settings
|
db.version(1).stores(NonsensitiveSchemasV1);
|
||||||
db.on("populate", () => {
|
|
||||||
|
db.version(2)
|
||||||
|
.stores(NonsensitiveSchemas)
|
||||||
|
.upgrade((tx) => {
|
||||||
|
return tx
|
||||||
|
.table("settings")
|
||||||
|
.toCollection()
|
||||||
|
.modify((settings) => {
|
||||||
|
if (
|
||||||
|
typeof settings.firstName === "string" &&
|
||||||
|
typeof settings.lastName === "string"
|
||||||
|
) {
|
||||||
|
settings.firstName += " " + settings.lastName;
|
||||||
|
} else if (typeof settings.lastName === "string") {
|
||||||
|
settings.firstName = settings.lastName;
|
||||||
|
}
|
||||||
|
delete settings.lastName;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log("caught modify exception", e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// initialize, a la https://dexie.org/docs/Tutorial/Design#the-populate-event
|
||||||
|
db.on("populate", function () {
|
||||||
|
// 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,46 +1,34 @@
|
|||||||
/**
|
|
||||||
* BoundingBox type describes the geographical bounding box coordinates.
|
|
||||||
*/
|
|
||||||
export type BoundingBox = {
|
export type BoundingBox = {
|
||||||
eastLong: number; // Eastern longitude
|
eastLong: number;
|
||||||
maxLat: number; // Maximum (Northernmost) latitude
|
maxLat: number;
|
||||||
minLat: number; // Minimum (Southernmost) latitude
|
minLat: number;
|
||||||
westLong: number; // Western longitude
|
westLong: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// a singleton
|
||||||
* Settings type encompasses user-specific configuration details.
|
|
||||||
*/
|
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
id: number; // Only one entry using MASTER_SETTINGS_KEY
|
id: number; // there's only one entry: 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
|
|
||||||
|
|
||||||
// Array of named search boxes defined by bounding boxes
|
|
||||||
|
|
||||||
|
activeDid?: string;
|
||||||
|
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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const SettingsSchemaV1 = {
|
||||||
* Schema for the Settings table in the database.
|
|
||||||
*/
|
|
||||||
export const SettingsSchema = {
|
|
||||||
settings: "id",
|
settings: "id",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const SettingsSchema = {
|
||||||
* Constants.
|
settings:
|
||||||
*/
|
"id, activeDid, apiServer, firstName, lastname, lastViewedClaimId, searchBoxes, showContactGivesInline",
|
||||||
|
};
|
||||||
|
|
||||||
export const MASTER_SETTINGS_KEY = 1;
|
export const MASTER_SETTINGS_KEY = 1;
|
||||||
|
|||||||
@@ -11,8 +11,5 @@ 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