add push notification

This commit is contained in:
MuslemRahimi 2025-02-02 18:15:50 +01:00
parent 7a485ddc02
commit 7270087a17
8 changed files with 309 additions and 8 deletions

105
package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -43,4 +43,64 @@ export function sendNotification(
};
}
}
}
}
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;
}

View File

@ -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 @@
-->
<slot />
<Toaster class="bg-[#1A1A27] text-white text-medium" />
<Toaster />
{#if Cookie && $showCookieConsent === true}
<Cookie />
{/if}

View File

@ -60,6 +60,8 @@
<SEO
title="Free Online Stock Analysis for Investors"
description="Stocknear has everything you need to analyze stocks with help of AI, including detailed financial data, statistics, news and charts."
image=""
/>
/>
<div

View File

@ -0,0 +1,70 @@
import type { RequestHandler } from "./$types";
import { error} from '@sveltejs/kit';
import webpush from "web-push";
const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY;
const VAPID_PRIVATE_KEY = import.meta.env.VITE_VAPID_PRIVATE_KEY;
function initWebPush() {
webpush.setVapidDetails('mailto:contact@stocknear.com',VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY )
}
async function sendNotification(subscription, payload) {
try {
const res = await webpush.sendNotification(subscription, payload);
return {
ok: res.statusCode === 201,
status: res.statusCode,
body: res.body
};
} catch (err) {
const msg = `Could not send notification: ${err}`;
console.error(msg);
return {
ok: false,
status: undefined,
body: msg
};
}
}
export const POST = (async ({ locals, request }) => {
const { user, pb } = locals;
if (!user?.id) {
console.log('No username passed to addSubscription');
throw error(401, 'Unauthorized');
}
const data = await request.json();
if (!data?.subscription) {
console.log('No subscription passed to addSubscription', data);
throw error(400, 'Bad Request');
}
initWebPush()
// find all subscription of users if they exist and delete them first before creating the new one.
const output = await pb.collection("pushSubscription").getFullList({
filter: `user="${user?.id}"`,
});
if (output?.length > 0) {
for (const item of output) {
await pb.collection("pushSubscription").delete(item?.id);
}
}
await pb.collection("pushSubscription").create({user: user?.id, subscription: data})
//addUserDevice(username, data.subscription);
//addUserToChannel(username, 'album-updates');
return new Response(JSON.stringify({'success': true}));
}) satisfies RequestHandler;

View File

@ -0,0 +1,35 @@
import type { RequestHandler } from "./$types";
import { error} from '@sveltejs/kit';
export const POST = (async ({ locals, request }) => {
const { user, pb } = locals;
if (!user?.id) {
console.log('No username passed to addSubscription');
throw error(401, 'Unauthorized');
}
const data = await request.json();
if (!data?.subscription) {
console.log('No subscription passed to unsubscribe', data);
throw error(400, 'Bad Request');
}
const output = await pb.collection("pushSubscription").getFullList({
filter: `user="${user?.id}"`,
});
if (output?.length > 0) {
for (const item of output) {
await pb.collection("pushSubscription")?.delete(item?.id);
}
}
return new Response(JSON.stringify({'success': true}));
}) satisfies RequestHandler;

View File

@ -5,16 +5,20 @@ declare let self: ServiceWorkerGlobalScope;
import { build, files, version } from "$service-worker";
// Fixed template literal syntax
const CACHE = `cache-${version}`;
const ASSETS = [...build, ...files];
// install service worker
self.addEventListener("install", (event) => {
async function addFilesToCache() {
const cache = await caches.open(CACHE);
await cache.addAll(ASSETS);
try {
const cache = await caches.open(CACHE);
await cache.addAll(ASSETS);
} catch (error) {
console.error('Service worker installation failed:', error);
}
}
event.waitUntil(addFilesToCache());
});
@ -72,3 +76,16 @@ self.addEventListener("message", (event) => {
self.skipWaiting();
}
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
self.addEventListener('push', function (event: any) {
const payload = event.data?.text() ?? 'no payload';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const registration = (self as any).registration as ServiceWorkerRegistration;
event.waitUntil(
registration.showNotification('Stocknear', {
body: payload
})
);
} as EventListener);