"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.sendChallengeDayReminders = exports.unlockDueMemoryCapsules = void 0; const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); const DAY_MS = 24 * 60 * 60 * 1000; const CHALLENGE_TITLES = { gratitude_week: { title: 'Gratitude Week', durationDays: 7 }, appreciation_notes: { title: 'Appreciation Notes', durationDays: 7 }, quality_time: { title: 'Quality Time', durationDays: 7 }, deep_conversations: { title: 'Deep Conversations', durationDays: 7 }, }; exports.unlockDueMemoryCapsules = functions.pubsub .schedule('every 1 hours') .onRun(async () => { const db = admin.firestore(); const messaging = admin.messaging(); const now = Date.now(); const snapshot = await db .collectionGroup('capsules') .where('status', '==', 'sealed') .where('unlockAt', '<=', now) .limit(100) .get(); const notifications = []; for (const capsuleDoc of snapshot.docs) { const capsuleRef = capsuleDoc.ref; const coupleRef = capsuleRef.parent.parent; if (!coupleRef) continue; const capsuleNotifications = await db.runTransaction(async (tx) => { var _a, _b, _c, _d; const freshCapsule = await tx.get(capsuleRef); const capsule = (_a = freshCapsule.data()) !== null && _a !== void 0 ? _a : {}; if (capsule.status !== 'sealed' || Number((_b = capsule.unlockAt) !== null && _b !== void 0 ? _b : 0) > now) { return []; } const coupleDoc = await tx.get(coupleRef); const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []); if (userIds.length === 0) return []; tx.update(capsuleRef, { status: 'unlocked', unlockedAt: now, unlockNotifiedAt: admin.firestore.FieldValue.serverTimestamp(), }); const capsuleId = capsuleRef.id; const coupleId = coupleRef.id; const title = typeof capsule.title === 'string' && capsule.title.trim().length > 0 ? capsule.title.trim() : 'A memory capsule'; return userIds.map((userId) => ({ userId, type: 'memory_capsule_unlocked', title: 'Your memory capsule opened', body: `${title} is ready to read together.`, data: { couple_id: coupleId, capsule_id: capsuleId }, })); }); notifications.push(...capsuleNotifications); } await Promise.all(notifications.map((notification) => sendNotification(db, messaging, notification))); console.log(`[unlockDueMemoryCapsules] unlocked ${snapshot.size}; notified ${notifications.length}`); }); exports.sendChallengeDayReminders = functions.pubsub .schedule('every 24 hours') .onRun(async () => { const db = admin.firestore(); const messaging = admin.messaging(); const now = Date.now(); const snapshot = await db .collectionGroup('challenges') .where('status', '==', 'active') .limit(100) .get(); const notifications = []; for (const challengeDoc of snapshot.docs) { const challengeRef = challengeDoc.ref; const coupleRef = challengeRef.parent.parent; if (!coupleRef) continue; const challengeNotifications = await db.runTransaction(async (tx) => { var _a, _b, _c, _d, _e, _f, _g; const freshChallenge = await tx.get(challengeRef); const challenge = (_a = freshChallenge.data()) !== null && _a !== void 0 ? _a : {}; if (challenge.status !== 'active') return []; const startedAt = Number((_b = challenge.startedAt) !== null && _b !== void 0 ? _b : 0); if (startedAt <= 0 || startedAt > now) return []; const challengeId = typeof challenge.challengeId === 'string' ? challenge.challengeId : challengeRef.id; const catalogEntry = (_c = CHALLENGE_TITLES[challengeId]) !== null && _c !== void 0 ? _c : { title: 'Connection Challenge', durationDays: 7, }; const day = Math.floor((now - startedAt) / DAY_MS) + 1; if (day < 1 || day > catalogEntry.durationDays) return []; const coupleDoc = await tx.get(coupleRef); const userIds = ((_e = (_d = coupleDoc.data()) === null || _d === void 0 ? void 0 : _d.userIds) !== null && _e !== void 0 ? _e : []); if (userIds.length === 0) return []; const completions = ((_f = challenge.completions) !== null && _f !== void 0 ? _f : {}); const reminderSent = ((_g = challenge.challengeReminderSent) !== null && _g !== void 0 ? _g : {}); const dueUserIds = userIds.filter((userId) => { var _a; const completedDays = (_a = completions[userId]) !== null && _a !== void 0 ? _a : []; const alreadyCompleted = completedDays.map(Number).includes(day); const alreadySent = reminderSent[reminderKey(userId, day)] === true; return !alreadyCompleted && !alreadySent; }); if (dueUserIds.length === 0) return []; const updates = { lastChallengeReminderAt: admin.firestore.FieldValue.serverTimestamp(), }; dueUserIds.forEach((userId) => { updates[`challengeReminderSent.${reminderKey(userId, day)}`] = true; }); tx.update(challengeRef, updates); const coupleId = coupleRef.id; return dueUserIds.map((userId) => ({ userId, type: 'challenge_day_ready', title: `Day ${day} is ready`, body: `${catalogEntry.title}: today's connection prompt is waiting.`, data: { couple_id: coupleId, challenge_id: challengeId, day: String(day) }, })); }); notifications.push(...challengeNotifications); } await Promise.all(notifications.map((notification) => sendNotification(db, messaging, notification))); console.log(`[sendChallengeDayReminders] scanned ${snapshot.size}; notified ${notifications.length}`); }); function reminderKey(userId, day) { return `${userId.replace(/[^\w-]/g, '_')}_${day}`; } async function sendNotification(db, messaging, notification) { await db .collection('users') .doc(notification.userId) .collection('notification_queue') .add({ type: notification.type, title: notification.title, body: notification.body, read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(), }); const tokens = await getUserTokens(db, notification.userId); if (tokens.length === 0) { console.log(`[sendNotification] no FCM tokens for ${notification.userId}`); return; } const message = { token: tokens[0], notification: { title: notification.title, body: notification.body, }, // E-OBS: challenge reminders → Reminders channel; capsule-unlocked → partner-activity channel. android: { notification: { channelId: notification.type === 'challenge_day_ready' ? 'reminders' : 'partner_activity', }, }, data: Object.assign({ type: notification.type }, notification.data), }; const sendResults = await Promise.allSettled(tokens.map((token) => messaging.send(Object.assign(Object.assign({}, message), { token })))); const failures = []; sendResults.forEach((result, index) => { if (result.status === 'rejected') { failures.push(`${tokens[index]}: ${String(result.reason)}`); } }); if (failures.length > 0) { console.error(`[sendNotification] some notifications failed:`, failures); } } async function getUserTokens(db, userId) { var _a; const tokens = []; const userDoc = await db.collection('users').doc(userId).get(); const legacyToken = (_a = userDoc.data()) === null || _a === void 0 ? void 0 : _a.fcmToken; if (typeof legacyToken === 'string' && legacyToken.length > 0) { tokens.push(legacyToken); } const tokenSnapshot = await db .collection('users') .doc(userId) .collection('fcmTokens') .get(); tokenSnapshot.docs.forEach((doc) => { var _a; const token = (_a = doc.data()) === null || _a === void 0 ? void 0 : _a.token; if (typeof token === 'string' && token.length > 0 && !tokens.includes(token)) { tokens.push(token); } }); return tokens; } //# sourceMappingURL=gameRetention.js.map