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 { 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.", }, data: { type: 'date_match', couple_id: coupleId, date_idea_id: dateIdeaId, }, }) ) ) } async function getUserTokens( db: admin.firestore.Firestore, userId: string ): Promise { 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 }