Skip to content
Merged
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
28 changes: 19 additions & 9 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,28 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 10.20.0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22' # match your local node version
cache: 'npm'
cache: 'pnpm'

- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile

- name: Run ESLint
run: npm run lint
run: pnpm run lint

- name: Run TypeScript type check
run: npm run type-check
run: pnpm run type-check

- name: Build project
run: npm run build || echo "Build skipped due to network dependencies"
run: pnpm run build || echo "Build skipped due to network dependencies"


tests:
Expand All @@ -43,21 +48,26 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 10.20.0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache: 'pnpm'

- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile

# Uncomment when tests are added
# - name: Run tests
# run: npm test
# run: pnpm test

- name: Check for unused dependencies
run: npm run check-deps || echo "check-deps script not found, skipping"
run: pnpm run check-deps || echo "check-deps script not found, skipping"

pr-validation:
name: PR Validation
Expand Down
33 changes: 22 additions & 11 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import localFont from "next/font/local";
// @ts-ignore: allow importing global css without type declarations
import "antd/dist/reset.css";
// @ts-ignore: allow importing global css without type declarations
import "./globals.css";
import { Poppins } from "next/font/google";
import ClientProvider from "@/components/ClientProvider";
import { Toaster } from "@/components/ui/toaster";
import PWAInstallPrompt from "@/components/PWAInstallPrompt";

const poppins = Poppins({
subsets: ["latin"],
weight: ["700", "400"],
const poppins = localFont({
src: [
{
path: "../public/fonts/Poppins-Regular.ttf",
weight: "400",
style: "normal",
},
{
path: "../public/fonts/Poppins-Bold.ttf",
weight: "700",
style: "normal",
},
],
variable: "--font-poppins",
});
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
Expand Down Expand Up @@ -47,12 +57,13 @@ export const metadata: Metadata = {
title: "QueCode - Chat & Payment App",
description: "Chat with friends, send money, and manage groups - all in one place",
},
viewport: {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
},
};

export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
themeColor: "#00B512",
};

Expand Down
9 changes: 9 additions & 0 deletions components/chat/chat-page-clean.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useState, useEffect } from 'react';
import { usePushNotifications } from '@/hooks/use-push-notifications';
import ChatArea from '@/components/chat/chat-area';
import ConversationListLayout from '@/components/chat/conversation-list-layout';
import Navigation from '@/components/Navigation';
Expand All @@ -25,6 +26,7 @@ export default function ChatPageClean() {
const { getToken } = useAuthToken();
const token = getToken();
const { isExpanded } = useSidebar();
const { requestPermission } = usePushNotifications();

const {
conversations,
Expand Down Expand Up @@ -92,6 +94,13 @@ export default function ChatPageClean() {
}
}, [activeChat, conversations]);

// Request notification permission on mount
useEffect(() => {
if (token) {
requestPermission();
}
}, [token, requestPermission]);

const handleConversationSelect = (conversation: any) => {
setSelectedChat({
id: conversation.id,
Expand Down
96 changes: 96 additions & 0 deletions hooks/use-push-notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use client';

import { useEffect, useState } from 'react';
import { requestNotificationPermission, onMessageListener } from '@/lib/firebase';
import { useAuthToken } from './use-auth-token';
import axios from 'axios';

export function usePushNotifications() {
const [isSupported, setIsSupported] = useState(false);
const [permission, setPermission] = useState<NotificationPermission>('default');
const { getToken } = useAuthToken();

useEffect(() => {
if (typeof window !== 'undefined' && 'Notification' in window) {
setIsSupported(true);
setPermission(Notification.permission);
}
}, []);

const requestPermission = async () => {
if (!isSupported) return false;

try {
const fcmToken = await requestNotificationPermission();
if (fcmToken) {
setPermission('granted');
await registerToken(fcmToken);
return true;
}
return false;
} catch (error) {
console.error('Error requesting notification permission:', error);
return false;
}
};

const registerToken = async (fcmToken: string) => {
const authToken = getToken();
if (!authToken) return;

try {
await axios.put(
`${process.env.NEXT_PUBLIC_API_URL}/fcm/token`,
{ fcmToken },
{
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
} catch (error) {
console.error('Error registering FCM token:', error);
}
};

const unregisterToken = async () => {
const authToken = getToken();
if (!authToken) return;

try {
await axios.delete(`${process.env.NEXT_PUBLIC_API_URL}/fcm/token`, {
headers: {
Authorization: `Bearer ${authToken}`,
},
});
} catch (error) {
console.error('Error unregistering FCM token:', error);
}
};

useEffect(() => {
if (permission === 'granted') {
const unsubscribe = onMessageListener().then((payload: any) => {
console.log('Foreground message received:', payload);

if (payload.notification) {
new Notification(payload.notification.title, {
body: payload.notification.body,
icon: '/icon-192x192.png',
});
}
});

return () => {
unsubscribe.catch(console.error);
};
}
}, [permission]);

return {
isSupported,
permission,
requestPermission,
unregisterToken,
};
}
46 changes: 46 additions & 0 deletions lib/firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';

const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);

let messaging: any = null;
if (typeof window !== 'undefined') {
messaging = getMessaging(app);
}

export { messaging };

export const requestNotificationPermission = async (): Promise<string | null> => {
if (!messaging) return null;

try {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY,
});
return token;
}
return null;
} catch (error) {
console.error('Error getting notification permission:', error);
return null;
}
};

export const onMessageListener = () =>
new Promise((resolve) => {
if (!messaging) return;
onMessage(messaging, (payload) => {
resolve(payload);
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.18",
"firebase": "^12.7.0",
"jose": "^6.0.12",
"jspdf": "^3.0.4",
"lucide-react": "^0.469.0",
"moment": "^2.30.1",
"next": "14.2.14",
"pdfjs-dist": "^5.4.449",
"next-pwa": "^5.6.0",
"pdfjs-dist": "^5.4.449",
"qr-scanner": "^1.4.2",
"react": "^18",
"react-bootstrap": "^2.10.9",
Expand Down
Loading