Closer/functions/src/questions/onAnswerWritten.ts

109 lines
3.5 KiB
TypeScript

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<Record<string, unknown>>
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}`
)
})