import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' /** * Firestore trigger that sends an FCM notification to the other partner when * one partner writes an answer under * couples/{coupleId}/daily_question/{date}/answers/{userId}. * * The notification payload is a data message so the client can route directly to * the answer reveal screen. It also contains a `notification` block for * system-tray display when the app is in the background. */ export const onAnswerWritten = functions.firestore .document('couples/{coupleId}/daily_question/{date}/answers/{userId}') .onCreate(async (snap, context) => { const { coupleId, date, userId } = context.params as { coupleId: string date: string userId: string } const db = admin.firestore() const coupleDoc = await db.collection('couples').doc(coupleId).get() if (!coupleDoc.exists) { console.warn(`[onAnswerWritten] couple ${coupleId} not found`) return } const userIds = (coupleDoc.data()?.userIds ?? []) as string[] const partnerId = userIds.find((uid) => uid !== userId) if (!partnerId) { console.warn(`[onAnswerWritten] no partner found for couple ${coupleId}`) return } // Look up the partner's FCM tokens. We support a legacy `fcmToken` field // on the user doc and a dedicated `fcmTokens` subcollection for multiple devices. const tokens: string[] = [] const partnerUserDoc = await db.collection('users').doc(partnerId).get() if (partnerUserDoc.exists) { const legacyToken = partnerUserDoc.data()?.fcmToken if (typeof legacyToken === 'string' && legacyToken.length > 0) { tokens.push(legacyToken) } } const tokenSnapshot = await db .collection('users') .doc(partnerId) .collection('fcmTokens') .get() tokenSnapshot.docs.forEach((doc) => { const t = doc.data()?.token if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) { tokens.push(t) } }) if (tokens.length === 0) { console.log(`[onAnswerWritten] no FCM tokens for partner ${partnerId}`) return } // Respect the partner's notification preference (opt-out; default is enabled). const notifEnabled = partnerUserDoc.data()?.notifPartnerAnswered if (notifEnabled === false) { console.log(`[onAnswerWritten] partner ${partnerId} has partner-answered notifications off`) return } const answerData = snap.data() as Partial> const questionId = typeof answerData.questionId === 'string' ? answerData.questionId : '' const payload: admin.messaging.MessagingPayload = { notification: { title: 'Your partner just answered!', body: "See what they shared for tonight's prompt.", }, data: { type: 'partner_answered', coupleId, questionId, date, }, } const sendResults = await Promise.allSettled( tokens.map((token) => admin.messaging().send({ ...payload, token } as admin.messaging.Message) ) ) const failures: string[] = [] sendResults.forEach((result, index) => { if (result.status === 'rejected') { failures.push(`${tokens[index]}: ${String(result.reason)}`) } }) if (failures.length > 0) { console.error(`[onAnswerWritten] some notifications failed:`, failures) } console.log( `[onAnswerWritten] notified partner ${partnerId} for couple ${coupleId} question ${questionId}` ) })