Compare commits

..

2 Commits

Author SHA1 Message Date
Matthew Aaron Raymer
aa6cf0c9f6 Merge branch 'master' into why-migrate-fail 2023-11-08 14:51:15 +08:00
99db5deb77 attempt migration to consolidate name fields, but it fails with these errors:
- Transaction aborted
- Not yet support for changing primary key
- TypeError: WeakMap key must be an object, got pr
2023-11-05 15:43:57 -07:00
17 changed files with 134 additions and 10442 deletions

1
.gitignore vendored
View File

@@ -4,7 +4,6 @@ node_modules
signature.bin
*.pem
verified.txt
myenv
*~
# local env files

143
package-lock.json generated
View File

@@ -1,18 +1,17 @@
{
"name": "kickstart-for-time-pwa",
"version": "0.1.3",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kickstart-for-time-pwa",
"version": "0.1.3",
"version": "0.1.0",
"dependencies": {
"@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@lionello/secp256k1-js": "^1.1.0",
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0",
"@veramo/core": "^5.4.1",
@@ -31,13 +30,11 @@
"dexie": "^3.2.4",
"dexie-export-import": "^4.0.7",
"did-jwt": "^7.2.7",
"elliptic": "^6.5.4",
"ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.1.2",
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9",
"jssha": "^3.3.1",
"localstorage-slim": "^2.5.0",
"luxon": "^3.4.3",
"merkletreejs": "^0.3.10",
@@ -55,7 +52,6 @@
"vue": "^3.3.4",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.2",
"vue-qrcode-reader": "^5.4.1",
"vue-router": "^4.2.4",
"web-did-resolver": "^2.0.27"
},
@@ -75,13 +71,13 @@
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.15",
"eslint": "^8.53.0",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"leaflet": "^1.9.4",
"postcss": "^8.4.29",
"prettier": "^3.1.0",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.3",
"typescript": "~5.2.2"
}
@@ -2824,9 +2820,9 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz",
"integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
"integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
@@ -2874,9 +2870,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz",
"integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz",
"integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -5501,12 +5497,12 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.1",
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^3.0.5"
},
@@ -5528,9 +5524,9 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
"node_modules/@jest/create-cache-key-function": {
@@ -6061,22 +6057,6 @@
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
"dev": true
},
"node_modules/@lionello/secp256k1-js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lionello/secp256k1-js/-/secp256k1-js-1.1.0.tgz",
"integrity": "sha512-frvrdwwgWm0gq43rYcGwwZee+k5sX9HfWnB80740h1dvRT0FO0Z6Wt5C5I7bS0dXKy1pt6IomkR1VcXirP+g4Q==",
"dependencies": {
"bn.js": "^4.11.8"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@lionello/secp256k1-js/node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"node_modules/@noble/ciphers": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.3.0.tgz",
@@ -8696,16 +8676,6 @@
"@types/node": "*"
}
},
"node_modules/@types/dom-webcodecs": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.10.tgz",
"integrity": "sha512-qQfLMw4yhtagKQApMQKaf21KZeJu3Psysbm/wLQ3mkpyBWY3x3dHCKFcYs43WEH+s8zgTSF0DvJUPWTtyZP0Dw=="
},
"node_modules/@types/emscripten": {
"version": "1.39.10",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz",
"integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw=="
},
"node_modules/@types/eslint": {
"version": "8.44.4",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.4.tgz",
@@ -9225,12 +9195,6 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
"dev": true
},
"node_modules/@unimodules/core": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@unimodules/core/-/core-7.1.2.tgz",
@@ -11551,15 +11515,6 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/barcode-detector": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.1.1.tgz",
"integrity": "sha512-yA6gR5u5j22uw2eHSlFGzhYgnnQqx6hc4amDb/r0bKWl2gcDOqVE6SzUE6O87UzJ3ZhjJjM9uG/L9+D705HsKg==",
"dependencies": {
"@types/dom-webcodecs": "^0.1.9",
"zxing-wasm": "1.0.0-rc.4"
}
},
"node_modules/base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
@@ -14245,19 +14200,18 @@
}
},
"node_modules/eslint": {
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz",
"integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz",
"integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.3",
"@eslint/js": "8.53.0",
"@humanwhocodes/config-array": "^0.11.13",
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "8.51.0",
"@humanwhocodes/config-array": "^0.11.11",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@@ -18885,14 +18839,6 @@
"node": ">=0.10.0"
}
},
"node_modules/jssha": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz",
"integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==",
"engines": {
"node": "*"
}
},
"node_modules/keccak": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz",
@@ -23171,9 +23117,9 @@
}
},
"node_modules/prettier": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
@@ -24691,11 +24637,6 @@
"resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
"integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="
},
"node_modules/sdp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
},
"node_modules/secp256k1": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz",
@@ -27323,18 +27264,6 @@
"node": ">=8"
}
},
"node_modules/vue-qrcode-reader": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.4.1.tgz",
"integrity": "sha512-jwETIaRdumSCnXOpp0BkpZW8sySNFUfIPNOFa8oHAEmoSSdKK/ub5C1+3vMwokjU8iNERR2v/YhfBdcWDe0s5A==",
"dependencies": {
"barcode-detector": "2.1.1",
"webrtc-adapter": "8.2.3"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/vue-router": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",
@@ -27910,18 +27839,6 @@
"node": ">=6"
}
},
"node_modules/webrtc-adapter": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz",
"integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==",
"dependencies": {
"sdp": "^3.2.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/websocket-driver": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
@@ -28727,14 +28644,6 @@
"engines": {
"node": ">=14"
}
},
"node_modules/zxing-wasm": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.0.0-rc.4.tgz",
"integrity": "sha512-SvVErHUZhzFqpqA2vpwmXeAPa6sgGdUCOkMCd5cMch6L1urZbZCZR8jb2+NI9bCfJRNkQi2ZjME9/NaiUFiSGg==",
"dependencies": {
"@types/emscripten": "^1.39.9"
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "kickstart-for-time-pwa",
"version": "0.1.3",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@@ -12,7 +12,6 @@
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@lionello/secp256k1-js": "^1.1.0",
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0",
"@veramo/core": "^5.4.1",
@@ -31,13 +30,11 @@
"dexie": "^3.2.4",
"dexie-export-import": "^4.0.7",
"did-jwt": "^7.2.7",
"elliptic": "^6.5.4",
"ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.1.2",
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9",
"jssha": "^3.3.1",
"localstorage-slim": "^2.5.0",
"luxon": "^3.4.3",
"merkletreejs": "^0.3.10",
@@ -55,7 +52,6 @@
"vue": "^3.3.4",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.2",
"vue-qrcode-reader": "^5.4.1",
"vue-router": "^4.2.4",
"web-did-resolver": "^2.0.27"
},
@@ -75,13 +71,13 @@
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.15",
"eslint": "^8.53.0",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"leaflet": "^1.9.4",
"postcss": "^8.4.29",
"prettier": "^3.1.0",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.3",
"typescript": "~5.2.2"
}

View File

@@ -162,22 +162,17 @@
<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"
@click="
close(notification.id);
turnOnNotifications();
"
>
Turn on Notifications
</button>
<div class="grid grid-cols-2 gap-2">
<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"
>
Maybe Later
</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"
>
Never
@@ -259,248 +254,4 @@
<style></style>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import axios, { AxiosError } from "axios";
interface ServiceWorkerMessage {
type: string;
data: string;
}
interface ServiceWorkerResponse {
// Define the properties and their types
success: boolean;
message?: string;
}
// Example interface for error
interface ErrorResponse {
message: string;
// Other properties as needed
}
interface VapidResponse {
data: {
vapidKey: string;
};
}
@Component
export default class App extends Vue {
b64 = "";
mounted() {
axios
.get("https://timesafari-pwa.anomalistlabs.com/web-push/vapid")
.then((response: VapidResponse) => {
this.b64 = response.data.vapidKey;
console.log(this.b64);
navigator.serviceWorker.addEventListener("controllerchange", () => {
console.log("New service worker is now controlling the page");
});
})
.catch((error: AxiosError) => {
console.error("API error", error);
});
}
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> {
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) => {
console.log("Response from service worker:", 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 (Notification.permission === "granted") {
return Promise.resolve();
}
return Promise.resolve();
}
private requestNotificationPermission(): Promise<NotificationPermission> {
return Notification.requestPermission().then((permission) => {
if (permission !== "granted") {
alert("We need notification permission to provide certain features.");
throw new Error("We weren't granted permission.");
}
return permission;
});
}
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;
}
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 (Notification.permission !== "granted") {
const errorMsg = "Notification permission not granted";
console.warn(errorMsg);
return reject(new Error(errorMsg));
}
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
const options: PushSubscriptionOptions = {
userVisibleOnly: true,
applicationServerKey: applicationServerKey,
};
navigator.serviceWorker.ready
.then((registration) => {
return registration.pushManager.subscribe(options);
})
.then((subscription) => {
console.log("Push subscription successful:", subscription);
resolve();
})
.catch((error) => {
console.error(
"Subscription or server communication failed:",
error,
options,
);
// 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: 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>
<script lang="ts"></script>

View File

@@ -4,7 +4,7 @@
* See also ../libs/veramo/setup.ts
*/
export enum AppString {
APP_NAME = "TimeSafari",
APP_NAME = "Time Safari",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",

View File

@@ -6,42 +6,87 @@ import {
MASTER_SETTINGS_KEY,
Settings,
SettingsSchema,
SettingsSchemaV1,
} from "./tables/settings";
import { AppString } from "@/constants/app";
// Define types for tables that hold sensitive and non-sensitive data
type SensitiveTables = { accounts: Table<Account> };
// a separate DB because the seed is super-sensitive data
type SensitiveTables = {
accounts: Table<Account>;
};
type NonsensitiveTables = {
contacts: Table<Contact>;
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 const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
const SensitiveSchemas = Object.assign({}, AccountsSchema);
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
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;
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 =
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 });
// Define the schema for our databases
accountsDB.version(1).stores(SensitiveSchemas);
db.version(1).stores(NonsensitiveSchemas);
// Event handler to initialize the non-sensitive database with default settings
db.on("populate", () => {
db.version(1).stores(NonsensitiveSchemasV1);
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({
id: MASTER_SETTINGS_KEY,
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,

View File

@@ -1,47 +1,34 @@
/**
* BoundingBox type describes the geographical bounding box coordinates.
*/
export type BoundingBox = {
eastLong: number; // Eastern longitude
maxLat: number; // Maximum (Northernmost) latitude
minLat: number; // Minimum (Southernmost) latitude
westLong: number; // Western longitude
eastLong: number;
maxLat: number;
minLat: number;
westLong: number;
};
/**
* Settings type encompasses user-specific configuration details.
*/
// a singleton
export type Settings = {
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
id: number; // there's only one entry: MASTER_SETTINGS_KEY
activeDid?: string;
apiServer?: string;
firstName?: string;
isRegistered?: boolean;
// Array of named search boxes defined by bounding boxes
lastName?: string; // deprecated, pre v 0.1.3
lastViewedClaimId?: string;
searchBoxes?: Array<{
name: string;
bbox: BoundingBox;
}>;
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
showContactGivesInline?: boolean;
};
/**
* Schema for the Settings table in the database.
*/
export const SettingsSchema = {
export const SettingsSchemaV1 = {
settings: "id",
};
/**
* Constants.
*/
export const SettingsSchema = {
settings:
"id, activeDid, apiServer, firstName, lastname, lastViewedClaimId, searchBoxes, showContactGivesInline",
};
export const MASTER_SETTINGS_KEY = 1;

View File

@@ -134,8 +134,8 @@ export function didInfo(
return contact
? contact.name || "Contact With No Name"
: isHiddenDid(did)
? "Someone Not In Network"
: "Someone Not In Contacts";
? "Someone Not In Network"
: "Someone Not In Contacts";
}
export interface ResultWithType {
@@ -231,10 +231,10 @@ export async function createAndSubmitGive(
error === null
? "Null error"
: error instanceof Error
? error.message
: typeof error === "object" && error !== null && "message" in error
? (error as { message: string }).message
: "Unknown error";
? error.message
: typeof error === "object" && error !== null && "message" in error
? (error as { message: string }).message
: "Unknown error";
return {
type: "error",

View File

@@ -3,7 +3,7 @@
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
register("/additional-scripts.js", {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log(
"App is being served from cache by a service worker.\n" +

View File

@@ -67,8 +67,8 @@
showGiveTotals
? "Total"
: showGiveConfirmed
? "Confirmed"
: "Unconfirmed"
? "Confirmed"
: "Unconfirmed"
}}
</button>
<br />

View File

@@ -8,38 +8,22 @@
<!-- show the actions for recognizing a give -->
<div class="mb-8">
<div
v-if="!activeDid"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<p class="text-lg mb-3">
You need an <b>identifier</b> before you can record others' giving.
</p>
<router-link
:to="{ name: 'start' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
>
Create Your Identifier</router-link
<div v-if="!activeDid">
To record others' giving,
<router-link :to="{ name: 'start' }" class="text-blue-500">
create your identifier.</router-link
>
</div>
<div
v-else-if="!isRegistered"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
Someone must register your account before you can record others' giving.
To do this:
<router-link
:to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
<div v-else-if="!isRegistered">
To record others' giving, someone must register your account, so show
them
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
your identity info</router-link
>
1. Show Them Your Identity Info</router-link
>
<router-link
:to="{ name: 'account' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
>
2. Check Your Limits</router-link
and then
<router-link :to="{ name: 'account' }" class="text-blue-500">
check your limits.</router-link
>
</div>

View File

@@ -1,63 +0,0 @@
/* eslint-env serviceworker */
/* global workbox */
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
);
self.addEventListener("install", (event) => {
importScripts(
"safari-notifications.js",
"nacl.js",
"noble-curves.js",
"noble-hashes.js",
);
});
self.addEventListener("push", function (event) {
event.waitUntil(
(async () => {
try {
let payload;
if (event.data) {
payload = JSON.parse(event.data.text());
}
const message = await self.getNotificationCount();
const title = payload ? payload.title : "Custom Title";
const options = {
body: message,
icon: payload ? payload.icon : "icon.png",
badge: payload ? payload.badge : "badge.png",
};
await self.registration.showNotification(title, options);
} catch (error) {
console.error("Error in processing the push event:", error);
}
})(),
);
});
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SEND_LOCAL_DATA") {
self.secret = event.data.data;
event.ports[0].postMessage({ success: true });
}
});
self.addEventListener("activate", (event) => {
event.waitUntil(clients.claim());
console.log("Service worker activated", event);
});
self.addEventListener("fetch", (event) => {
console.log(event.request);
});
self.addEventListener("error", (event) => {
console.error("Error in Service Worker:", event.message);
console.error("File:", event.filename);
console.error("Line:", event.lineno);
console.error("Column:", event.colno);
console.error("Error Object:", event.error);
});
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,545 +0,0 @@
function bufferFromBase64(base64) {
const binaryString = atob(base64);
const length = binaryString.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
function fromString(str, encoding = "utf8") {
if (encoding === "utf8") {
return new TextEncoder().encode(str);
} else if (encoding === "base16") {
if (str.length % 2 !== 0) {
throw new Error("Invalid hex string length.");
}
let bytes = new Uint8Array(str.length / 2);
for (let i = 0; i < str.length; i += 2) {
bytes[i / 2] = parseInt(str.substring(i, i + 2), 16);
}
return bytes;
} else if (encoding === "base64url") {
str = str.replace(/-/g, "+").replace(/_/g, "/");
while (str.length % 4) {
str += "=";
}
return new Uint8Array(bufferFromBase64(str));
} else {
throw new Error(`Unsupported encoding "${encoding}"`);
}
}
/**
* Convert a Uint8Array to a string with the given encoding.
*
* @param {Uint8Array} byteArray - The Uint8Array to convert.
* @param {string} [encoding='utf8'] - The desired encoding ('utf8', 'base16', 'base64url').
* @returns {string} - The encoded string.
* @throws {Error} - Throws an error if the encoding is unsupported.
*/
function toString(byteArray, encoding = "utf8") {
switch (encoding) {
case "utf8":
return decodeUTF8(byteArray);
case "base16":
return toBase16(byteArray);
case "base64url":
return toBase64Url(byteArray);
default:
throw new Error(`Unsupported encoding "${encoding}"`);
}
}
/**
* Decode a Uint8Array as a UTF-8 string.
*
* @param {Uint8Array} byteArray
* @returns {string}
*/
function decodeUTF8(byteArray) {
return new TextDecoder().decode(byteArray);
}
/**
* Convert a Uint8Array to a base16 (hex) encoded string.
*
* @param {Uint8Array} byteArray
* @returns {string}
*/
function toBase16(byteArray) {
return Array.from(byteArray)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
/**
* Convert a Uint8Array to a base64url encoded string.
*
* @param {Uint8Array} byteArray
* @returns {string}
*/
function toBase64Url(byteArray) {
let uint8Array = new Uint8Array(byteArray);
let binaryString = "";
for (let i = 0; i < uint8Array.length; i++) {
binaryString += String.fromCharCode(uint8Array[i]);
}
// Encode to base64
let base64 = btoa(binaryString);
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
const u8a = { toString, fromString };
function sha256(payload) {
const data = typeof payload === "string" ? u8a.fromString(payload) : payload;
return nobleHashes.sha256(data);
}
async function accessToken(identifier) {
const did = identifier["did"];
const privateKeyHex = identifier["keys"][0]["privateKeyHex"];
const signer = await SimpleSigner(privateKeyHex);
const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
const jwt = await createJWT(tokenPayload, {
alg,
issuer: did,
signer,
});
return jwt;
}
async function createJWT(payload, options, header = {}) {
const { issuer, signer, alg, expiresIn, canonicalize } = options;
if (!signer)
throw new Error(
"missing_signer: No Signer functionality has been configured",
);
if (!issuer)
throw new Error("missing_issuer: No issuing DID has been configured");
if (!header.typ) header.typ = "JWT";
if (!header.alg) header.alg = alg;
const timestamps = {
iat: Math.floor(Date.now() / 1000),
exp: undefined,
};
if (expiresIn) {
if (typeof expiresIn === "number") {
timestamps.exp = (payload.nbf || timestamps.iat) + Math.floor(expiresIn);
} else {
throw new Error("invalid_argument: JWT expiresIn is not a number");
}
}
const fullPayload = { ...timestamps, ...payload, iss: issuer };
return createJWS(fullPayload, signer, header, { canonicalize });
}
const defaultAlg = "ES256K";
async function createJWS(payload, signer, header = {}, options = {}) {
if (!header.alg) header.alg = defaultAlg;
const encodedPayload =
typeof payload === "string"
? payload
: encodeSection(payload, options.canonicalize);
const signingInput = [
encodeSection(header, options.canonicalize),
encodedPayload,
].join(".");
const jwtSigner = ES256KSignerAlg(false);
const signature = await jwtSigner(signingInput, signer);
// JWS Compact Serialization
// https://www.rfc-editor.org/rfc/rfc7515#section-7.1
return [signingInput, signature].join(".");
}
function canonicalizeData(object) {
if (typeof object === "number" && isNaN(object)) {
throw new Error("NaN is not allowed");
}
if (typeof object === "number" && !isFinite(object)) {
throw new Error("Infinity is not allowed");
}
if (object === null || typeof object !== "object") {
return JSON.stringify(object);
}
if (object.toJSON instanceof Function) {
return serialize(object.toJSON());
}
if (Array.isArray(object)) {
const values = object.reduce((t, cv, ci) => {
const comma = ci === 0 ? "" : ",";
const value = cv === undefined || typeof cv === "symbol" ? null : cv;
return `${t}${comma}${serialize(value)}`;
}, "");
return `[${values}]`;
}
const values = Object.keys(object)
.sort()
.reduce((t, cv) => {
if (object[cv] === undefined || typeof object[cv] === "symbol") {
return t;
}
const comma = t.length === 0 ? "" : ",";
return `${t}${comma}${serialize(cv)}:${serialize(object[cv])}`;
}, "");
return `{${values}}`;
}
function encodeSection(data, shouldCanonicalize = false) {
if (shouldCanonicalize) {
return encodeBase64url(canonicalizeData(data));
} else {
return encodeBase64url(JSON.stringify(data));
}
}
function encodeBase64url(s) {
return bytesToBase64url(u8a.fromString(s));
}
function instanceOfEcdsaSignature(object) {
return typeof object === "object" && "r" in object && "s" in object;
}
function ES256KSignerAlg(recoverable) {
return async function sign(payload, signer) {
const signature = await signer(payload);
if (instanceOfEcdsaSignature(signature)) {
return toJose(signature, recoverable);
} else {
if (
recoverable &&
typeof fromJose(signature).recoveryParam === "undefined"
) {
throw new Error(
`not_supported: ES256K-R not supported when signer doesn't provide a recovery param`,
);
}
return signature;
}
};
}
function leftpad(data, size = 64) {
if (data.length === size) return data;
return "0".repeat(size - data.length) + data;
}
async function SimpleSigner(hexPrivateKey) {
const signer = await ES256KSigner(hexToBytes(hexPrivateKey), true);
return async (data) => {
const signature = await signer(data);
return fromJose(signature);
};
}
function hexToBytes(s, minLength) {
let input = s.startsWith("0x") ? s.substring(2) : s;
if (input.length % 2 !== 0) {
input = `0${input}`;
}
if (minLength) {
const paddedLength = Math.max(input.length, minLength * 2);
input = input.padStart(paddedLength, "00");
}
return u8a.fromString(input.toLowerCase(), "base16");
}
function ES256KSigner(privateKey, recoverable = false) {
const privateKeyBytes = privateKey;
if (privateKeyBytes.length !== 32) {
throw new Error(
`bad_key: Invalid private key format. Expecting 32 bytes, but got ${privateKeyBytes.length}`,
);
}
return async function (data) {
const signature = nobleCurves.secp256k1.sign(sha256(data), privateKeyBytes);
return toJose(
{
r: leftpad(signature.r.toString(16)),
s: leftpad(signature.s.toString(16)),
recoveryParam: signature.recovery,
},
recoverable,
);
};
}
function toJose(signature, recoverable) {
const { r, s, recoveryParam } = signature;
const jose = new Uint8Array(recoverable ? 65 : 64);
jose.set(u8a.fromString(r, "base16"), 0);
jose.set(u8a.fromString(s, "base16"), 32);
if (recoverable) {
if (typeof recoveryParam === "undefined") {
throw new Error("Signer did not return a recoveryParam");
}
jose[64] = recoveryParam;
}
return bytesToBase64url(jose);
}
function bytesToBase64url(b) {
return u8a.toString(b, "base64url");
}
function base64ToBytes(s) {
const inputBase64Url = s
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
return u8a.fromString(inputBase64Url, "base64url");
}
function bytesToHex(b) {
return u8a.toString(b, "base16");
}
function fromJose(signature) {
const signatureBytes = base64ToBytes(signature);
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
throw new TypeError(
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
);
}
const r = bytesToHex(signatureBytes.slice(0, 32));
const s = bytesToHex(signatureBytes.slice(32, 64));
const recoveryParam =
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
return { r, s, recoveryParam };
}
function validateBase64(s) {
if (
!/^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/.test(
s,
)
) {
throw new TypeError("invalid encoding");
}
}
function decodeBase64(s) {
validateBase64(s);
var i,
d = atob(s),
b = new Uint8Array(d.length);
for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i);
return b;
}
async function getSettingById(id) {
return new Promise((resolve, reject) => {
let openRequest = indexedDB.open("TimeSafari");
openRequest.onupgradeneeded = (event) => {
// Handle database setup if necessary
let db = event.target.result;
if (!db.objectStoreNames.contains("settings")) {
db.createObjectStore("settings", { keyPath: "id" });
}
};
openRequest.onsuccess = (event) => {
let db = event.target.result;
let transaction = db.transaction("settings", "readonly");
let objectStore = transaction.objectStore("settings");
let getRequest = objectStore.get(id);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
};
openRequest.onerror = () => reject(openRequest.error);
});
}
async function setMostRecentNotified(id) {
try {
const db = await openIndexedDB("TimeSafari");
const transaction = db.transaction("settings", "readwrite");
const store = transaction.objectStore("settings");
const data = await getRecord(store, 1);
if (data) {
data["lastNotifiedClaimId"] = id;
await updateRecord(store, data);
} else {
console.error("Record not found");
}
transaction.oncomplete = () => db.close();
} catch (error) {
console.error("Database error: " + error.message);
}
}
function openIndexedDB(dbName) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains("settings")) {
db.createObjectStore("settings");
}
};
});
}
function getRecord(store, key) {
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function updateRecord(store, data) {
return new Promise((resolve, reject) => {
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function fetchAllAccounts() {
return new Promise((resolve, reject) => {
let openRequest = indexedDB.open("TimeSafariAccounts");
openRequest.onupgradeneeded = function (event) {
let db = event.target.result;
if (!db.objectStoreNames.contains("accounts")) {
db.createObjectStore("accounts", { keyPath: "id" });
}
};
openRequest.onsuccess = function (event) {
let db = event.target.result;
let transaction = db.transaction("accounts", "readonly");
let objectStore = transaction.objectStore("accounts");
let getAllRequest = objectStore.getAll();
getAllRequest.onsuccess = function () {
resolve(getAllRequest.result);
};
getAllRequest.onerror = function () {
reject(getAllRequest.error);
};
};
openRequest.onerror = function () {
reject(openRequest.error);
};
});
}
async function getNotificationCount() {
let secret = null;
let accounts = [];
let result = null;
if ("secret" in self) {
secret = self.secret;
const secretUint8Array = self.decodeBase64(secret);
const settings = await getSettingById(1);
let lastNotifiedClaimId = null;
if ("lastNotifiedClaimId" in settings) {
lastNotifiedClaimId = settings["lastNotifiedClaimId"];
}
const activeDid = settings["activeDid"];
accounts = await fetchAllAccounts();
let did = null;
for (var i = 0; i < accounts.length; i++) {
let account = accounts[i];
let did = account["did"];
if (did == activeDid) {
let publicKeyHex = account["publicKeyHex"];
let identity = account["identity"];
const messageWithNonceAsUint8Array = self.decodeBase64(identity);
const nonce = messageWithNonceAsUint8Array.slice(0, 24);
const message = messageWithNonceAsUint8Array.slice(24, identity.length);
const decoder = new TextDecoder("utf-8");
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
const msg = decoder.decode(decrypted);
const identifier = JSON.parse(JSON.parse(msg));
const headers = {
"Content-Type": "application/json",
};
headers["Authorization"] = "Bearer " + (await accessToken(identifier));
let response = await fetch(
"https://test-api.endorser.ch/api/v2/report/claims",
{
method: "GET",
headers: headers,
},
);
if (response.status == 200) {
let json = await response.json();
let claims = json["data"];
let newClaims = 0;
for (var i = 0; i < claims.length; i++) {
let claim = claims[i];
if (claim["id"] === lastNotifiedClaimId) {
break;
}
newClaims++;
}
if (newClaims === 0) {
result = "You have no new claims today.";
} else {
result = `${newClaims} have been shared with you`;
}
const most_recent_notified = claims[0]["id"];
await setMostRecentNotified(most_recent_notified);
return "TEST";
} else {
console.error(response.status);
}
}
}
}
return result;
}
self.getNotificationCount = getNotificationCount;
self.decodeBase64 = decodeBase64;

View File

@@ -11,9 +11,5 @@ module.exports = defineConfig({
iconPaths: {
faviconSVG: "img/icons/safari-pinned-tab.svg",
},
workboxPluginMode: "InjectManifest",
workboxOptions: {
swSrc: "./sw_scripts/additional-scripts.js",
},
},
});