Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
"fastify": "^4.27.0",
"fastify-plugin": "^4.5.1",
"langchain": "^0.2.5",
"node-schedule": "^2.1.1",
"orchid-orm": "^1.31.7",
"orchid-orm-schema-to-zod": "^0.8.8",
"web-push": "^3.6.7",
"zod-to-json-schema": "^3.23.0"
},
"devDependencies": {
Expand All @@ -49,6 +51,8 @@
"tap": "^18.8.0",
"ts-node": "^10.9.2",
"tsx": "^4.10.5",
"typescript": "^5.4.5"
"typescript": "^5.4.5",
"@types/node-schedule": "^2.1.7",
"@types/web-push": "^3.6.3"
}
}
62 changes: 60 additions & 2 deletions apps/api/src/app.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be trpc routes.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import { logsConfig } from "./configs/logger.config";
import { trpcContext } from "./context.trpc";
import { ApiRouter, trpcRouter } from "./router.trpc";
import { googleAuth } from "./auth/google-auth";
import webpush from 'web-push';
import { PushSubscription } from 'web-push';
import { addSubscription, removeSubscription, scheduleFrequentNotification } from './controllers/pushNotifications';

function isSubscriptionActive(subscription: PushSubscription): boolean {
return Array.from(pushSubscriptions).some(
sub => sub.endpoint === subscription.endpoint
);
}

export const app = fastify({
logger: logsConfig[env.ENVIRONMENT],
Expand All @@ -19,7 +28,7 @@ export const logger = app.log;

// Declare a route
app.get("/", function (_, reply) {
reply.send("Hello World!");
reply.send("Fastify is running");
});

// Fastify level centralized error handling
Expand All @@ -37,6 +46,55 @@ app.setErrorHandler(function (error, _request, reply) {

app.register(cookiePlugin).register(fastifyJwt, { secret: env.JWT_SECRET as string }).register(googleAuth)

// Configure web-push
webpush.setVapidDetails(
'mailto:mohit@teziapp.com',
env.VAPID_PUBLIC_KEY,
env.VAPID_PRIVATE_KEY
);

// In-memory storage for subscriptions (replace with database in production)
const pushSubscriptions = new Set<PushSubscription>();

// Add push subscription
app.post('/subscribe', async (request, reply) => {
const subscription = request.body as PushSubscription;

if (isSubscriptionActive(subscription)) {
reply.send({ success: false, message: 'Subscription already active' });
} else {
addSubscription(subscription);
scheduleFrequentNotification();
reply.send({ success: true });
}
});

// Unsubscribe from push notifications
app.post<{
Body: PushSubscription
}>('/unsubscribe', async (request, reply) => {
const subscription = request.body;
removeSubscription(subscription);
reply.send({ success: true });
});

// Trigger push notification

app.post<{
Body: { title: string; body: string }
}>('/send-notification', async (request, reply) => {
const { title, body } = request.body;
for (const subscription of pushSubscriptions) {
try {
await webpush.sendNotification(subscription, JSON.stringify({ title, body }));
} catch (error) {
console.error('Error sending notification:', error);
pushSubscriptions.delete(subscription);
}
}

reply.send({ success: true });
});

app.register(fastifyTRPCPlugin, {
prefix: "/v1",
Expand All @@ -49,4 +107,4 @@ app.register(fastifyTRPCPlugin, {
console.error(`Error in tRPC handler on path '${path}':`, error);
},
} satisfies FastifyTRPCPluginOptions<ApiRouter>["trpcOptions"],
});
});
2 changes: 2 additions & 0 deletions apps/api/src/configs/env.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const envDefaultFields = z
// for storing the logs which are sent to axiom
AXIOM_DATASET: z.string().optional(),
AXIOM_TOKEN: z.string().optional(),
VAPID_PUBLIC_KEY: z.string().min(1),
VAPID_PRIVATE_KEY: z.string().min(1)
})
.refine((env) => {
if (env.ENVIRONMENT === "prod" && env.DB_TEST_URL) {
Expand Down
40 changes: 40 additions & 0 deletions apps/api/src/controllers/pushNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import webpush, { PushSubscription } from 'web-push';
import { env } from "../configs/env.config";
import { scheduleJob } from 'node-schedule';

// Configure web-push
webpush.setVapidDetails(
'mailto:mohit@teziapp.com',
env.VAPID_PUBLIC_KEY,
env.VAPID_PRIVATE_KEY
);

// In-memory storage for subscriptions (replace with database in production)
const pushSubscriptions = new Set<PushSubscription>();

export function addSubscription(subscription: PushSubscription) {
pushSubscriptions.add(subscription);
}

export function removeSubscription(subscription: PushSubscription) {
pushSubscriptions.delete(subscription);
}

Comment on lines +15 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If multiple instances of backend are running in production, this will lead to same notification being sent to user multiple times.

export async function sendNotificationToAll(title: string, body: string) {
for (const subscription of pushSubscriptions) {
try {
await webpush.sendNotification(subscription, JSON.stringify({ title, body }));
} catch (error) {
console.error('Error sending notification:', error);
pushSubscriptions.delete(subscription);
}
}
}

// Schedule a notification every 5 seconds
export function scheduleFrequentNotification() {
scheduleJob('*/5 * * * * *', async () => {
console.log('Sending notification');
await sendNotificationToAll('Frequent Update', 'Here is your notification every 5 seconds!');
});
}
Comment on lines +34 to +40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand you needed this for dev, but then have guardrails to run only in isDev

3 changes: 2 additions & 1 deletion apps/api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ESNext"
"target": "ESNext",
"typeRoots": ["./src/types", "./node_modules/@types"]
},
"include": [
"src/**/*.ts",
Expand Down
4 changes: 4 additions & 0 deletions apps/pwa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@
"react-lazy-load-image-component": "^1.6.2",
"react-microsoft-clarity": "^1.2.0",
"react-router-dom": "^6.26.2",
"react-toastify": "^10.0.5",
"simplebar": "^6.2.7",
"simplebar-react": "^3.2.6",
"stylis": "^4.3.4",
"stylis-plugin-rtl": "^2.1.1",
"vite-plugin-svg-icons": "^2.0.1",
"vite-tsconfig-paths": "^4.3.2",
"web-push": "^3.6.7",
"workbox-core": "^7.1.0",
"workbox-precaching": "^7.1.0",
"zustand": "^4.5.4"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions apps/pwa/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ThemeProvider from '@/theme';
import SnackbarProvider from '@/component/snackbar/SnackbarProvider';
import Router from '@/routes';
import ThemeSettings from '@/component/settings/ThemeSettings';
import ServiceWorkerUpdateDialog from './component/ServiceWorkerUpdate';

// Add these constants for PostHog configuration
const POSTHOG_KEY = import.meta.env.VITE_POSTHOG_KEY;
Expand All @@ -45,6 +46,7 @@ export const App = () => {
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
<PostHogProvider apiKey={POSTHOG_KEY} options={{ api_host: POSTHOG_HOST }}>
<Router />
<ServiceWorkerUpdateDialog />
</PostHogProvider>
</SnackbarProvider>
</ThemeProvider>
Expand Down
73 changes: 73 additions & 0 deletions apps/pwa/src/component/ServiceWorkerUpdate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/// <reference types="vite-plugin-pwa/client" />
import React, { useState, useCallback } from "react";
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, Box} from "@mui/material";
import { registerSW } from "virtual:pwa-register";
import { toast } from 'react-toastify';
import { NotificationToggle } from "./settings/drawer/NotificationToggle";

const ServiceWorkerUpdateDialog: React.FC = () => {
const [updateAvailable, setUpdateAvailable] = useState(false);

// // Static update function
const updateSWFunction = registerSW({
onNeedRefresh() {
setUpdateAvailable(true);
},
onOfflineReady() {
toast.success('App is ready for offline use');
},
});

const handleUpdate = () => {
updateSWFunction(true); // This triggers the installation of the new SW

navigator.serviceWorker.getRegistration().then((registration) => {
if (registration?.waiting) {
// Tell the service worker to skip the waiting phase and activate
registration.waiting.postMessage({ type: 'SKIP_WAITING' });

// Reload the page when the new service worker takes control
registration.waiting.addEventListener('statechange', (event) => {
if ((event.target as ServiceWorker).state === 'activated') {
window.location.reload();
}
});
}
});

setUpdateAvailable(false); // Close the modal
};

const handleClose = useCallback(() => {
setUpdateAvailable(false);
}, []);

return (
<Dialog
open={updateAvailable}
onClose={handleClose}
aria-labelledby="update-dialog-title"
>
<DialogTitle id="update-dialog-title">An update is available!</DialogTitle>
<DialogContent>
<Box sx={{ mb: 2 }}>
<Typography>Performance Improvement Update</Typography>
<Typography variant="h6">New Features:</Typography>
<ul>
<li>Customizations Added</li>
<li>Bug Fixes</li>
<li>Feature Improvements</li>
</ul>
</Box>
<NotificationToggle />
</DialogContent>
<DialogActions>
<Button onClick={handleUpdate} variant="contained" color="primary">
Update
</Button>
</DialogActions>
</Dialog>
);
};

export default ServiceWorkerUpdateDialog;
14 changes: 14 additions & 0 deletions apps/pwa/src/component/settings/drawer/NotificationToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CustomToggleButton, { CustomToggleButtonProps } from '@/theme/CustomToggleButton';
import { handleNotificationSubscription } from '@/utils/handleNotification';

export const NotificationToggle = () => {

const property : CustomToggleButtonProps= {
initialState: false,
label: 'Enable Notifications',
direction: 'row' as const,
onToggleFunction: handleNotificationSubscription
};

return <CustomToggleButton {...property} />;
};
5 changes: 5 additions & 0 deletions apps/pwa/src/component/settings/drawer/SettingsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import DirectionOptions from './DirectionOptions';
import FullScreenOptions from './FullScreenOptions';
import ColorPresetsOptions from './ColorPresetsOptions';
import { useSettingsContext } from '@/component/settings/settingContext';
import { NotificationToggle } from './NotificationToggle';

// ----------------------------------------------------------------------

Expand Down Expand Up @@ -121,6 +122,10 @@ export default function SettingsDrawer() {
<Block title="Presets">
<ColorPresetsOptions />
</Block>

<Block title="Notifications">
<NotificationToggle />
</Block>
</Scrollbar>

<Box sx={{ p: SPACING, pt: 0 }}>
Expand Down
2 changes: 1 addition & 1 deletion apps/pwa/src/layouts/dashboard/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Box } from '@mui/material';
// hooks
import useResponsive from '../../hooks/useResponsive';
//
import Main from './Main';
import Main from './main';
import Header from './header';
import NavMini from './nav/NavMini';
import NavVertical from './nav/NavVertical';
Expand Down
Loading