Closer/functions/src/games/onGameSessionUpdate.ts

209 lines
6.3 KiB
TypeScript
Raw Normal View History

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
/**
* Firestore trigger that notifies partners when a game session is created or completed.
*
* Path: couples/{coupleId}/sessions/{sessionId}
* Condition: onWrite (create, update, delete)
*/
export const onGameSessionUpdate = functions.firestore
.document('couples/{coupleId}/sessions/{sessionId}')
.onWrite(async (change, context) => {
const { coupleId, sessionId } = context.params as { coupleId: string; sessionId: string }
const db = admin.firestore()
const messaging = admin.messaging()
// Get the session document
const sessionDoc = await db.collection('couples').doc(coupleId).collection('sessions').doc(sessionId).get()
const session = sessionDoc.data()
if (!session) {
console.log(`[onGameSessionUpdate] session ${sessionId} not found, skipping`)
return
}
// Get couple info
const coupleDoc = await db.collection('couples').doc(coupleId).get()
if (!coupleDoc.exists) {
console.warn(`[onGameSessionUpdate] couple ${coupleId} not found`)
return
}
const coupleData = coupleDoc.data() ?? {}
const userIds = (coupleData.userIds ?? []) as string[]
if (userIds.length !== 2) {
console.warn(`[onGameSessionUpdate] invalid couple ${coupleId}: expected 2 users, got ${userIds.length}`)
return
}
const partnerA = userIds[0]
const partnerB = userIds[1]
// Get user display names for notifications
const userA = await db.collection('users').doc(partnerA).get()
const userB = await db.collection('users').doc(partnerB).get()
const partnerAName = userA.data()?.displayName ?? 'Partner A'
const partnerBName = userB.data()?.displayName ?? 'Partner B'
// Check if session was just created (status = "active")
const previousData = change.before.data() ?? {}
const currentData = change.after.data() ?? {}
const wasInactive = (previousData.status ?? '') !== 'active'
const isActiveNow = currentData.status === 'active'
if (wasInactive && isActiveNow) {
// New session started - notify the other partner
const startedBy = currentData.startedByUserId
const gameType = currentData.gameType ?? 'wheel'
const partnerId = startedBy === partnerA ? partnerB : partnerA
const partnerName = startedBy === partnerA ? partnerBName : partnerAName
await notifyPartner(
db,
messaging,
partnerId,
partnerName,
gameType,
'partner_started_game',
`${partnerName} has started a game. Tap to join!`
)
return
}
// Check if session was completed
const wasActive = (previousData.status ?? '') === 'active'
const isCompletedNow = currentData.status === 'completed'
if (wasActive && isCompletedNow) {
const completedBy = currentData.startedByUserId
const partnerId = completedBy === partnerA ? partnerB : partnerA
const completingPartnerName = completedBy === partnerA ? partnerAName : partnerBName
// Check if partner has also completed
const partnerCompletedAt = currentData.partnerCompletedAt
if (partnerCompletedAt) {
// Both completed - notify both
await notifyPartner(
db,
messaging,
partnerA,
partnerAName,
currentData.gameType ?? 'wheel',
'partner_finished_game',
`${partnerBName} has finished the game. Tap to see the results!`
)
await notifyPartner(
db,
messaging,
partnerB,
partnerBName,
currentData.gameType ?? 'wheel',
'partner_finished_game',
`${partnerAName} has finished the game. Tap to see the results!`
)
} else {
// Only one completed - notify the other to continue
await notifyPartner(
db,
messaging,
partnerId,
completingPartnerName,
currentData.gameType ?? 'wheel',
'partner_finished_game',
`${completingPartnerName} has finished. Tap to continue playing!`
)
}
return
}
})
/**
* Send notification to partner via FCM and write to notification_queue.
*/
async function notifyPartner(
db: admin.firestore.Firestore,
messaging: admin.messaging.Messaging,
partnerId: string,
partnerName: string,
gameType: string,
notificationType: string,
body: string
): Promise<void> {
const notificationPayload = {
type: notificationType,
title: `${partnerName} is playing`,
body: body,
}
// Write an in-app notification record for the partner
await db
.collection('users')
.doc(partnerId)
.collection('notification_queue')
.add({
...notificationPayload,
read: false,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
})
// Collect the partner's FCM tokens
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(`[notifyPartner] no FCM tokens for ${partnerId}`)
return
}
const fcmMessage: admin.messaging.Message = {
token: tokens[0],
notification: {
title: notificationPayload.title,
body: notificationPayload.body,
},
data: {
type: notificationPayload.type,
gameType: gameType,
partnerId: partnerId,
},
}
const sendResults = await Promise.allSettled(
tokens.map((token) => messaging.send({ ...fcmMessage, token }))
)
const failures: string[] = []
sendResults.forEach((result, index) => {
if (result.status === 'rejected') {
failures.push(`${tokens[index]}: ${String(result.reason)}`)
}
})
if (failures.length > 0) {
console.error(`[notifyPartner] some notifications failed:`, failures)
} else {
console.log(`[notifyPartner] notified ${partnerId} (${notificationType})`)
}
}