102 lines
3.2 KiB
TypeScript
102 lines
3.2 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
|
||
|
|
}
|
||
|
|
|
||
|
|
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}`
|
||
|
|
)
|
||
|
|
})
|