diff --git a/functions/src/index.ts b/functions/src/index.ts index c5f1c80..4812211 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -20,7 +20,7 @@ if (!admin.apps.length) { admin.initializeApp(); } export const sendEmailWithAttachment = onRequest({ - region: '#{SERVICES_RGN}#' + region: process.env.SERVICES_RGN }, async (request: Request, response: express.Response) => { try { const { toAddress, subject, message, fileUrl, fileName } = request.body; @@ -134,10 +134,13 @@ interface Invitation { phoneNumber: string; gymName: string; invitedByName: string; + status: string; + notificationSent: boolean; + subscriptionName?: string; } -export const notifyInvitation = onDocumentCreated({ - document: 'notifications/{notificationId}', +export const sendInvitationNotification = onDocumentCreated({ + document: 'client_invitations/{invitationId}', region: process.env.SERVICES_RGN }, async (event: any) => { const invitation = event.data?.data() as Invitation; @@ -148,41 +151,60 @@ export const notifyInvitation = onDocumentCreated({ return null; } + if (invitation.status !== 'pending') { + console.log(`Invitation ${invitationId} is not pending, skipping notification.`); + return null; + } + + if (invitation.notificationSent) { + console.log(`Invitation notification ${invitationId} already sent, skipping.`); + return null; + } + try { + if (!invitation.phoneNumber) { + console.error('Phone number not found in invitation.'); + return null; + } + const userQuery = await admin .firestore() .collection('users') - .where('email', '==', invitation.email) .where('phoneNumber', '==', invitation.phoneNumber) .limit(1) .get(); - + if (userQuery.empty) { - console.log( - `User not found for email: ${invitation.email} and phone: ${invitation.phoneNumber}.` - ); + console.log(`User not found for phone: ${invitation.phoneNumber}`); return null; } - - const user = userQuery.docs[0].data(); - const fcmToken = user.fcmToken; + + const userDoc = userQuery.docs[0]; + const user = userDoc.data(); + const fcmToken = user?.fcmToken; if (!fcmToken) { - console.log(`FCM token not found for user: ${invitation.email}.`); + console.log(`FCM token not found for user with phone: ${invitation.phoneNumber}`); return null; } + const title = 'New Gym Invitation'; + const body = `${invitation.invitedByName} has invited you to join ${invitation.gymName}`; + + const data: Record = { + type: 'client_invitation', + invitationId: invitationId, + gymName: invitation.gymName || '', + senderName: invitation.invitedByName || '', + subscriptionName: invitation.subscriptionName || '' + }; + const message: admin.messaging.Message = { notification: { - title: 'New Gym Invitation', - body: `${invitation.invitedByName} has invited you to join ${invitation.gymName}`, - }, - data: { - type: 'invitation', - invitationId: invitationId, - gymName: invitation.gymName, - senderName: invitation.invitedByName, + title: title, + body: body, }, + data: data, android: { priority: 'high', notification: { @@ -194,11 +216,25 @@ export const notifyInvitation = onDocumentCreated({ clickAction: 'FLUTTER_NOTIFICATION_CLICK', }, }, + apns: { + payload: { + aps: { + sound: 'default', + badge: 1, + }, + }, + }, token: fcmToken, }; await admin.messaging().send(message); - console.log(`Invitation notification sent to ${invitation.email}.`); + console.log(`Invitation notification sent to user with phone: ${invitation.phoneNumber}`); + + await admin.firestore().collection('client_invitations').doc(invitationId).update({ + notificationSent: true, + notificationSentAt: admin.firestore.FieldValue.serverTimestamp() + }); + return null; } catch (error) { console.error('Error sending invitation notification:', error); @@ -257,7 +293,7 @@ export const createCashfreeOrder = onRequest({ }, order_meta: { return_url: `https://fitlien.com/payment/status?order_id={order_id}`, - // notify_url: `https://$filien.web.app/verifyCashfreePayment` + notify_url: `https://filien.web.app/verifyCashfreePayment` }, order_note: productInfo || 'Fitlien Membership' }, @@ -352,3 +388,183 @@ export const verifyCashfreePayment = onRequest({ }); } }); + +export const sendFCMNotificationByType = onRequest({ + region: process.env.SERVICES_RGN +}, async (request: Request, response: express.Response) => { + try { + const notificationType = request.body.type || request.query.type; + const userId = request.body.userId || request.body.clientId || request.query.userId || request.query.clientId || request.body.invitorId || request.query.invitorId; + + if (!notificationType) { + response.status(400).json({ error: 'Notification type is required' }); + return; + } + + if (!userId) { + response.status(400).json({ error: 'User ID is required' }); + return; + } + + const userDoc = await admin.firestore().collection('users').doc(userId).get(); + + if (!userDoc.exists) { + response.status(404).json({ error: `User not found for ID: ${userId}` }); + return; + } + + const user = userDoc.data(); + const fcmToken = user?.fcmToken; + + if (!fcmToken) { + response.status(400).json({ error: `FCM token not found for user: ${userId}` }); + return; + } + + const userIdQuery = admin.firestore().collection('notifications') + .where('userId', '==', userId) + .where('type', '==', notificationType); + + const clientIdQuery = admin.firestore().collection('notifications') + .where('clientId', '==', userId) + .where('type', '==', notificationType); + + const invitorIdQuery = admin.firestore().collection('notifications') + .where('invitorId', '==', userId) + .where('type', '==', notificationType); + + const [userIdResults, clientIdResults, invitorIdResults] = await Promise.all([ + userIdQuery.get(), + clientIdQuery.get(), + invitorIdQuery.get() + ]); + + const notificationDocs = [...userIdResults.docs, ...clientIdResults.docs, ...invitorIdResults.docs] + .filter(doc => { + const data = doc.data(); + return data.notificationSent !== true; + }); + + if (notificationDocs.length === 0) { + response.status(404).json({ + error: `No unsent notifications of type '${notificationType}' found for user/client ${userId}` + }); + return; + } + + const results = []; + + for (const doc of notificationDocs) { + const notification = doc.data(); + const docId = doc.id; + if (notification.notificationSent) { + logger.info(`Notification ${notificationType} already sent, skipping.`); + continue; + } + + let title = 'New Notification'; + let body = notification.message || 'You have a new notification'; + let data: Record = { + type: notification.type, + }; + + switch (notification.type) { + case 'day_pass_entry': + const isAccepted = notification.status === 'ACCEPTED'; + title = isAccepted ? 'Day Pass Approved' : 'Day Pass Denied'; + body = notification.message || (isAccepted ? + `Your day pass has been approved` : + `Your day pass has been denied`); + data.gymName = notification.gymName || ''; + break; + + case 'trainer_assigned_to_client': + title = 'Trainer Assigned'; + body = notification.message || `${notification.trainerName} has been assigned as your trainer`; + data.trainerName = notification.trainerName || ''; + data.membershipId = notification.membershipId || ''; + break; + + case 'client_invitations': + const isAccept = notification.status === 'ACCEPTED'; + title = isAccept ? 'Invitation Accepted' : 'Invitation Rejected'; + body = notification.message || (isAccept ? + `The invitation you shared has been accepted` : + `The invitation you shared has been rejected`); + data.gymName = notification.gymName || ''; + data.clientEmail = notification.clientEmail || ''; + data.clientName = notification.name || ''; + data.invitationId = notification.invitationId || ''; + data.subcriptionname= notification.subcriptionname || ''; + break; + + default: + logger.info(`Using default handling for notification type: ${notification.type}`); + break; + } + + const message: admin.messaging.Message = { + notification: { + title: title, + body: body, + }, + data: data, + android: { + priority: 'high', + notification: { + channelId: 'notifications_channel', + priority: 'high', + defaultSound: true, + defaultVibrateTimings: true, + icon: '@mipmap/ic_launcher', + clickAction: 'FLUTTER_NOTIFICATION_CLICK', + }, + }, + apns: { + payload: { + aps: { + sound: 'default', + badge: 1, + }, + }, + }, + token: fcmToken, + }; + + try { + const fcmResponse = await admin.messaging().send(message); + logger.info(`FCM notification sent to user ${userId} for type: ${notification.type}`); + + await admin.firestore().collection('notifications').doc(docId).update({ + notificationSent: true, + sentAt: admin.firestore.FieldValue?.serverTimestamp?.() || new Date() + }); + + results.push({ + success: true, + messageId: fcmResponse + }); + } catch (error) { + logger.error(`Error sending notification ${notificationType}:`, error); + results.push({ + success: false, + type: notificationType, + error: error instanceof Error ? error.message : String(error) + }); + } + } + + response.json({ + success: true, + processed: results.length, + results: results + }); + + } catch (error) { + logger.error('Error processing notifications:', error); + response.status(500).json({ + success: false, + error: error instanceof Error ? error.message : String(error) + }); + } +});