const {onRequest} = require("firebase-functions/v2/https"); const {getAuth} = require("firebase-admin/auth"); const {getFirestore} = require("firebase-admin/firestore"); const logger = require("firebase-functions/logger"); const functions = require('firebase-functions'); const admin = require('firebase-admin'); const {Expo} = require('expo-server-sdk'); admin.initializeApp(); const db = admin.firestore(); let expo = new Expo({accessToken: process.env.EXPO_ACCESS_TOKEN}); // Firestore trigger that listens for new events in the 'Events' collection exports.sendNotificationOnEventCreation = functions.firestore .document('Events/{eventId}') .onCreate(async (snapshot, context) => { const eventData = snapshot.data(); const { familyId, creatorId } = eventData; if (!familyId || !creatorId) { console.error('Missing familyId or creatorId in event data'); return; } const pushTokens = await getPushTokensForFamilyExcludingCreator(familyId, creatorId); if (!pushTokens.length) { console.log('No push tokens available for the event.'); return; } let messages = []; for (let pushToken of pushTokens) { if (!Expo.isExpoPushToken(pushToken)) { console.error(`Push token ${pushToken} is not a valid Expo push token`); continue; } messages.push({ to: pushToken, sound: 'default', title: 'New Event Added!', body: `An event "${eventData.title}" has been added. Check it out!`, data: { eventId: context.params.eventId }, }); } let chunks = expo.chunkPushNotifications(messages); let tickets = []; for (let chunk of chunks) { try { let ticketChunk = await expo.sendPushNotificationsAsync(chunk); tickets.push(...ticketChunk); for (let ticket of ticketChunk) { if (ticket.status === 'ok') { console.log('Notification successfully sent:', ticket.id); } else if (ticket.status === 'error') { console.error(`Notification error: ${ticket.message}`); if (ticket.details && ticket.details.error) { console.error('Error details:', ticket.details.error); if (ticket.details.error === 'DeviceNotRegistered') { console.log(`Removing invalid push token: ${ticket.to}`); await removeInvalidPushToken(ticket.to); } } } } } catch (error) { console.error('Error sending notification:', error); } } // Retrieve and handle notification receipts let receiptIds = []; for (let ticket of tickets) { if (ticket.id) { receiptIds.push(ticket.id); } } let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds); for (let chunk of receiptIdChunks) { try { let receipts = await expo.getPushNotificationReceiptsAsync(chunk); console.log('Receipts:', receipts); for (let receiptId in receipts) { let { status, message, details } = receipts[receiptId]; if (status === 'ok') { console.log(`Notification with receipt ID ${receiptId} was delivered successfully`); } else if (status === 'error') { console.error(`Notification error: ${message}`); if (details && details.error) { console.error(`Error details: ${details.error}`); } } } } catch (error) { console.error('Error retrieving receipts:', error); } } return null; }); exports.createSubUser = onRequest(async (request, response) => { const authHeader = request.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { logger.warn("Missing or incorrect Authorization header", {authHeader}); response.status(401).json({error: 'Unauthorized'}); return; } try { const token = authHeader.split('Bearer ')[1]; logger.info("Verifying ID token", {token}); let decodedToken; try { decodedToken = await getAuth().verifyIdToken(token); logger.info("ID token verified successfully", {uid: decodedToken.uid}); } catch (verifyError) { logger.error("ID token verification failed", {error: verifyError.message}); response.status(401).json({error: 'Unauthorized: Invalid token'}); return; } logger.info("Processing user creation", {requestBody: request.body.data}); const {userType, firstName, lastName, email, password, familyId} = request.body.data; if (!email || !password || !firstName || !userType || !familyId) { logger.warn("Missing required fields in request body", {requestBody: request.body.data}); response.status(400).json({error: "Missing required fields"}); return; } let userRecord; try { userRecord = await getAuth().createUser({ email, password, displayName: `${firstName} ${lastName}`, }); logger.info("User record created", {userId: userRecord.uid}); } catch (createUserError) { logger.error("User creation failed", {error: createUserError.message}); response.status(500).json({error: "Failed to create user"}); return; } const userProfile = { userType, firstName, lastName, familyId, email, uid: userRecord.uid }; try { await getFirestore().collection("Profiles").doc(userRecord.uid).set(userProfile); logger.info("User profile saved to Firestore", {userId: userRecord.uid}); } catch (firestoreError) { logger.error("Failed to save user profile to Firestore", {error: firestoreError.message}); response.status(500).json({error: "Failed to save user profile"}); return; } response.status(200).json({ data: { message: "User created successfully", userId: userRecord.uid, } }); } catch (error) { logger.error("Error in createSubUser function", {error: error.message}); response.status(500).json({data: {error: error.message}}); } }); exports.generateCustomToken = onRequest(async (request, response) => { try { const {userId} = request.body.data; console.log("Generating custom token for userId", {userId}); if (!userId) { response.status(400).json({error: 'Missing userId'}); return; } const customToken = await getAuth().createCustomToken(userId); response.status(200).json({data: {token: customToken}}); } catch (error) { console.error("Error generating custom token", {error: error.message}); response.status(500).json({error: "Failed to generate custom token"}); } }); async function getPushTokensForEvent() { const usersRef = db.collection('Profiles'); const snapshot = await usersRef.get(); let pushTokens = []; snapshot.forEach(doc => { const data = doc.data(); if (data.pushToken) { pushTokens.push(data.pushToken); } }); console.log('Push Tokens:', pushTokens); return pushTokens; } async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) { const usersRef = db.collection('Profiles'); const snapshot = await usersRef.where('familyId', '==', familyId).get(); let pushTokens = []; snapshot.forEach(doc => { const data = doc.data(); // Exclude the creator if (data.uid !== creatorId && data.pushToken) { pushTokens.push(data.pushToken); } }); return pushTokens; }