From 7fa44712bfd9229ff24824fba5f25af6606cc6b9 Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:51:02 +0530 Subject: [PATCH] enh --- .../app/(drawer)/send-notifications/index.tsx | 28 +++++++++--- apps/backend/src/lib/notif-job.ts | 29 +++---------- apps/backend/src/trpc/admin-apis/user.ts | 43 ++++++++++++------- 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/apps/admin-ui/app/(drawer)/send-notifications/index.tsx b/apps/admin-ui/app/(drawer)/send-notifications/index.tsx index ff06e44..e4673c0 100644 --- a/apps/admin-ui/app/(drawer)/send-notifications/index.tsx +++ b/apps/admin-ui/app/(drawer)/send-notifications/index.tsx @@ -106,9 +106,20 @@ export default function SendNotifications() { return; } - if (selectedUserIds.length === 0) { - Alert.alert('Error', 'Please select at least one user'); - return; + // Check if sending to all users + const isSendingToAll = selectedUserIds.length === 0; + if (isSendingToAll) { + const confirmed = await new Promise((resolve) => { + Alert.alert( + 'Send to All Users?', + 'This will send the notification to all users with push tokens. Continue?', + [ + { text: 'Cancel', style: 'cancel', onPress: () => resolve(false) }, + { text: 'Send', style: 'default', onPress: () => resolve(true) }, + ] + ); + }); + if (!confirmed) return; } try { @@ -153,7 +164,7 @@ export default function SendNotifications() { }; const getDisplayText = () => { - if (selectedUserIds.length === 0) return 'Select users'; + if (selectedUserIds.length === 0) return 'All Users'; if (selectedUserIds.length === 1) { const user = eligibleUsers.find((u: User) => u.id === selectedUserIds[0]); return user ? `${user.mobile}${user.name ? ` - ${user.name}` : ''}` : '1 user selected'; @@ -237,20 +248,23 @@ export default function SendNotifications() { {getDisplayText()} + + Leave empty to send to all users + {/* Submit Button */} - {sendNotification.isPending ? 'Sending...' : 'Send Notification'} + {sendNotification.isPending ? 'Sending...' : selectedUserIds.length === 0 ? 'Send to All Users' : 'Send Notification'} diff --git a/apps/backend/src/lib/notif-job.ts b/apps/backend/src/lib/notif-job.ts index ce154b3..f534843 100644 --- a/apps/backend/src/lib/notif-job.ts +++ b/apps/backend/src/lib/notif-job.ts @@ -2,8 +2,6 @@ import { Queue, Worker } from 'bullmq'; import { Expo } from 'expo-server-sdk'; import { redisUrl } from './env-exporter'; import { db } from '../db/db_index'; -import { notifCreds } from '../db/schema'; -import { eq } from 'drizzle-orm'; import { generateSignedUrlFromS3Url } from './s3-client'; import { NOTIFS_QUEUE, @@ -43,32 +41,17 @@ export const notificationWorker = new Worker(NOTIFS_QUEUE, async (job) => { }); async function sendAdminNotification(data: { - userId: number; + token: string; title: string; body: string; imageUrl: string | null; - notificationId: number; }) { - const { userId, title, body, imageUrl } = data; - - // Get user's push token - const [cred] = await db - .select({ token: notifCreds.token }) - .from(notifCreds) - .where(eq(notifCreds.userId, userId)) - .limit(1); - - if (!cred || !cred.token) { - console.log(`No push token found for user ${userId}`); - return; - } - - const token = cred.token; + const { token, title, body, imageUrl } = data; // Validate Expo push token if (!Expo.isExpoPushToken(token)) { - console.error(`Invalid Expo push token for user ${userId}: ${token}`); - throw new Error('Invalid push token'); + console.error(`Invalid Expo push token: ${token}`); + return; } // Generate signed URL for image if provided @@ -94,9 +77,9 @@ async function sendAdminNotification(data: { try { const [ticket] = await expo.sendPushNotificationsAsync([message]); - console.log(`Notification sent to user ${userId}:`, ticket); + console.log(`Notification sent:`, ticket); } catch (error) { - console.error(`Failed to send notification to user ${userId}:`, error); + console.error(`Failed to send notification:`, error); throw error; } } diff --git a/apps/backend/src/trpc/admin-apis/user.ts b/apps/backend/src/trpc/admin-apis/user.ts index 59ea561..cc83090 100644 --- a/apps/backend/src/trpc/admin-apis/user.ts +++ b/apps/backend/src/trpc/admin-apis/user.ts @@ -1,8 +1,8 @@ import { protectedProcedure } from '../trpc-index'; import { z } from 'zod'; import { db } from '../../db/db_index'; -import { users, complaints, orders, orderItems, notifCreds, userNotifications, userDetails } from '../../db/schema'; -import { eq, sql, desc, asc, count, max } from 'drizzle-orm'; +import { users, complaints, orders, orderItems, notifCreds, unloggedUserTokens, userDetails } from '../../db/schema'; +import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm'; import { ApiError } from '../../lib/api-error'; import { notificationQueue } from '../../lib/notif-job'; @@ -362,7 +362,7 @@ export const userRouter = { sendNotification: protectedProcedure .input(z.object({ - userIds: z.array(z.number()), + userIds: z.array(z.number()).default([]), title: z.string().min(1, 'Title is required'), text: z.string().min(1, 'Message is required'), imageUrl: z.string().optional(), @@ -370,24 +370,36 @@ export const userRouter = { .mutation(async ({ input }) => { const { userIds, title, text, imageUrl } = input; - // Store notification in database - const [notification] = await db.insert(userNotifications).values({ - title, - body: text, - imageUrl: imageUrl || null, - applicableUsers: userIds.length > 0 ? userIds : null, - }).returning(); + let tokens: string[] = []; - // Queue one job per user + if (userIds.length === 0) { + // Send to all users - get tokens from both logged-in and unlogged users + const loggedInTokens = await db.select({ token: notifCreds.token }).from(notifCreds); + const unloggedTokens = await db.select({ token: unloggedUserTokens.token }).from(unloggedUserTokens); + + tokens = [ + ...loggedInTokens.map(t => t.token), + ...unloggedTokens.map(t => t.token) + ]; + } else { + // Send to specific users - get their tokens + const userTokens = await db + .select({ token: notifCreds.token }) + .from(notifCreds) + .where(inArray(notifCreds.userId, userIds)); + + tokens = userTokens.map(t => t.token); + } + + // Queue one job per token let queuedCount = 0; - for (const userId of userIds) { + for (const token of tokens) { try { await notificationQueue.add('send-admin-notification', { - userId, + token, title, body: text, imageUrl: imageUrl || null, - notificationId: notification.id, }, { attempts: 3, backoff: { @@ -397,14 +409,13 @@ export const userRouter = { }); queuedCount++; } catch (error) { - console.error(`Failed to queue notification for user ${userId}:`, error); + console.error(`Failed to queue notification for token:`, error); } } return { success: true, message: `Notification queued for ${queuedCount} users`, - notificationId: notification.id, }; }), }; \ No newline at end of file