Closer/functions/src/games/onGameSessionUpdate.ts

191 lines
6.1 KiB
TypeScript

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'
const avatarA = userA.data()?.photoUrl as string | undefined
const avatarB = userB.data()?.photoUrl as string | undefined
// 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, naming the person who started it.
const startedBy = currentData.startedByUserId
const gameType = currentData.gameType ?? 'wheel'
const recipientId = startedBy === partnerA ? partnerB : partnerA
const starterName = startedBy === partnerA ? partnerAName : partnerBName
const starterAvatar = startedBy === partnerA ? avatarA : avatarB
await notifyPartner(
db, messaging, recipientId, starterName, gameType,
'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId,
starterAvatar
)
return
}
// Check if session was completed
const wasActive = (previousData.status ?? '') === 'active'
const isCompletedNow = currentData.status === 'completed'
if (wasActive && isCompletedNow) {
// The session is complete (both partners have answered) — the reveal is ready for each of
// them, so notify BOTH, each naming the OTHER partner.
const gt = currentData.gameType ?? 'wheel'
await notifyPartner(
db, messaging, partnerA, partnerBName, gt,
'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId,
avatarB
)
await notifyPartner(
db, messaging, partnerB, partnerAName, gt,
'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId,
avatarA
)
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,
coupleId: string,
senderAvatarUrl?: string
): Promise<void> {
const title =
notificationType === 'partner_finished_game'
? `${partnerName} finished the game`
: `${partnerName} is playing`
const notificationPayload = {
type: notificationType,
title,
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,
couple_id: coupleId,
game_type: gameType,
...(senderAvatarUrl && senderAvatarUrl.length > 0
? { sender_avatar_url: senderAvatarUrl }
: {}),
},
}
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})`)
}
}