feat: code push -- notifications, cloud functions, iOS updates

This commit is contained in:
null 2026-06-22 08:53:23 -05:00
parent 7e3c61c6e4
commit 5e16177eb2
31 changed files with 796 additions and 74 deletions

View File

@ -1,5 +1,5 @@
{ {
"projects": { "projects": {
"default": "closer-real-app" "default": "closer-app-22014"
} }
} }

View File

@ -80,11 +80,15 @@ class PartnerNotificationManager @Inject constructor(
} }
private fun isEnabled(type: PartnerNotificationType, settings: AppSettings): Boolean { private fun isEnabled(type: PartnerNotificationType, settings: AppSettings): Boolean {
return when (type.rateType) { return when (type) {
PartnerNotificationType.CHAT_MESSAGE -> settings.chatMessageEnabled
PartnerNotificationType.OUTCOME_REMINDER -> settings.outcomeReminderEnabled
else -> when (type.rateType) {
NotificationRateLimiter.Type.PARTNER_TRIGGER -> settings.partnerAnsweredEnabled NotificationRateLimiter.Type.PARTNER_TRIGGER -> settings.partnerAnsweredEnabled
NotificationRateLimiter.Type.REMINDER -> settings.dailyReminderEnabled NotificationRateLimiter.Type.REMINDER -> settings.dailyReminderEnabled
} }
} }
}
private fun showNotification(id: Int, type: PartnerNotificationType, route: String) { private fun showNotification(id: Int, type: PartnerNotificationType, route: String) {
if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) return if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) return
@ -186,6 +190,36 @@ enum class PartnerNotificationType(
body = "Answer together before it expires.", body = "Answer together before it expires.",
channelId = NotificationChannelSetup.CHANNEL_REMINDERS, channelId = NotificationChannelSetup.CHANNEL_REMINDERS,
rateType = NotificationRateLimiter.Type.REMINDER rateType = NotificationRateLimiter.Type.REMINDER
),
CHAT_MESSAGE(
title = "Your partner sent a message.",
body = "Tap to read and reply.",
channelId = NotificationChannelSetup.CHANNEL_PARTNER_ACTIONS,
rateType = NotificationRateLimiter.Type.PARTNER_TRIGGER
),
OUTCOME_REMINDER(
title = "How did that go?",
body = "Take a moment to reflect on your last date.",
channelId = NotificationChannelSetup.CHANNEL_REMINDERS,
rateType = NotificationRateLimiter.Type.REMINDER
),
PARTNER_JOINED(
title = "Your partner joined!",
body = "You're connected. Time to answer tonight's question together.",
channelId = NotificationChannelSetup.CHANNEL_PARTNER_ACTIONS,
rateType = NotificationRateLimiter.Type.PARTNER_TRIGGER
),
DATE_MATCH(
title = "It's a match!",
body = "You both want to go on this date. Time to make it happen.",
channelId = NotificationChannelSetup.CHANNEL_PARTNER_ACTIONS,
rateType = NotificationRateLimiter.Type.PARTNER_TRIGGER
),
REENGAGEMENT(
title = "It's been a while.",
body = "Tonight's question is a good reason to reconnect.",
channelId = NotificationChannelSetup.CHANNEL_REMINDERS,
rateType = NotificationRateLimiter.Type.REMINDER
); );
/** /**
@ -200,6 +234,11 @@ enum class PartnerNotificationType(
CAPSULE_UNLOCKED -> AppRoute.MEMORY_LANE CAPSULE_UNLOCKED -> AppRoute.MEMORY_LANE
GENTLE_REMINDER -> AppRoute.DAILY_QUESTION GENTLE_REMINDER -> AppRoute.DAILY_QUESTION
DAILY_QUESTION_REMINDER -> AppRoute.DAILY_QUESTION DAILY_QUESTION_REMINDER -> AppRoute.DAILY_QUESTION
CHAT_MESSAGE -> AppRoute.ANSWER_HISTORY
OUTCOME_REMINDER -> AppRoute.SETTINGS
PARTNER_JOINED -> AppRoute.HOME
DATE_MATCH -> AppRoute.DATE_MATCHES
REENGAGEMENT -> AppRoute.DAILY_QUESTION
} }
companion object { companion object {
@ -212,6 +251,11 @@ enum class PartnerNotificationType(
"memory_capsule_unlocked" -> CAPSULE_UNLOCKED "memory_capsule_unlocked" -> CAPSULE_UNLOCKED
"gentle_reminder" -> GENTLE_REMINDER "gentle_reminder" -> GENTLE_REMINDER
"daily_question_reminder" -> DAILY_QUESTION_REMINDER "daily_question_reminder" -> DAILY_QUESTION_REMINDER
"chat_message" -> CHAT_MESSAGE
"outcome_reminder" -> OUTCOME_REMINDER
"partner_joined" -> PARTNER_JOINED
"date_match" -> DATE_MATCH
"reengagement" -> REENGAGEMENT
else -> null else -> null
} }
} }

View File

@ -164,6 +164,8 @@ exports.acceptInviteCallable = functions.https.onCall(async (data, context) => {
}); });
await batch.commit(); await batch.commit();
console.log(`[acceptInviteCallable] ${callerId} accepted invite ${code}; created couple ${coupleId}`); console.log(`[acceptInviteCallable] ${callerId} accepted invite ${code}; created couple ${coupleId}`);
// Notify the inviter that their partner joined — fire-and-forget.
notifyPartnerJoined(db, inviterUserId, coupleId).catch((e) => console.warn('[acceptInviteCallable] partner_joined FCM failed:', e));
return { return {
coupleId, coupleId,
inviterUserId, inviterUserId,
@ -173,4 +175,43 @@ exports.acceptInviteCallable = functions.https.onCall(async (data, context) => {
encryptedRecoveryPhrase: encryptedRecoveryPhrase !== null && encryptedRecoveryPhrase !== void 0 ? encryptedRecoveryPhrase : null, encryptedRecoveryPhrase: encryptedRecoveryPhrase !== null && encryptedRecoveryPhrase !== void 0 ? encryptedRecoveryPhrase : null,
}; };
}); });
async function notifyPartnerJoined(db, inviterUserId, coupleId) {
await db.collection('users').doc(inviterUserId).collection('notification_queue').add({
type: 'partner_joined',
title: 'Your partner joined!',
body: "You're connected. Time to answer tonight's question together.",
read: false,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
});
const tokens = await getUserTokens(db, inviterUserId);
if (tokens.length === 0)
return;
await Promise.allSettled(tokens.map((token) => admin.messaging().send({
token,
notification: {
title: 'Your partner joined!',
body: "You're connected. Time to answer tonight's question together.",
},
data: {
type: 'partner_joined',
couple_id: coupleId,
},
})));
}
async function getUserTokens(db, userId) {
var _a;
const tokens = [];
const userDoc = await db.collection('users').doc(userId).get();
const legacy = (_a = userDoc.data()) === null || _a === void 0 ? void 0 : _a.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) => {
var _a;
const t = (_a = doc.data()) === null || _a === void 0 ? void 0 : _a.token;
if (typeof t === 'string' && t.length > 0 && !tokens.includes(t))
tokens.push(t);
});
return tokens;
}
//# sourceMappingURL=acceptInviteCallable.js.map //# sourceMappingURL=acceptInviteCallable.js.map

File diff suppressed because one or more lines are too long

View File

@ -55,7 +55,7 @@ const LOVE = 'love';
exports.createDateMatchOnMutualLove = functions.firestore exports.createDateMatchOnMutualLove = functions.firestore
.document('couples/{coupleId}/date_swipes/{dateIdeaId}') .document('couples/{coupleId}/date_swipes/{dateIdeaId}')
.onWrite(async (change, context) => { .onWrite(async (change, context) => {
var _a; var _a, _b, _c;
const after = change.after.data(); const after = change.after.data();
if (!after) if (!after)
return; // swipe document was deleted return; // swipe document was deleted
@ -77,12 +77,67 @@ exports.createDateMatchOnMutualLove = functions.firestore
await db.runTransaction(async (tx) => { await db.runTransaction(async (tx) => {
const existing = await tx.get(matchRef); const existing = await tx.get(matchRef);
if (existing.exists) if (existing.exists)
return; // already matched — no-op return;
tx.set(matchRef, { tx.set(matchRef, {
dateIdeaId, dateIdeaId,
matchedBy: lovedBy, matchedBy: lovedBy,
revealedAt: admin.firestore.FieldValue.serverTimestamp(), revealedAt: admin.firestore.FieldValue.serverTimestamp(),
fcmNotified: false,
}); });
}); });
// Atomically claim FCM send so concurrent trigger invocations don't double-send.
const shouldSend = await db.runTransaction(async (tx) => {
var _a;
const doc = await tx.get(matchRef);
if (!doc.exists || ((_a = doc.data()) === null || _a === void 0 ? void 0 : _a.fcmNotified) === true)
return false;
tx.update(matchRef, { fcmNotified: true });
return true;
}); });
if (shouldSend) {
const coupleDoc = await db.collection('couples').doc(coupleId).get();
const userIds = ((_c = (_b = coupleDoc.data()) === null || _b === void 0 ? void 0 : _b.userIds) !== null && _c !== void 0 ? _c : []);
await Promise.all(userIds.map((uid) => notifyDateMatch(db, uid, coupleId, dateIdeaId)));
}
});
async function notifyDateMatch(db, userId, coupleId, dateIdeaId) {
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, userId) {
var _a;
const tokens = [];
const userDoc = await db.collection('users').doc(userId).get();
const legacy = (_a = userDoc.data()) === null || _a === void 0 ? void 0 : _a.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) => {
var _a;
const t = (_a = doc.data()) === null || _a === void 0 ? void 0 : _a.token;
if (typeof t === 'string' && t.length > 0 && !tokens.includes(t))
tokens.push(t);
});
return tokens;
}
//# sourceMappingURL=createDateMatch.js.map //# sourceMappingURL=createDateMatch.js.map

View File

@ -1 +1 @@
{"version":3,"file":"createDateMatch.js","sourceRoot":"","sources":["../../src/dates/createDateMatch.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC,MAAM,IAAI,GAAG,MAAM,CAAA;AAOnB;;;;;;;;;;;;;;GAcG;AACU,QAAA,2BAA2B,GAAG,SAAS,CAAC,SAAS;KAC3D,QAAQ,CAAC,6CAA6C,CAAC;KACvD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;;IACjC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;IACjC,IAAI,CAAC,KAAK;QAAE,OAAM,CAAC,6BAA6B;IAEhD,MAAM,OAAO,GAAG,CAAC,MAAA,KAAK,CAAC,OAAO,mCAAI,EAAE,CAA+B,CAAA;IACnE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;SACpC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,MAAM,MAAK,IAAI,CAAC;SAC7C,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC;SACnB,IAAI,EAAE,CAAA;IAET,2DAA2D;IAC3D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAM;IAE9B,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,MAGxC,CAAA;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAC5B,MAAM,QAAQ,GAAG,EAAE;SAChB,UAAU,CAAC,SAAS,CAAC;SACrB,GAAG,CAAC,QAAQ,CAAC;SACb,UAAU,CAAC,cAAc,CAAC;SAC1B,GAAG,CAAC,UAAU,CAAC,CAAA;IAElB,MAAM,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACnC,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACvC,IAAI,QAAQ,CAAC,MAAM;YAAE,OAAM,CAAC,0BAA0B;QACtD,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE;YACf,UAAU;YACV,SAAS,EAAE,OAAO;YAClB,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;SACzD,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"} {"version":3,"file":"createDateMatch.js","sourceRoot":"","sources":["../../src/dates/createDateMatch.ts"],"names":[],"mappings":"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"}

View File

@ -45,7 +45,7 @@ const admin = __importStar(require("firebase-admin"));
exports.onGameSessionUpdate = functions.firestore exports.onGameSessionUpdate = functions.firestore
.document('couples/{coupleId}/sessions/{sessionId}') .document('couples/{coupleId}/sessions/{sessionId}')
.onWrite(async (change, context) => { .onWrite(async (change, context) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
const { coupleId, sessionId } = context.params; const { coupleId, sessionId } = context.params;
const db = admin.firestore(); const db = admin.firestore();
const messaging = admin.messaging(); const messaging = admin.messaging();
@ -86,7 +86,7 @@ exports.onGameSessionUpdate = functions.firestore
const gameType = (_k = currentData.gameType) !== null && _k !== void 0 ? _k : 'wheel'; const gameType = (_k = currentData.gameType) !== null && _k !== void 0 ? _k : 'wheel';
const partnerId = startedBy === partnerA ? partnerB : partnerA; const partnerId = startedBy === partnerA ? partnerB : partnerA;
const partnerName = startedBy === partnerA ? partnerBName : partnerAName; const partnerName = startedBy === partnerA ? partnerBName : partnerAName;
await notifyPartner(db, messaging, partnerId, partnerName, gameType, 'partner_started_game', `${partnerName} has started a game. Tap to join!`); await notifyPartner(db, messaging, partnerId, partnerName, gameType, 'partner_started_game', `${partnerName} has started a game. Tap to join!`, coupleId);
return; return;
} }
// Check if session was completed // Check if session was completed
@ -96,16 +96,14 @@ exports.onGameSessionUpdate = functions.firestore
const completedBy = currentData.startedByUserId; const completedBy = currentData.startedByUserId;
const partnerId = completedBy === partnerA ? partnerB : partnerA; const partnerId = completedBy === partnerA ? partnerB : partnerA;
const completingPartnerName = completedBy === partnerA ? partnerAName : partnerBName; const completingPartnerName = completedBy === partnerA ? partnerAName : partnerBName;
// Check if partner has also completed const gt = (_m = currentData.gameType) !== null && _m !== void 0 ? _m : 'wheel';
const partnerCompletedAt = currentData.partnerCompletedAt; const partnerCompletedAt = currentData.partnerCompletedAt;
if (partnerCompletedAt) { if (partnerCompletedAt) {
// Both completed - notify both await notifyPartner(db, messaging, partnerA, partnerAName, gt, 'partner_finished_game', `${partnerBName} has finished the game. Tap to see the results!`, coupleId);
await notifyPartner(db, messaging, partnerA, partnerAName, (_m = currentData.gameType) !== null && _m !== void 0 ? _m : 'wheel', 'partner_finished_game', `${partnerBName} has finished the game. Tap to see the results!`); await notifyPartner(db, messaging, partnerB, partnerBName, gt, 'partner_finished_game', `${partnerAName} has finished the game. Tap to see the results!`, coupleId);
await notifyPartner(db, messaging, partnerB, partnerBName, (_o = currentData.gameType) !== null && _o !== void 0 ? _o : 'wheel', 'partner_finished_game', `${partnerAName} has finished the game. Tap to see the results!`);
} }
else { else {
// Only one completed - notify the other to continue await notifyPartner(db, messaging, partnerId, completingPartnerName, gt, 'partner_finished_game', `${completingPartnerName} has finished. Tap to continue playing!`, coupleId);
await notifyPartner(db, messaging, partnerId, completingPartnerName, (_p = currentData.gameType) !== null && _p !== void 0 ? _p : 'wheel', 'partner_finished_game', `${completingPartnerName} has finished. Tap to continue playing!`);
} }
return; return;
} }
@ -113,7 +111,7 @@ exports.onGameSessionUpdate = functions.firestore
/** /**
* Send notification to partner via FCM and write to notification_queue. * Send notification to partner via FCM and write to notification_queue.
*/ */
async function notifyPartner(db, messaging, partnerId, partnerName, gameType, notificationType, body) { async function notifyPartner(db, messaging, partnerId, partnerName, gameType, notificationType, body, coupleId) {
var _a; var _a;
const notificationPayload = { const notificationPayload = {
type: notificationType, type: notificationType,
@ -159,8 +157,8 @@ async function notifyPartner(db, messaging, partnerId, partnerName, gameType, no
}, },
data: { data: {
type: notificationPayload.type, type: notificationPayload.type,
gameType: gameType, couple_id: coupleId,
partnerId: partnerId, game_type: gameType,
}, },
}; };
const sendResults = await Promise.allSettled(tokens.map((token) => messaging.send(Object.assign(Object.assign({}, fcmMessage), { token })))); const sendResults = await Promise.allSettled(tokens.map((token) => messaging.send(Object.assign(Object.assign({}, fcmMessage), { token }))));

File diff suppressed because one or more lines are too long

View File

@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
}; };
})(); })();
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.health = exports.onGameSessionUpdate = exports.onUserDelete = exports.scheduledOutcomesReminder = exports.submitOutcomeCallable = exports.createInviteCallable = exports.acceptInviteCallable = exports.leaveCoupleCallable = exports.onCoupleLeave = exports.onMessageWritten = exports.onAnswerWritten = exports.assignDailyQuestionCallable = exports.assignDailyQuestion = exports.createDateMatchOnMutualLove = exports.checkDeviceIntegrity = exports.unlockDueMemoryCapsules = exports.sendChallengeDayReminders = exports.sendGentleReminderCallable = exports.sendPartnerAnsweredNotification = exports.sendDailyQuestionReminder = exports.syncEntitlement = exports.revenueCatWebhook = void 0; exports.health = exports.onGameSessionUpdate = exports.onUserDelete = exports.scheduledOutcomesReminder = exports.submitOutcomeCallable = exports.createInviteCallable = exports.acceptInviteCallable = exports.leaveCoupleCallable = exports.onCoupleLeave = exports.onMessageWritten = exports.onAnswerWritten = exports.assignDailyQuestionCallable = exports.assignDailyQuestion = exports.createDateMatchOnMutualLove = exports.checkDeviceIntegrity = exports.sendReengagementReminder = exports.sendDailyQuestionProactiveReminder = exports.unlockDueMemoryCapsules = exports.sendChallengeDayReminders = exports.sendGentleReminderCallable = exports.sendPartnerAnsweredNotification = exports.sendDailyQuestionReminder = exports.syncEntitlement = exports.revenueCatWebhook = void 0;
const functions = __importStar(require("firebase-functions")); const functions = __importStar(require("firebase-functions"));
const admin = __importStar(require("firebase-admin")); const admin = __importStar(require("firebase-admin"));
// Initialize the Admin SDK once for every function in this codebase. // Initialize the Admin SDK once for every function in this codebase.
@ -54,6 +54,10 @@ Object.defineProperty(exports, "sendGentleReminderCallable", { enumerable: true,
var gameRetention_1 = require("./notifications/gameRetention"); var gameRetention_1 = require("./notifications/gameRetention");
Object.defineProperty(exports, "sendChallengeDayReminders", { enumerable: true, get: function () { return gameRetention_1.sendChallengeDayReminders; } }); Object.defineProperty(exports, "sendChallengeDayReminders", { enumerable: true, get: function () { return gameRetention_1.sendChallengeDayReminders; } });
Object.defineProperty(exports, "unlockDueMemoryCapsules", { enumerable: true, get: function () { return gameRetention_1.unlockDueMemoryCapsules; } }); Object.defineProperty(exports, "unlockDueMemoryCapsules", { enumerable: true, get: function () { return gameRetention_1.unlockDueMemoryCapsules; } });
var dailyQuestionReminder_1 = require("./notifications/dailyQuestionReminder");
Object.defineProperty(exports, "sendDailyQuestionProactiveReminder", { enumerable: true, get: function () { return dailyQuestionReminder_1.sendDailyQuestionProactiveReminder; } });
var reengagement_1 = require("./notifications/reengagement");
Object.defineProperty(exports, "sendReengagementReminder", { enumerable: true, get: function () { return reengagement_1.sendReengagementReminder; } });
var checkDeviceIntegrity_1 = require("./security/checkDeviceIntegrity"); var checkDeviceIntegrity_1 = require("./security/checkDeviceIntegrity");
Object.defineProperty(exports, "checkDeviceIntegrity", { enumerable: true, get: function () { return checkDeviceIntegrity_1.checkDeviceIntegrity; } }); Object.defineProperty(exports, "checkDeviceIntegrity", { enumerable: true, get: function () { return checkDeviceIntegrity_1.checkDeviceIntegrity; } });
var createDateMatch_1 = require("./dates/createDateMatch"); var createDateMatch_1 = require("./dates/createDateMatch");

View File

@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC,qEAAqE;AACrE,8EAA8E;AAC9E,gFAAgF;AAChF,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC5B,KAAK,CAAC,aAAa,EAAE,CAAA;AACvB,CAAC;AAED,iEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAC1B,6DAA2D;AAAlD,kHAAA,eAAe,OAAA;AACxB,uDAGkC;AAFhC,sHAAA,yBAAyB,OAAA;AACzB,4HAAA,+BAA+B,OAAA;AAEjC,yFAAuF;AAA9E,wIAAA,0BAA0B,OAAA;AACnC,+DAGsC;AAFpC,0HAAA,yBAAyB,OAAA;AACzB,wHAAA,uBAAuB,OAAA;AAEzB,wEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAC7B,2DAAqE;AAA5D,8HAAA,2BAA2B,OAAA;AACpC,uEAGwC;AAFtC,0HAAA,mBAAmB,OAAA;AACnB,kIAAA,2BAA2B,OAAA;AAE7B,+DAA6D;AAApD,kHAAA,eAAe,OAAA;AACxB,iEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AACzB,yDAAuD;AAA9C,8GAAA,aAAa,OAAA;AACtB,qEAAmE;AAA1D,0HAAA,mBAAmB,OAAA;AAC5B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,yEAAuE;AAA9D,8HAAA,qBAAqB,OAAA;AAC9B,iFAA+E;AAAtE,sIAAA,yBAAyB,OAAA;AAClC,qDAAmD;AAA1C,4GAAA,YAAY,OAAA;AACrB,mEAAiE;AAAxD,0HAAA,mBAAmB,OAAA;AAE5B;;;GAGG;AACU,QAAA,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC3D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;AACxC,CAAC,CAAC,CAAA"} {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC,qEAAqE;AACrE,8EAA8E;AAC9E,gFAAgF;AAChF,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC5B,KAAK,CAAC,aAAa,EAAE,CAAA;AACvB,CAAC;AAED,iEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAC1B,6DAA2D;AAAlD,kHAAA,eAAe,OAAA;AACxB,uDAGkC;AAFhC,sHAAA,yBAAyB,OAAA;AACzB,4HAAA,+BAA+B,OAAA;AAEjC,yFAAuF;AAA9E,wIAAA,0BAA0B,OAAA;AACnC,+DAGsC;AAFpC,0HAAA,yBAAyB,OAAA;AACzB,wHAAA,uBAAuB,OAAA;AAEzB,+EAA0F;AAAjF,2IAAA,kCAAkC,OAAA;AAC3C,6DAAuE;AAA9D,wHAAA,wBAAwB,OAAA;AACjC,wEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAC7B,2DAAqE;AAA5D,8HAAA,2BAA2B,OAAA;AACpC,uEAGwC;AAFtC,0HAAA,mBAAmB,OAAA;AACnB,kIAAA,2BAA2B,OAAA;AAE7B,+DAA6D;AAApD,kHAAA,eAAe,OAAA;AACxB,iEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AACzB,yDAAuD;AAA9C,8GAAA,aAAa,OAAA;AACtB,qEAAmE;AAA1D,0HAAA,mBAAmB,OAAA;AAC5B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,yEAAuE;AAA9D,8HAAA,qBAAqB,OAAA;AAC9B,iFAA+E;AAAtE,sIAAA,yBAAyB,OAAA;AAClC,qDAAmD;AAA1C,4GAAA,YAAY,OAAA;AACrB,mEAAiE;AAAxD,0HAAA,mBAAmB,OAAA;AAE5B;;;GAGG;AACU,QAAA,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC3D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;AACxC,CAAC,CAAC,CAAA"}

View File

@ -0,0 +1,185 @@
"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.sendDailyQuestionProactiveReminder = void 0;
const functions = __importStar(require("firebase-functions"));
const admin = __importStar(require("firebase-admin"));
/**
* Proactive daily question reminder.
*
* Schedule: 4:00 PM America/Chicago (2 hours before the question expires at 6 PM).
*
* Logic:
* 1. Query all `daily_question` docs (collection group) with expiresAt in the
* next 3 hours these are the ones expiring today.
* 2. For each, count the answers subcollection. If 0 answers (neither partner
* has responded), the couple needs a nudge.
* 3. Skip couples where a gentle_reminder or automated_daily_reminder was
* already sent today (avoid over-notifying couples where a partner already
* manually nudged the other).
* 4. Send FCM to all users in the couple + write to notification_queue.
* 5. Record automated_daily_reminder/{docId} so this function is idempotent
* on re-runs.
*/
exports.sendDailyQuestionProactiveReminder = functions.pubsub
.schedule('0 16 * * *')
.timeZone('America/Chicago')
.onRun(async () => {
const db = admin.firestore();
const messaging = admin.messaging();
const now = Date.now();
const threeHoursMs = 3 * 60 * 60 * 1000;
// Docs expiring within the next 3 hours — the active window for today's question.
const expiringSnap = await db
.collectionGroup('daily_question')
.where('expiresAt', '>', admin.firestore.Timestamp.fromMillis(now))
.where('expiresAt', '<', admin.firestore.Timestamp.fromMillis(now + threeHoursMs))
.get();
let notified = 0;
let skipped = 0;
await Promise.all(expiringSnap.docs.map(async (questionDoc) => {
var _a, _b;
const coupleRef = questionDoc.ref.parent.parent;
if (!coupleRef)
return;
const reminderId = `auto_${questionDoc.id}`;
const reminderRef = coupleRef.collection('daily_reminders').doc(reminderId);
// Idempotency: skip if we already sent this reminder.
const alreadySent = await reminderRef.get();
if (alreadySent.exists) {
skipped++;
return;
}
// Skip if a manual gentle_reminder was sent today (same date key).
const dateKey = questionDoc.id; // daily_question docs are keyed by date (YYYY-MM-DD)
const gentleRef = coupleRef.collection('gentle_reminders').doc(dateKey);
const gentleSent = await gentleRef.get();
if (gentleSent.exists) {
skipped++;
return;
}
// Check answer count — only remind if nobody has answered yet.
const answersSnap = await questionDoc.ref.collection('answers').limit(1).get();
if (!answersSnap.empty) {
skipped++;
return;
}
// Fetch couple members.
const coupleDoc = await coupleRef.get();
const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []);
if (userIds.length === 0) {
skipped++;
return;
}
// Claim the reminder slot atomically.
try {
await db.runTransaction(async (tx) => {
const fresh = await tx.get(reminderRef);
if (fresh.exists)
throw new Error('already_sent');
tx.set(reminderRef, {
sentAt: admin.firestore.FieldValue.serverTimestamp(),
questionDate: dateKey,
});
});
}
catch (_c) {
skipped++;
return;
}
// Send FCM + notification_queue entry to every user in the couple.
await Promise.all(userIds.map((userId) => sendReminder(db, messaging, userId, coupleRef.id, dateKey)));
notified += userIds.length;
}));
console.log(`[sendDailyQuestionProactiveReminder] scanned ${expiringSnap.size} docs; notified ${notified} users; skipped ${skipped}`);
});
async function sendReminder(db, messaging, userId, coupleId, questionDate) {
// In-app notification record.
await db
.collection('users')
.doc(userId)
.collection('notification_queue')
.add({
type: 'daily_question_reminder',
title: "Tonight's question is waiting.",
body: 'Answer together before it expires.',
read: false,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
});
// FCM push.
const tokens = await getUserTokens(db, userId);
if (tokens.length === 0)
return;
const sendResults = await Promise.allSettled(tokens.map((token) => messaging.send({
token,
notification: {
title: "Tonight's question is waiting.",
body: 'Answer together before it expires.',
},
data: {
type: 'daily_question_reminder',
couple_id: coupleId,
question_date: questionDate,
},
})));
sendResults.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`[sendDailyQuestionProactiveReminder] FCM failed for token ${tokens[i]}:`, result.reason);
}
});
}
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 tokenSnap = await db
.collection('users')
.doc(userId)
.collection('fcmTokens')
.get();
tokenSnap.docs.forEach((doc) => {
var _a;
const t = (_a = doc.data()) === null || _a === void 0 ? void 0 : _a.token;
if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) {
tokens.push(t);
}
});
return tokens;
}
//# sourceMappingURL=dailyQuestionReminder.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"dailyQuestionReminder.js","sourceRoot":"","sources":["../../src/notifications/dailyQuestionReminder.ts"],"names":[],"mappings":"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"}

View File

@ -87,7 +87,7 @@ exports.unlockDueMemoryCapsules = functions.pubsub
type: 'memory_capsule_unlocked', type: 'memory_capsule_unlocked',
title: 'Your memory capsule opened', title: 'Your memory capsule opened',
body: `${title} is ready to read together.`, body: `${title} is ready to read together.`,
data: { coupleId, capsuleId }, data: { couple_id: coupleId, capsule_id: capsuleId },
})); }));
}); });
notifications.push(...capsuleNotifications); notifications.push(...capsuleNotifications);
@ -159,7 +159,7 @@ exports.sendChallengeDayReminders = functions.pubsub
type: 'challenge_day_ready', type: 'challenge_day_ready',
title: `Day ${day} is ready`, title: `Day ${day} is ready`,
body: `${catalogEntry.title}: today's connection prompt is waiting.`, body: `${catalogEntry.title}: today's connection prompt is waiting.`,
data: { coupleId, challengeId, day: String(day) }, data: { couple_id: coupleId, challenge_id: challengeId, day: String(day) },
})); }));
}); });
notifications.push(...challengeNotifications); notifications.push(...challengeNotifications);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,145 @@
"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.sendReengagementReminder = void 0;
const functions = __importStar(require("firebase-functions"));
const admin = __importStar(require("firebase-admin"));
const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;
const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000;
const REENGAGEMENT_COOLDOWN_MS = 3 * 24 * 60 * 60 * 1000;
/**
* Re-engagement nudge for couples who went quiet.
*
* Schedule: 12:00 PM America/Chicago daily.
*
* Targets couples whose lastAnsweredAt is between 3 and 10 days ago
* recently lapsed but not completely inactive. Respects a 3-day cooldown
* via reengagementSentAt to avoid spamming.
*
* Requires a Firestore composite index on couples: lastAnsweredAt ASC.
*/
exports.sendReengagementReminder = functions.pubsub
.schedule('0 12 * * *')
.timeZone('America/Chicago')
.onRun(async () => {
const db = admin.firestore();
const messaging = admin.messaging();
const now = Date.now();
const threeDaysAgo = admin.firestore.Timestamp.fromMillis(now - THREE_DAYS_MS);
const tenDaysAgo = admin.firestore.Timestamp.fromMillis(now - TEN_DAYS_MS);
const snap = await db
.collection('couples')
.where('lastAnsweredAt', '>', tenDaysAgo)
.where('lastAnsweredAt', '<', threeDaysAgo)
.limit(200)
.get();
let notified = 0;
let skipped = 0;
await Promise.all(snap.docs.map(async (coupleDoc) => {
var _a;
const data = coupleDoc.data();
const coupleId = coupleDoc.id;
const userIds = ((_a = data.userIds) !== null && _a !== void 0 ? _a : []);
if (userIds.length === 0) {
skipped++;
return;
}
// Respect cooldown — don't re-send within 3 days.
const sentAt = data.reengagementSentAt;
if (sentAt && now - sentAt.toMillis() < REENGAGEMENT_COOLDOWN_MS) {
skipped++;
return;
}
// Claim atomically so parallel runs don't double-send.
const claimed = await db.runTransaction(async (tx) => {
var _a;
const fresh = await tx.get(coupleDoc.ref);
const freshSentAt = (_a = fresh.data()) === null || _a === void 0 ? void 0 : _a.reengagementSentAt;
if (freshSentAt && now - freshSentAt.toMillis() < REENGAGEMENT_COOLDOWN_MS)
return false;
tx.update(coupleDoc.ref, {
reengagementSentAt: admin.firestore.FieldValue.serverTimestamp(),
});
return true;
});
if (!claimed) {
skipped++;
return;
}
await Promise.all(userIds.map((uid) => sendNudge(db, messaging, uid, coupleId)));
notified += userIds.length;
}));
console.log(`[sendReengagementReminder] scanned ${snap.size}; notified ${notified}; skipped ${skipped}`);
});
async function sendNudge(db, messaging, userId, coupleId) {
await db.collection('users').doc(userId).collection('notification_queue').add({
type: 'reengagement',
title: "It's been a while.",
body: "Tonight's question is a good reason to reconnect.",
read: false,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
});
const tokens = await getUserTokens(db, userId);
if (tokens.length === 0)
return;
await Promise.allSettled(tokens.map((token) => messaging.send({
token,
notification: {
title: "It's been a while.",
body: "Tonight's question is a good reason to reconnect.",
},
data: {
type: 'reengagement',
couple_id: coupleId,
},
})));
}
async function getUserTokens(db, userId) {
var _a;
const tokens = [];
const userDoc = await db.collection('users').doc(userId).get();
const legacy = (_a = userDoc.data()) === null || _a === void 0 ? void 0 : _a.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) => {
var _a;
const t = (_a = doc.data()) === null || _a === void 0 ? void 0 : _a.token;
if (typeof t === 'string' && t.length > 0 && !tokens.includes(t))
tokens.push(t);
});
return tokens;
}
//# sourceMappingURL=reengagement.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"reengagement.js","sourceRoot":"","sources":["../../src/notifications/reengagement.ts"],"names":[],"mappings":"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"}

View File

@ -103,8 +103,8 @@ exports.onAnswerWritten = functions.firestore
}, },
data: { data: {
type: 'partner_answered', type: 'partner_answered',
coupleId, couple_id: coupleId,
questionId, question_id: questionId,
date, date,
}, },
}; };
@ -118,6 +118,10 @@ exports.onAnswerWritten = functions.firestore
if (failures.length > 0) { if (failures.length > 0) {
console.error(`[onAnswerWritten] some notifications failed:`, failures); console.error(`[onAnswerWritten] some notifications failed:`, failures);
} }
// Track last activity time on the couple doc for re-engagement targeting.
await db.collection('couples').doc(coupleId).update({
lastAnsweredAt: admin.firestore.FieldValue.serverTimestamp(),
}).catch((e) => console.warn('[onAnswerWritten] lastAnsweredAt update failed:', e));
console.log(`[onAnswerWritten] notified partner ${partnerId} for couple ${coupleId} question ${questionId}`); console.log(`[onAnswerWritten] notified partner ${partnerId} for couple ${coupleId} question ${questionId}`);
}); });
//# sourceMappingURL=onAnswerWritten.js.map //# sourceMappingURL=onAnswerWritten.js.map

View File

@ -1 +1 @@
{"version":3,"file":"onAnswerWritten.js","sourceRoot":"","sources":["../../src/questions/onAnswerWritten.ts"],"names":[],"mappings":"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"} {"version":3,"file":"onAnswerWritten.js","sourceRoot":"","sources":["../../src/questions/onAnswerWritten.ts"],"names":[],"mappings":"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"}

View File

@ -104,8 +104,8 @@ exports.onMessageWritten = functions.firestore
}, },
data: { data: {
type: 'chat_message', type: 'chat_message',
coupleId, couple_id: coupleId,
threadId, thread_id: threadId,
}, },
}; };
const sendResults = await Promise.allSettled(tokens.map((token) => admin.messaging().send(Object.assign(Object.assign({}, payload), { token })))); const sendResults = await Promise.allSettled(tokens.map((token) => admin.messaging().send(Object.assign(Object.assign({}, payload), { token }))));

View File

@ -1 +1 @@
{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":"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"} {"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":"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"}

View File

@ -153,6 +153,11 @@ export const acceptInviteCallable = functions.https.onCall(async (data: any, con
console.log(`[acceptInviteCallable] ${callerId} accepted invite ${code}; created couple ${coupleId}`) console.log(`[acceptInviteCallable] ${callerId} accepted invite ${code}; created couple ${coupleId}`)
// Notify the inviter that their partner joined — fire-and-forget.
notifyPartnerJoined(db, inviterUserId, coupleId).catch((e) =>
console.warn('[acceptInviteCallable] partner_joined FCM failed:', e)
)
return { return {
coupleId, coupleId,
inviterUserId, inviterUserId,
@ -162,3 +167,53 @@ export const acceptInviteCallable = functions.https.onCall(async (data: any, con
encryptedRecoveryPhrase: encryptedRecoveryPhrase ?? null, encryptedRecoveryPhrase: encryptedRecoveryPhrase ?? null,
} }
}) })
async function notifyPartnerJoined(
db: admin.firestore.Firestore,
inviterUserId: string,
coupleId: string
): Promise<void> {
await db.collection('users').doc(inviterUserId).collection('notification_queue').add({
type: 'partner_joined',
title: 'Your partner joined!',
body: "You're connected. Time to answer tonight's question together.",
read: false,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
})
const tokens = await getUserTokens(db, inviterUserId)
if (tokens.length === 0) return
await Promise.allSettled(
tokens.map((token) =>
admin.messaging().send({
token,
notification: {
title: 'Your partner joined!',
body: "You're connected. Time to answer tonight's question together.",
},
data: {
type: 'partner_joined',
couple_id: coupleId,
},
})
)
)
}
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
}

View File

@ -52,11 +52,78 @@ export const createDateMatchOnMutualLove = functions.firestore
await db.runTransaction(async (tx) => { await db.runTransaction(async (tx) => {
const existing = await tx.get(matchRef) const existing = await tx.get(matchRef)
if (existing.exists) return // already matched — no-op if (existing.exists) return
tx.set(matchRef, { tx.set(matchRef, {
dateIdeaId, dateIdeaId,
matchedBy: lovedBy, matchedBy: lovedBy,
revealedAt: admin.firestore.FieldValue.serverTimestamp(), revealedAt: admin.firestore.FieldValue.serverTimestamp(),
fcmNotified: false,
}) })
}) })
// Atomically claim FCM send so concurrent trigger 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) {
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.",
},
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
}

View File

@ -61,13 +61,8 @@ export const onGameSessionUpdate = functions.firestore
const partnerName = startedBy === partnerA ? partnerBName : partnerAName const partnerName = startedBy === partnerA ? partnerBName : partnerAName
await notifyPartner( await notifyPartner(
db, db, messaging, partnerId, partnerName, gameType,
messaging, 'partner_started_game', `${partnerName} has started a game. Tap to join!`, coupleId
partnerId,
partnerName,
gameType,
'partner_started_game',
`${partnerName} has started a game. Tap to join!`
) )
return return
} }
@ -80,39 +75,22 @@ export const onGameSessionUpdate = functions.firestore
const completedBy = currentData.startedByUserId const completedBy = currentData.startedByUserId
const partnerId = completedBy === partnerA ? partnerB : partnerA const partnerId = completedBy === partnerA ? partnerB : partnerA
const completingPartnerName = completedBy === partnerA ? partnerAName : partnerBName const completingPartnerName = completedBy === partnerA ? partnerAName : partnerBName
const gt = currentData.gameType ?? 'wheel'
// Check if partner has also completed
const partnerCompletedAt = currentData.partnerCompletedAt const partnerCompletedAt = currentData.partnerCompletedAt
if (partnerCompletedAt) { if (partnerCompletedAt) {
// Both completed - notify both
await notifyPartner( await notifyPartner(
db, db, messaging, partnerA, partnerAName, gt,
messaging, 'partner_finished_game', `${partnerBName} has finished the game. Tap to see the results!`, coupleId
partnerA,
partnerAName,
currentData.gameType ?? 'wheel',
'partner_finished_game',
`${partnerBName} has finished the game. Tap to see the results!`
) )
await notifyPartner( await notifyPartner(
db, db, messaging, partnerB, partnerBName, gt,
messaging, 'partner_finished_game', `${partnerAName} has finished the game. Tap to see the results!`, coupleId
partnerB,
partnerBName,
currentData.gameType ?? 'wheel',
'partner_finished_game',
`${partnerAName} has finished the game. Tap to see the results!`
) )
} else { } else {
// Only one completed - notify the other to continue
await notifyPartner( await notifyPartner(
db, db, messaging, partnerId, completingPartnerName, gt,
messaging, 'partner_finished_game', `${completingPartnerName} has finished. Tap to continue playing!`, coupleId
partnerId,
completingPartnerName,
currentData.gameType ?? 'wheel',
'partner_finished_game',
`${completingPartnerName} has finished. Tap to continue playing!`
) )
} }
return return
@ -129,7 +107,8 @@ async function notifyPartner(
partnerName: string, partnerName: string,
gameType: string, gameType: string,
notificationType: string, notificationType: string,
body: string body: string,
coupleId: string
): Promise<void> { ): Promise<void> {
const notificationPayload = { const notificationPayload = {
type: notificationType, type: notificationType,
@ -184,8 +163,8 @@ async function notifyPartner(
}, },
data: { data: {
type: notificationPayload.type, type: notificationPayload.type,
gameType: gameType, couple_id: coupleId,
partnerId: partnerId, game_type: gameType,
}, },
} }

View File

@ -20,6 +20,7 @@ export {
unlockDueMemoryCapsules, unlockDueMemoryCapsules,
} from './notifications/gameRetention' } from './notifications/gameRetention'
export { sendDailyQuestionProactiveReminder } from './notifications/dailyQuestionReminder' export { sendDailyQuestionProactiveReminder } from './notifications/dailyQuestionReminder'
export { sendReengagementReminder } from './notifications/reengagement'
export { checkDeviceIntegrity } from './security/checkDeviceIntegrity' export { checkDeviceIntegrity } from './security/checkDeviceIntegrity'
export { createDateMatchOnMutualLove } from './dates/createDateMatch' export { createDateMatchOnMutualLove } from './dates/createDateMatch'
export { export {

View File

@ -128,8 +128,8 @@ async function sendReminder(
}, },
data: { data: {
type: 'daily_question_reminder', type: 'daily_question_reminder',
coupleId, couple_id: coupleId,
questionDate, question_date: questionDate,
}, },
}) })
) )

View File

@ -69,7 +69,7 @@ export const unlockDueMemoryCapsules = functions.pubsub
type: 'memory_capsule_unlocked' as const, type: 'memory_capsule_unlocked' as const,
title: 'Your memory capsule opened', title: 'Your memory capsule opened',
body: `${title} is ready to read together.`, body: `${title} is ready to read together.`,
data: { coupleId, capsuleId }, data: { couple_id: coupleId, capsule_id: capsuleId },
})) }))
}) })
@ -147,7 +147,7 @@ export const sendChallengeDayReminders = functions.pubsub
type: 'challenge_day_ready' as const, type: 'challenge_day_ready' as const,
title: `Day ${day} is ready`, title: `Day ${day} is ready`,
body: `${catalogEntry.title}: today's connection prompt is waiting.`, body: `${catalogEntry.title}: today's connection prompt is waiting.`,
data: { coupleId, challengeId, day: String(day) }, data: { couple_id: coupleId, challenge_id: challengeId, day: String(day) },
})) }))
}) })

View File

@ -0,0 +1,123 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000
const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000
const REENGAGEMENT_COOLDOWN_MS = 3 * 24 * 60 * 60 * 1000
/**
* Re-engagement nudge for couples who went quiet.
*
* Schedule: 12:00 PM America/Chicago daily.
*
* Targets couples whose lastAnsweredAt is between 3 and 10 days ago
* recently lapsed but not completely inactive. Respects a 3-day cooldown
* via reengagementSentAt to avoid spamming.
*
* Requires a Firestore composite index on couples: lastAnsweredAt ASC.
*/
export const sendReengagementReminder = functions.pubsub
.schedule('0 12 * * *')
.timeZone('America/Chicago')
.onRun(async () => {
const db = admin.firestore()
const messaging = admin.messaging()
const now = Date.now()
const threeDaysAgo = admin.firestore.Timestamp.fromMillis(now - THREE_DAYS_MS)
const tenDaysAgo = admin.firestore.Timestamp.fromMillis(now - TEN_DAYS_MS)
const snap = await db
.collection('couples')
.where('lastAnsweredAt', '>', tenDaysAgo)
.where('lastAnsweredAt', '<', threeDaysAgo)
.limit(200)
.get()
let notified = 0
let skipped = 0
await Promise.all(
snap.docs.map(async (coupleDoc) => {
const data = coupleDoc.data()
const coupleId = coupleDoc.id
const userIds = (data.userIds ?? []) as string[]
if (userIds.length === 0) { skipped++; return }
// Respect cooldown — don't re-send within 3 days.
const sentAt = data.reengagementSentAt as admin.firestore.Timestamp | undefined
if (sentAt && now - sentAt.toMillis() < REENGAGEMENT_COOLDOWN_MS) {
skipped++
return
}
// Claim atomically so parallel runs don't double-send.
const claimed = await db.runTransaction(async (tx) => {
const fresh = await tx.get(coupleDoc.ref)
const freshSentAt = fresh.data()?.reengagementSentAt as admin.firestore.Timestamp | undefined
if (freshSentAt && now - freshSentAt.toMillis() < REENGAGEMENT_COOLDOWN_MS) return false
tx.update(coupleDoc.ref, {
reengagementSentAt: admin.firestore.FieldValue.serverTimestamp(),
})
return true
})
if (!claimed) { skipped++; return }
await Promise.all(userIds.map((uid) => sendNudge(db, messaging, uid, coupleId)))
notified += userIds.length
})
)
console.log(`[sendReengagementReminder] scanned ${snap.size}; notified ${notified}; skipped ${skipped}`)
})
async function sendNudge(
db: admin.firestore.Firestore,
messaging: admin.messaging.Messaging,
userId: string,
coupleId: string
): Promise<void> {
await db.collection('users').doc(userId).collection('notification_queue').add({
type: 'reengagement',
title: "It's been a while.",
body: "Tonight's question is a good reason to reconnect.",
read: false,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
})
const tokens = await getUserTokens(db, userId)
if (tokens.length === 0) return
await Promise.allSettled(
tokens.map((token) =>
messaging.send({
token,
notification: {
title: "It's been a while.",
body: "Tonight's question is a good reason to reconnect.",
},
data: {
type: 'reengagement',
couple_id: coupleId,
},
})
)
)
}
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
}

View File

@ -79,8 +79,8 @@ export const onAnswerWritten = functions.firestore
}, },
data: { data: {
type: 'partner_answered', type: 'partner_answered',
coupleId, couple_id: coupleId,
questionId, question_id: questionId,
date, date,
}, },
} }
@ -102,6 +102,11 @@ export const onAnswerWritten = functions.firestore
console.error(`[onAnswerWritten] some notifications failed:`, failures) console.error(`[onAnswerWritten] some notifications failed:`, failures)
} }
// Track last activity time on the couple doc for re-engagement targeting.
await db.collection('couples').doc(coupleId).update({
lastAnsweredAt: admin.firestore.FieldValue.serverTimestamp(),
}).catch((e) => console.warn('[onAnswerWritten] lastAnsweredAt update failed:', e))
console.log( console.log(
`[onAnswerWritten] notified partner ${partnerId} for couple ${coupleId} question ${questionId}` `[onAnswerWritten] notified partner ${partnerId} for couple ${coupleId} question ${questionId}`
) )

View File

@ -80,8 +80,8 @@ export const onMessageWritten = functions.firestore
}, },
data: { data: {
type: 'chat_message', type: 'chat_message',
coupleId, couple_id: coupleId,
threadId, thread_id: threadId,
}, },
} }

View File

@ -83,6 +83,12 @@ extension NotificationService: UNUserNotificationCenterDelegate {
NotificationCenter.default.post(name: .navigateToMemoryLane, object: nil) NotificationCenter.default.post(name: .navigateToMemoryLane, object: nil)
case "challenge_day_ready", "challenge_waiting": case "challenge_day_ready", "challenge_waiting":
NotificationCenter.default.post(name: .navigateToConnectionChallenges, object: nil) NotificationCenter.default.post(name: .navigateToConnectionChallenges, object: nil)
case "chat_message":
NotificationCenter.default.post(name: .navigateToAnswerHistory, object: nil)
case "outcome_reminder":
NotificationCenter.default.post(name: .navigateToSettings, object: nil)
case "partner_joined", "date_match", "reengagement":
NotificationCenter.default.post(name: .navigateToHome, object: nil)
default: default:
break break
} }
@ -98,6 +104,8 @@ extension Notification.Name {
static let navigateToPlay = Notification.Name("navigateToPlay") static let navigateToPlay = Notification.Name("navigateToPlay")
static let navigateToMemoryLane = Notification.Name("navigateToMemoryLane") static let navigateToMemoryLane = Notification.Name("navigateToMemoryLane")
static let navigateToConnectionChallenges = Notification.Name("navigateToConnectionChallenges") static let navigateToConnectionChallenges = Notification.Name("navigateToConnectionChallenges")
static let navigateToAnswerHistory = Notification.Name("navigateToAnswerHistory")
static let navigateToSettings = Notification.Name("navigateToSettings")
} }
typealias FieldValue = FirebaseFirestore.FieldValue typealias FieldValue = FirebaseFirestore.FieldValue

View File

@ -90,6 +90,12 @@ struct MainTabView: View {
.onReceive(NotificationCenter.default.publisher(for: .navigateToConnectionChallenges)) { _ in .onReceive(NotificationCenter.default.publisher(for: .navigateToConnectionChallenges)) { _ in
selectedTab = .play selectedTab = .play
} }
.onReceive(NotificationCenter.default.publisher(for: .navigateToAnswerHistory)) { _ in
selectedTab = .dailyQuestion
}
.onReceive(NotificationCenter.default.publisher(for: .navigateToSettings)) { _ in
selectedTab = .settings
}
} }
} }