diff --git a/src/lib/notifications.ts b/src/lib/notifications.ts index fafd3612..1929dd67 100644 --- a/src/lib/notifications.ts +++ b/src/lib/notifications.ts @@ -64,15 +64,17 @@ export async function unsubscribe() { async function sendSubscriptionToServer(subscription) { try { - const res = await fetch('/api/addPushSubscription', { + const response = 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})`); + + const output = await response?.json() + return output; + } catch (error) { console.error('Error saving subscription on server:', error); unsubscribe(); @@ -87,9 +89,11 @@ export async function subscribeUser() { userVisibleOnly: true, applicationServerKey: import.meta.env.VITE_VAPID_PUBLIC_KEY }); - sendSubscriptionToServer(subscription); + const output = sendSubscriptionToServer(subscription); + return output; } catch (err) { console.error('Error subscribing:', err); + return {'success': false} } } } @@ -104,7 +108,7 @@ export async function checkSubscriptionStatus() { //this can be optional if (exists) { // just to make sure the subscription is saved on the server - sendSubscriptionToServer(subscription); + //sendSubscriptionToServer(subscription); } return exists; } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2aeac013..85e92ad5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -52,12 +52,6 @@ import Gem from "lucide-svelte/icons/gem"; import stocknear_logo from "$lib/images/stocknear_logo.png"; - import { - requestNotificationPermission, - subscribeUser, - checkSubscriptionStatus, - } from "$lib/notifications"; - export let data; let hideHeader = false; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 252ba728..d33508e6 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -25,6 +25,7 @@ let AppInstalled = null; function getClosedPWA() { + //if user closed the banner const item = localStorage.getItem("closePWA"); if (!item) return null; diff --git a/src/routes/api/addPushSubscription/+server.ts b/src/routes/api/addPushSubscription/+server.ts index 6a794065..6e956773 100644 --- a/src/routes/api/addPushSubscription/+server.ts +++ b/src/routes/api/addPushSubscription/+server.ts @@ -1,70 +1,33 @@ 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'); - } - + let output = false; + 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({ + try { + const items = await pb.collection("pushSubscription").getFullList({ filter: `user="${user?.id}"`, }); if (output?.length > 0) { - for (const item of output) { + for (const item of items) { 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'); + output = true; - return new Response(JSON.stringify({'success': true})); + } catch(err) { + console.log(err) + } + + return new Response(JSON.stringify({'success': output})); }) satisfies RequestHandler; \ No newline at end of file diff --git a/src/routes/api/sendPushSubscription/+server.ts b/src/routes/api/sendPushSubscription/+server.ts index 7d434e48..d73dba46 100644 --- a/src/routes/api/sendPushSubscription/+server.ts +++ b/src/routes/api/sendPushSubscription/+server.ts @@ -12,46 +12,54 @@ webPush.setVapidDetails( export const POST: RequestHandler = async ({ request, locals }) => { const { pb, apiKey } = locals; + const { body, key } = await request?.json(); - const { body, key } = await request?.json(); + if (apiKey !== key) { + console.warn('Invalid API key'); + return new Response(JSON.stringify({ success: false, error: 'Invalid API key' }), { status: 401 }); + } - if (apiKey === key) { - - try { + try { + // Get all push subscriptions + const subscriptions = await pb.collection('pushSubscription').getFullList({ sort: '-created' }); - // Get all push subscriptions - const subscriptions = await pb.collection('pushSubscription').getFullList({ - sort: '-created' - }); - - // Send notifications to all subscriptions - const sendNotifications = subscriptions?.map(async (subRecord) => { - try { - const subscriptionData = subRecord.subscription?.subscription; - await webPush.sendNotification( - subscriptionData, // Ensure correct format - body - ); - } catch (error: any) { - console.error('Error sending notification:', error); - - // Delete invalid subscriptions (410 means "Gone") - if (error.statusCode === 410) { - await pb.collection('pushSubscription').delete(subRecord.id); - } - } - }); - - await Promise.all(sendNotifications); - - return new Response(JSON.stringify({ success: true, message: `Notifications sent to ${subscriptions.length} devices` })); - } catch (error: any) { - console.error('Error sending notifications:', error); - return new Response(JSON.stringify({ success: false, error: error.message }, { status: 500 })); - } - - } else { - console.log('key is wrong') + if (!subscriptions.length) { + console.warn('No subscriptions found.'); + return new Response(JSON.stringify({ success: false, error: 'No subscriptions found' }), { status: 404 }); } + // Send notifications + const sendNotifications = subscriptions.map(async (subRecord) => { + try { + const subscriptionData = subRecord.subscription?.subscription; + + if (!subscriptionData || !subscriptionData.endpoint) { + console.warn(`Skipping invalid subscription: ${subRecord.id}`); + return; + } + + // Apple Push Notifications do not support VAPID + const payload = subscriptionData.endpoint.includes('web.push.apple.com') ? '' : body; + + await webPush.sendNotification(subscriptionData, payload); + console.log(`Notification sent to: ${subscriptionData.endpoint}`); + + } catch (error: any) { + console.error(`Error sending notification to ${subRecord.id}:`, error); + + // Remove invalid subscriptions + if (error.statusCode === 410 || error.statusCode === 404) { + console.warn(`Deleting invalid subscription: ${subRecord.id}`); + await pb.collection('pushSubscription').delete(subRecord.id); + } + } + }); + + await Promise.all(sendNotifications); + + return new Response(JSON.stringify({ success: true, message: `Notifications sent to ${subscriptions.length} devices` })); + } catch (error: any) { + console.error('Error sending notifications:', error); + return new Response(JSON.stringify({ success: false, error: error.message }), { status: 500 }); + } }; diff --git a/src/routes/profile/+page.server.ts b/src/routes/profile/+page.server.ts index 0610bd6b..ac5c8699 100644 --- a/src/routes/profile/+page.server.ts +++ b/src/routes/profile/+page.server.ts @@ -6,6 +6,30 @@ export const load = async ({ locals }) => { redirect(303, "/login"); } +const getPushSubscriptionData = async () => { + let output = {}; + try { + output = await pb.collection("pushSubscription").getFullList({ + filter: `user="${user?.id}"`, + sort: "-created", // Sorts newest first + }); + + if (output?.length > 1) { + const [, ...toDelete] = output; // Keep the first item, delete the rest + await Promise.all( + toDelete.map((item) => pb.collection("pushSubscription").delete(item?.id)) + ); + } + } catch (err) { + console.log(err); + } + + return output?.at(0) || null; // Return only the latest item +}; + + + + const getSubscriptionData = async () => { const output = ( @@ -23,7 +47,9 @@ export const load = async ({ locals }) => { return { getSubscriptionData: await getSubscriptionData(), + getPushSubscriptionData: await getPushSubscriptionData(), }; + }; export const actions = { diff --git a/src/routes/profile/+page.svelte b/src/routes/profile/+page.svelte index 3cc1fe29..a681769e 100644 --- a/src/routes/profile/+page.svelte +++ b/src/routes/profile/+page.svelte @@ -2,6 +2,7 @@ import SEO from "$lib/components/SEO.svelte"; import toast from "svelte-french-toast"; import { enhance } from "$app/forms"; + import { isPWAInstalled } from "$lib/utils"; import { requestNotificationPermission, checkSubscriptionStatus, @@ -13,8 +14,9 @@ export let data; export let form; + let pwaInstalled; let nottifPermGranted: boolean | null = null; - let isPushSubscribed = false; + let isPushSubscribed = data?.getPushSubscriptionData !== null ? true : false; let subscriptionData = data?.getSubscriptionData; let isClicked = false; @@ -159,11 +161,13 @@ }; onMount(async () => { - if (data?.user?.id) { - nottifPermGranted = await requestNotificationPermission(); - if (nottifPermGranted) { - isPushSubscribed = (await checkSubscriptionStatus()) || false; - } + pwaInstalled = isPWAInstalled(); + nottifPermGranted = await requestNotificationPermission(); + if (nottifPermGranted) { + isPushSubscribed = + ((await checkSubscriptionStatus()) && + data?.getPushSubscriptionData !== null) || + false; } }); @@ -177,12 +181,20 @@ } async function handlePushSubscribe() { - subscribeUser(); - isPushSubscribed = true; - toast.success("Push notification activated successfully!", { - style: - "border-radius: 5px; background: #fff; color: #000; border-color: #4B5563; font-size: 15px;", - }); + const output = await subscribeUser(); + console.log(output); + if (output?.success === true) { + isPushSubscribed = true; + toast.success("Push notification activated successfully!", { + style: + "border-radius: 5px; background: #fff; color: #000; border-color: #4B5563; font-size: 15px;", + }); + } else { + toast.error("Your browser does not support push notifications...", { + style: + "border-radius: 5px; background: #fff; color: #000; border-color: #4B5563; font-size: 15px;", + }); + } } @@ -236,46 +248,49 @@ > -
Checking permissions...
- {:else if nottifPermGranted === true} - {#if isPushSubscribed} -Push notifications are currently active.
-Checking permissions...
+ {:else if nottifPermGranted === true} + {#if isPushSubscribed} +Push notifications are currently active.
++ Stay up-to-date with real-time price alerts, the latest + stock news, and earnings calls delivered straight to your + device. +
Enable notifications -- Stay up-to-date with real-time price alerts, the latest - stock news, and earnings calls delivered straight to your - device. + {/if} + {:else if nottifPermGranted === false} +
+ Review your settings and enable notifications to stay + updated with Stocknear alerts.
- {/if} - {:else if nottifPermGranted === false} -- Review your settings and enable notifications to stay updated - with Stocknear alerts. -
- {/if} +