enh
This commit is contained in:
parent
5bd0f8ded7
commit
7fa44712bf
3 changed files with 54 additions and 46 deletions
|
|
@ -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<boolean>((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() {
|
|||
<MyText style={tw`text-gray-500 text-sm mt-2`}>
|
||||
{getDisplayText()}
|
||||
</MyText>
|
||||
<MyText style={tw`text-blue-600 text-xs mt-1`}>
|
||||
Leave empty to send to all users
|
||||
</MyText>
|
||||
</View>
|
||||
|
||||
{/* Submit Button */}
|
||||
<TouchableOpacity
|
||||
onPress={handleSend}
|
||||
disabled={sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0 || selectedUserIds.length === 0}
|
||||
disabled={sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0}
|
||||
style={tw`${
|
||||
sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0 || selectedUserIds.length === 0
|
||||
sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0
|
||||
? 'bg-gray-300'
|
||||
: 'bg-blue-600'
|
||||
} rounded-xl py-4 items-center shadow-sm`}
|
||||
>
|
||||
<MyText style={tw`text-white font-bold text-base`}>
|
||||
{sendNotification.isPending ? 'Sending...' : 'Send Notification'}
|
||||
{sendNotification.isPending ? 'Sending...' : selectedUserIds.length === 0 ? 'Send to All Users' : 'Send Notification'}
|
||||
</MyText>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}),
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue