Closer/functions/src/dates/createDateMatch.ts

98 lines
3.3 KiB
TypeScript

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
/**
* Fires the "It's a match!" notification when a date match is created.
*
* Trigger: couples/{coupleId}/date_matches/{dateIdeaId} (onCreate)
*
* Date swipes are E2E-encrypted, so the server can no longer detect mutual love.
* Mutual-match detection now happens client-side (whichever partner records the
* second LOVE writes the match marker, validated by Firestore rules). This trigger
* only sends the push to both partners — it never reads swipe content.
*
* Idempotency: `fcmNotified` is claimed in a transaction so concurrent invocations
* (or a client retry) never double-send. The match doc id is the date idea id, so
* the marker itself is already de-duplicated by the client transaction + rules.
*/
export const notifyOnDateMatch = functions.firestore
.document('couples/{coupleId}/date_matches/{dateIdeaId}')
.onCreate(async (snap, context) => {
if (!snap.exists) return
const { coupleId, dateIdeaId } = context.params as {
coupleId: string
dateIdeaId: string
}
const db = admin.firestore()
const matchRef = snap.ref
// Atomically claim the FCM send so concurrent invocations don't double-send.
const shouldSend = await db.runTransaction(async (tx) => {
const doc = await tx.get(matchRef)
if (!doc.exists || doc.data()?.fcmNotified === true) return false
tx.update(matchRef, { fcmNotified: true })
return true
})
if (!shouldSend) return
const coupleDoc = await db.collection('couples').doc(coupleId).get()
const userIds = (coupleDoc.data()?.userIds ?? []) as string[]
await Promise.all(userIds.map((uid) => notifyDateMatch(db, uid, coupleId, dateIdeaId)))
})
async function notifyDateMatch(
db: admin.firestore.Firestore,
userId: string,
coupleId: string,
dateIdeaId: string
): Promise<void> {
await db.collection('users').doc(userId).collection('notification_queue').add({
type: 'date_match',
title: "It's a match!",
body: "You both want to go on this date. Time to make it happen.",
read: false,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
})
const tokens = await getUserTokens(db, userId)
if (tokens.length === 0) return
await Promise.allSettled(
tokens.map((token) =>
admin.messaging().send({
token,
notification: {
title: "It's a match!",
body: "You both want to go on this date. Time to make it happen.",
},
android: { notification: { channelId: 'partner_activity' } }, // E-OBS
data: {
type: 'date_match',
couple_id: coupleId,
date_idea_id: dateIdeaId,
},
})
)
)
}
async function getUserTokens(
db: admin.firestore.Firestore,
userId: string
): Promise<string[]> {
const tokens: string[] = []
const userDoc = await db.collection('users').doc(userId).get()
const legacy = userDoc.data()?.fcmToken
if (typeof legacy === 'string' && legacy.length > 0) tokens.push(legacy)
const snap = await db.collection('users').doc(userId).collection('fcmTokens').get()
snap.docs.forEach((doc) => {
const t = doc.data()?.token
if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) tokens.push(t)
})
return tokens
}