Compare commits
5 commits
ffa4a0ed44
...
8fe3e4a301
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe3e4a301 | ||
|
|
002b73cf87 | ||
|
|
bce754d0a1 | ||
|
|
31395e5cc7 | ||
|
|
2a106b5467 |
16 changed files with 141 additions and 169 deletions
6
apps/admin-ui/.expo/types/router.d.ts
vendored
6
apps/admin-ui/.expo/types/router.d.ts
vendored
File diff suppressed because one or more lines are too long
|
|
@ -229,7 +229,6 @@ export default function Layout() {
|
||||||
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
|
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
|
||||||
<Drawer.Screen name="address-management" options={{ title: "Address Management" }} />
|
<Drawer.Screen name="address-management" options={{ title: "Address Management" }} />
|
||||||
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
|
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
|
||||||
<Drawer.Screen name="order-details/[id]" options={{ title: "Order Details" }} />
|
|
||||||
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
|
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
|
||||||
<Drawer.Screen name="user-management" options={{ title: "User Management" }} />
|
<Drawer.Screen name="user-management" options={{ title: "User Management" }} />
|
||||||
<Drawer.Screen name="send-notifications" options={{ title: "Send Notifications" }} />
|
<Drawer.Screen name="send-notifications" options={{ title: "Send Notifications" }} />
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export default function Layout() {
|
||||||
<Stack.Screen name="index" options={{ title: 'Manage Orders' }} />
|
<Stack.Screen name="index" options={{ title: 'Manage Orders' }} />
|
||||||
<Stack.Screen name="delivery-sequences" options={{ title: 'Delivery Sequences' }} />
|
<Stack.Screen name="delivery-sequences" options={{ title: 'Delivery Sequences' }} />
|
||||||
<Stack.Screen name="orders" options={{ title: 'Orders' }} />
|
<Stack.Screen name="orders" options={{ title: 'Orders' }} />
|
||||||
|
<Stack.Screen name="order-details" options={{ title: 'Order Details' }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -726,7 +726,7 @@ export default function DeliverySequences() {
|
||||||
}}
|
}}
|
||||||
onViewDetails={() => {
|
onViewDetails={() => {
|
||||||
if (selectedOrder) {
|
if (selectedOrder) {
|
||||||
router.push(`/order-details/${selectedOrder.id}`);
|
router.push(`/manage-orders/order-details/${selectedOrder.id}`);
|
||||||
}
|
}
|
||||||
setShowOrderMenu(false);
|
setShowOrderMenu(false);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export default function ManageOrders() {
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const target = getNavigationTarget();
|
const target = getNavigationTarget();
|
||||||
if (target) {
|
if (target) {
|
||||||
router.replace(target as any);
|
router.push(target as any);
|
||||||
}
|
}
|
||||||
}, [router, getNavigationTarget])
|
}, [router, getNavigationTarget])
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -348,12 +348,17 @@ export default function OrderDetails() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Customer Details */}
|
{/* Customer Details */}
|
||||||
<View
|
<TouchableOpacity
|
||||||
|
onPress={() => order.userId && router.push(`/(drawer)/user-management/${order.userId}`)}
|
||||||
|
activeOpacity={0.7}
|
||||||
style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}
|
style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}
|
||||||
>
|
>
|
||||||
<MyText style={tw`text-base font-bold text-gray-900 mb-4`}>
|
<View style={tw`flex-row items-center justify-between mb-4`}>
|
||||||
Customer Details
|
<MyText style={tw`text-base font-bold text-gray-900`}>
|
||||||
</MyText>
|
Customer Details
|
||||||
|
</MyText>
|
||||||
|
<MaterialIcons name="chevron-right" size={20} color="#6B7280" />
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={tw`flex-row items-center mb-4`}>
|
<View style={tw`flex-row items-center mb-4`}>
|
||||||
<View
|
<View
|
||||||
|
|
@ -363,7 +368,7 @@ export default function OrderDetails() {
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<MyText style={tw`text-sm font-bold text-gray-900`}>
|
<MyText style={tw`text-sm font-bold text-gray-900`}>
|
||||||
{order.customerName}
|
{order.customerName || 'Unknown User'}
|
||||||
</MyText>
|
</MyText>
|
||||||
<MyText style={tw`text-xs text-gray-500`}>Customer</MyText>
|
<MyText style={tw`text-xs text-gray-500`}>Customer</MyText>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -404,7 +409,7 @@ export default function OrderDetails() {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Order Items */}
|
{/* Order Items */}
|
||||||
<View
|
<View
|
||||||
|
|
@ -100,12 +100,12 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
|
||||||
const updateItemPackagingMutation = trpc.admin.order.updateOrderItemPackaging.useMutation();
|
const updateItemPackagingMutation = trpc.admin.order.updateOrderItemPackaging.useMutation();
|
||||||
|
|
||||||
const handleOrderPress = () => {
|
const handleOrderPress = () => {
|
||||||
router.push(`/order-details/${order.orderId}` as any);
|
router.push(`/manage-orders/order-details/${order.orderId}` as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMenuOption = () => {
|
const handleMenuOption = () => {
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
router.push(`/order-details/${order.orderId}` as any);
|
router.push(`/manage-orders/order-details/${order.orderId}` as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkPackaged = (isPackaged: boolean) => {
|
const handleMarkPackaged = (isPackaged: boolean) => {
|
||||||
|
|
|
||||||
|
|
@ -187,23 +187,6 @@ export default function SendNotifications() {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`p-4`}>
|
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`p-4`}>
|
||||||
{/* User Selection */}
|
|
||||||
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
|
||||||
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Select Users</MyText>
|
|
||||||
<BottomDropdown
|
|
||||||
label="Select Users"
|
|
||||||
value={selectedUserIds}
|
|
||||||
options={dropdownOptions}
|
|
||||||
onValueChange={(value) => setSelectedUserIds(value as number[])}
|
|
||||||
multiple={true}
|
|
||||||
placeholder="Select users"
|
|
||||||
onSearch={(query) => setSearchQuery(query)}
|
|
||||||
/>
|
|
||||||
<MyText style={tw`text-gray-500 text-sm mt-2`}>
|
|
||||||
{getDisplayText()}
|
|
||||||
</MyText>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Title Input */}
|
{/* Title Input */}
|
||||||
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
||||||
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Title</MyText>
|
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Title</MyText>
|
||||||
|
|
@ -228,8 +211,8 @@ export default function SendNotifications() {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Image Upload */}
|
{/* Image Upload - Hidden for now */}
|
||||||
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
{/* <View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
||||||
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Image (Optional)</MyText>
|
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Image (Optional)</MyText>
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
images={displayImage ? [displayImage] : []}
|
images={displayImage ? [displayImage] : []}
|
||||||
|
|
@ -237,6 +220,23 @@ export default function SendNotifications() {
|
||||||
onAddImage={handleImagePick}
|
onAddImage={handleImagePick}
|
||||||
onRemoveImage={handleRemoveImage}
|
onRemoveImage={handleRemoveImage}
|
||||||
/>
|
/>
|
||||||
|
</View> */}
|
||||||
|
|
||||||
|
{/* User Selection */}
|
||||||
|
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
||||||
|
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Select Users (Optional)</MyText>
|
||||||
|
<BottomDropdown
|
||||||
|
label="Select Users"
|
||||||
|
value={selectedUserIds}
|
||||||
|
options={dropdownOptions}
|
||||||
|
onValueChange={(value) => setSelectedUserIds(value as number[])}
|
||||||
|
multiple={true}
|
||||||
|
placeholder="Select users"
|
||||||
|
onSearch={(query) => setSearchQuery(query)}
|
||||||
|
/>
|
||||||
|
<MyText style={tw`text-gray-500 text-sm mt-2`}>
|
||||||
|
{getDisplayText()}
|
||||||
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export default function UserDetails() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleOrderPress = useCallback((orderId: number) => {
|
const handleOrderPress = useCallback((orderId: number) => {
|
||||||
router.push(`/(drawer)/order-details/${orderId}`);
|
router.push(`/(drawer)/manage-orders/order-details/${orderId}`);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const handleSuspensionToggle = useCallback((isSuspended: boolean) => {
|
const handleSuspensionToggle = useCallback((isSuspended: boolean) => {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
"expo-server-sdk": "^4.0.0",
|
"expo-server-sdk": "^4.0.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"firebase-admin": "^13.6.1",
|
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import admin from 'firebase-admin';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
// // Initialize Firebase Admin SDK
|
|
||||||
// const serviceAccountPath = path.join('.', 'creds', 'fcm-v1-account.json');
|
|
||||||
|
|
||||||
// if (!admin.apps.length) {
|
|
||||||
// admin.initializeApp({
|
|
||||||
// credential: admin.credential.cert(serviceAccountPath),
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export const messaging = admin.messaging();
|
|
||||||
// export default admin;
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { Queue, Worker } from 'bullmq';
|
import { Queue, Worker } from 'bullmq';
|
||||||
|
import { Expo } from 'expo-server-sdk';
|
||||||
import { redisUrl } from './env-exporter';
|
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 {
|
import {
|
||||||
NOTIFS_QUEUE,
|
NOTIFS_QUEUE,
|
||||||
ORDER_PLACED_MESSAGE,
|
ORDER_PLACED_MESSAGE,
|
||||||
|
|
@ -14,21 +19,88 @@ import {
|
||||||
export const notificationQueue = new Queue(NOTIFS_QUEUE, {
|
export const notificationQueue = new Queue(NOTIFS_QUEUE, {
|
||||||
connection: { url: redisUrl },
|
connection: { url: redisUrl },
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
removeOnComplete: 50,
|
removeOnComplete: true,
|
||||||
removeOnFail: 100,
|
removeOnFail: 10,
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const notificationWorker = new Worker(NOTIFS_QUEUE, async (job) => {
|
export const notificationWorker = new Worker(NOTIFS_QUEUE, async (job) => {
|
||||||
if (!job) return;
|
if (!job) return;
|
||||||
console.log(`Processing notification job ${job.id}`);
|
|
||||||
// TODO: Implement sendPushNotification
|
const { name, data } = job;
|
||||||
|
console.log(`Processing notification job ${job.id} - ${name}`);
|
||||||
|
|
||||||
|
if (name === 'send-admin-notification') {
|
||||||
|
await sendAdminNotification(data);
|
||||||
|
} else if (name === 'send-notification') {
|
||||||
|
// Handle legacy notification type
|
||||||
|
console.log('Legacy notification job - not implemented yet');
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
connection: { url: redisUrl },
|
connection: { url: redisUrl },
|
||||||
concurrency: 5,
|
concurrency: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function sendAdminNotification(data: {
|
||||||
|
userId: number;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Validate Expo push token
|
||||||
|
if (!Expo.isExpoPushToken(token)) {
|
||||||
|
console.error(`Invalid Expo push token for user ${userId}: ${token}`);
|
||||||
|
throw new Error('Invalid push token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate signed URL for image if provided
|
||||||
|
const signedImageUrl = imageUrl ? await generateSignedUrlFromS3Url(imageUrl) : null;
|
||||||
|
|
||||||
|
// Send notification
|
||||||
|
const expo = new Expo();
|
||||||
|
const message = {
|
||||||
|
to: token,
|
||||||
|
sound: 'default',
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
data: { imageUrl },
|
||||||
|
...(signedImageUrl ? {
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
url: signedImageUrl,
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [ticket] = await expo.sendPushNotificationsAsync([message]);
|
||||||
|
console.log(`Notification sent to user ${userId}:`, ticket);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to send notification to user ${userId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
notificationWorker.on('completed', (job) => {
|
notificationWorker.on('completed', (job) => {
|
||||||
if (job) console.log(`Notification job ${job.id} completed`);
|
if (job) console.log(`Notification job ${job.id} completed`);
|
||||||
});
|
});
|
||||||
|
|
@ -108,4 +180,4 @@ export async function sendRefundInitiatedNotification(userId: number, orderId?:
|
||||||
process.on('SIGTERM', async () => {
|
process.on('SIGTERM', async () => {
|
||||||
await notificationQueue.close();
|
await notificationQueue.close();
|
||||||
await notificationWorker.close();
|
await notificationWorker.close();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,7 @@ export const orderRouter = router({
|
||||||
return {
|
return {
|
||||||
id: orderData.id,
|
id: orderData.id,
|
||||||
readableId: orderData.id,
|
readableId: orderData.id,
|
||||||
|
userId: orderData.user.id,
|
||||||
customerName: `${orderData.user.name}`,
|
customerName: `${orderData.user.name}`,
|
||||||
customerEmail: orderData.user.email,
|
customerEmail: orderData.user.email,
|
||||||
customerMobile: orderData.user.mobile,
|
customerMobile: orderData.user.mobile,
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,7 @@ import { db } from '../../db/db_index';
|
||||||
import { users, complaints, orders, orderItems, notifCreds, userNotifications, userDetails } from '../../db/schema';
|
import { users, complaints, orders, orderItems, notifCreds, userNotifications, userDetails } from '../../db/schema';
|
||||||
import { eq, sql, desc, asc, count, max } from 'drizzle-orm';
|
import { eq, sql, desc, asc, count, max } from 'drizzle-orm';
|
||||||
import { ApiError } from '../../lib/api-error';
|
import { ApiError } from '../../lib/api-error';
|
||||||
import { Expo } from 'expo-server-sdk';
|
import { notificationQueue } from '../../lib/notif-job';
|
||||||
// import { messaging } from '../../lib/firebase';
|
|
||||||
import { generateSignedUrlFromS3Url } from '../../lib/s3-client';
|
|
||||||
|
|
||||||
// Toggle between notification providers: 'expo' | 'fcm'
|
|
||||||
// const NOTIFICATION_PROVIDER: 'expo' | 'fcm' = 'fcm';
|
|
||||||
// let NOTIFICATION_PROVIDER: 'expo' | 'fcm' = 'expo';
|
|
||||||
let NOTIFICATION_PROVIDER: string = 'expo';
|
|
||||||
|
|
||||||
async function createUserByMobile(mobile: string): Promise<typeof users.$inferSelect> {
|
async function createUserByMobile(mobile: string): Promise<typeof users.$inferSelect> {
|
||||||
// Clean mobile number (remove non-digits)
|
// Clean mobile number (remove non-digits)
|
||||||
|
|
@ -378,125 +371,40 @@ export const userRouter = {
|
||||||
const { userIds, title, text, imageUrl } = input;
|
const { userIds, title, text, imageUrl } = input;
|
||||||
|
|
||||||
// Store notification in database
|
// Store notification in database
|
||||||
await db.insert(userNotifications).values({
|
const [notification] = await db.insert(userNotifications).values({
|
||||||
title,
|
title,
|
||||||
body: text,
|
body: text,
|
||||||
imageUrl: imageUrl || null,
|
imageUrl: imageUrl || null,
|
||||||
applicableUsers: userIds.length > 0 ? userIds : null,
|
applicableUsers: userIds.length > 0 ? userIds : null,
|
||||||
});
|
}).returning();
|
||||||
|
|
||||||
// Fetch push tokens for target users
|
// Queue one job per user
|
||||||
const tokens = await db
|
let queuedCount = 0;
|
||||||
.select({ token: notifCreds.token, userId: notifCreds.userId })
|
for (const userId of userIds) {
|
||||||
.from(notifCreds)
|
|
||||||
.where(sql`${notifCreds.userId} IN (${sql.join(userIds, sql`, `)})`);
|
|
||||||
|
|
||||||
if (tokens.length === 0) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: 'Notification saved but no push tokens found'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate signed URL for image if provided
|
|
||||||
const signedImageUrl = imageUrl ? await generateSignedUrlFromS3Url(imageUrl) : null;
|
|
||||||
|
|
||||||
let sentCount = 0;
|
|
||||||
let failedCount = 0;
|
|
||||||
|
|
||||||
if (NOTIFICATION_PROVIDER === 'fcm') {
|
|
||||||
// Build FCM messages
|
|
||||||
const messages = tokens.map(({ token }) => ({
|
|
||||||
token: token,
|
|
||||||
notification: {
|
|
||||||
title: title,
|
|
||||||
body: text,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
imageUrl: imageUrl || '',
|
|
||||||
},
|
|
||||||
android: {
|
|
||||||
notification: {
|
|
||||||
imageUrl: signedImageUrl || undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apns: {
|
|
||||||
payload: {
|
|
||||||
aps: {
|
|
||||||
'mutable-content': 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fcm_options: {
|
|
||||||
image: signedImageUrl || undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Send notifications using Firebase Admin SDK
|
|
||||||
try {
|
try {
|
||||||
// const response = await messaging.sendEach(messages);
|
await notificationQueue.add('send-admin-notification', {
|
||||||
// sentCount = response.successCount;
|
userId,
|
||||||
// failedCount = response.failureCount;
|
title,
|
||||||
|
body: text,
|
||||||
// // Log failed tokens
|
imageUrl: imageUrl || null,
|
||||||
// response.responses.forEach((resp, idx) => {
|
notificationId: notification.id,
|
||||||
// if (!resp.success) {
|
}, {
|
||||||
// console.error(`Failed to send to token ${tokens[idx].token}:`, resp.error);
|
attempts: 3,
|
||||||
// }
|
backoff: {
|
||||||
// });
|
type: 'exponential',
|
||||||
|
delay: 2000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
queuedCount++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending push notifications:', error);
|
console.error(`Failed to queue notification for user ${userId}:`, error);
|
||||||
throw new ApiError('Failed to send push notifications', 500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Send using Expo (legacy)
|
|
||||||
const expo = new Expo();
|
|
||||||
|
|
||||||
// Helper function to chunk array
|
|
||||||
const chunkArray = (array: any[], size: number) => {
|
|
||||||
const chunks = [];
|
|
||||||
for (let i = 0; i < array.length; i += size) {
|
|
||||||
chunks.push(array.slice(i, i + size));
|
|
||||||
}
|
|
||||||
return chunks;
|
|
||||||
};
|
|
||||||
|
|
||||||
const chunks = chunkArray(tokens, 50);
|
|
||||||
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
const messages = chunk
|
|
||||||
.filter(({ token }) => Expo.isExpoPushToken(token))
|
|
||||||
.map(({ token }) => ({
|
|
||||||
to: token,
|
|
||||||
sound: 'default',
|
|
||||||
title,
|
|
||||||
body: text,
|
|
||||||
data: { imageUrl },
|
|
||||||
...(signedImageUrl ? {
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
url: signedImageUrl,
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} : {}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (messages.length > 0) {
|
|
||||||
try {
|
|
||||||
await expo.sendPushNotificationsAsync(messages);
|
|
||||||
sentCount += messages.length;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending push notifications:', error);
|
|
||||||
failedCount += chunk.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Notification sent to ${sentCount} users${failedCount > 0 ? `, ${failedCount} failed` : ''}`,
|
message: `Notification queued for ${queuedCount} users`,
|
||||||
|
notificationId: notification.id,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
@ -40,6 +40,7 @@ export async function registerForPushNotificationsAsync() {
|
||||||
projectId,
|
projectId,
|
||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
|
return pushTokenString;
|
||||||
// const pushTokenString = await Notifications.getDevicePushTokenAsync();
|
// const pushTokenString = await Notifications.getDevicePushTokenAsync();
|
||||||
// return pushTokenString.data;
|
// return pushTokenString.data;
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue