diff --git a/package-lock.json b/package-lock.json
index 4468a9d0..278edcc5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -89,6 +89,7 @@
"vite": "^5.4.9",
"vite-plugin-dynamic-import": "^1.5.0",
"vitest": "^1.5.1",
+ "web-push": "^3.6.7",
"zod": "^3.23.4"
}
},
@@ -2715,6 +2716,16 @@
"node": ">=0.4.0"
}
},
+ "node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2896,6 +2907,19 @@
"integrity": "sha512-UfobP5N12Qm4Qu4fwLDIi2v6+wZsSf6snYSxAMeKhrh37YGnNWZPRmVEKc/2wfms53TLQnzfpG8wCx2Y/6NG1w==",
"license": "MIT"
},
+ "node_modules/asn1.js": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+ "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
"node_modules/assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
@@ -3071,6 +3095,13 @@
"integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==",
"dev": true
},
+ "node_modules/bn.js": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
+ "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -5649,6 +5680,16 @@
"node": ">=8.0.0"
}
},
+ "node_modules/http_ece": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
+ "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@@ -5673,6 +5714,20 @@
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="
},
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
@@ -6761,6 +6816,13 @@
"mini-svg-data-uri": "cli.js"
}
},
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -10080,6 +10142,49 @@
"integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==",
"license": "Apache-2.0"
},
+ "node_modules/web-push": {
+ "version": "3.6.7",
+ "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
+ "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "asn1.js": "^5.3.0",
+ "http_ece": "1.2.0",
+ "https-proxy-agent": "^7.0.0",
+ "jws": "^4.0.0",
+ "minimist": "^1.2.5"
+ },
+ "bin": {
+ "web-push": "src/cli.js"
+ },
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/web-push/node_modules/jwa": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
+ "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/web-push/node_modules/jws": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+ "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.0",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/web-worker": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz",
diff --git a/package.json b/package.json
index 82048cc2..33a8b5d4 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,7 @@
"vite": "^5.4.9",
"vite-plugin-dynamic-import": "^1.5.0",
"vitest": "^1.5.1",
+ "web-push": "^3.6.7",
"zod": "^3.23.4"
},
"dependencies": {
diff --git a/src/lib/notifications.ts b/src/lib/notifications.ts
index 7bd1f0b1..5f8cc92e 100644
--- a/src/lib/notifications.ts
+++ b/src/lib/notifications.ts
@@ -43,4 +43,64 @@ export function sendNotification(
};
}
}
-}
\ No newline at end of file
+}
+
+
+async function unsubscribe() {
+ if ('serviceWorker' in navigator) {
+ const registration = await navigator.serviceWorker.ready;
+ const subscription = await registration.pushManager.getSubscription();
+ if (subscription) {
+ await subscription.unsubscribe();
+ }
+ }
+ }
+
+async function sendSubscriptionToServer(subscription) {
+ try {
+ const res = await fetch('/api/addPushSubscription', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ subscription })
+ });
+ if (!res.ok)
+ throw new Error(`Error saving subscription on server: ${res.statusText} (${res.status})`);
+ } catch (error) {
+ console.error('Error saving subscription on server:', error);
+ unsubscribe();
+ }
+ }
+
+export async function subscribeUser() {
+ if ('serviceWorker' in navigator) {
+ try {
+ const registration = await navigator.serviceWorker.ready;
+ const subscription = await registration.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: import.meta.env.VITE_VAPID_PUBLIC_KEY
+ });
+ sendSubscriptionToServer(subscription);
+ } catch (err) {
+ console.error('Error subscribing:', err);
+ }
+ }
+ }
+
+export async function checkSubscriptionStatus() {
+ if ('serviceWorker' in navigator) {
+ const registration = await navigator.serviceWorker.ready;
+
+ const subscription = await registration.pushManager.getSubscription();
+ //console.log('check Subscription:', subscription);
+ const exists = subscription !== null;
+ //this can be optional
+ if (exists) {
+ // just to make sure the subscription is saved on the server
+ sendSubscriptionToServer(subscription);
+ }
+ return exists;
+ }
+ return false;
+ }
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index ffde8c07..2a66ec63 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -51,13 +51,13 @@
import AudioLine from "lucide-svelte/icons/audio-lines";
import Gem from "lucide-svelte/icons/gem";
import stocknear_logo from "$lib/images/stocknear_logo.png";
- /*
+
import {
requestNotificationPermission,
- sendNotification,
+ subscribeUser,
+ checkSubscriptionStatus,
} from "$lib/notifications";
- */
export let data;
let hideHeader = false;
@@ -132,7 +132,18 @@
onMount(async () => {
if (data?.user?.id) {
await loadWorker();
+
+ const permissionGranted = await requestNotificationPermission();
+
+ if (permissionGranted) {
+ const isSubscribed = (await checkSubscriptionStatus()) || false;
+
+ if (!isSubscribed) {
+ await subscribeUser();
+ }
+ }
}
+
await checkMarketHour();
if ($showCookieConsent === true) {
Cookie = (await import("$lib/components/Cookie.svelte")).default;
@@ -1181,7 +1192,7 @@
-->