feat: code push -- notifications, cloud functions, iOS updates
This commit is contained in:
parent
7e3c61c6e4
commit
5e16177eb2
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"projects": {
|
"projects": {
|
||||||
"default": "closer-real-app"
|
"default": "closer-app-22014"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,9 +80,13 @@ 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) {
|
||||||
NotificationRateLimiter.Type.PARTNER_TRIGGER -> settings.partnerAnsweredEnabled
|
PartnerNotificationType.CHAT_MESSAGE -> settings.chatMessageEnabled
|
||||||
NotificationRateLimiter.Type.REMINDER -> settings.dailyReminderEnabled
|
PartnerNotificationType.OUTCOME_REMINDER -> settings.outcomeReminderEnabled
|
||||||
|
else -> when (type.rateType) {
|
||||||
|
NotificationRateLimiter.Type.PARTNER_TRIGGER -> settings.partnerAnsweredEnabled
|
||||||
|
NotificationRateLimiter.Type.REMINDER -> settings.dailyReminderEnabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
@ -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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"}
|
||||||
|
|
@ -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
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"dailyQuestionReminder.js","sourceRoot":"","sources":["../../src/notifications/dailyQuestionReminder.ts"],"names":[],"mappings":"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"}
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"reengagement.js","sourceRoot":"","sources":["../../src/notifications/reengagement.ts"],"names":[],"mappings":"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"}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC;;;;;;;;GAQG;AACU,QAAA,eAAe,GAAG,SAAS,CAAC,SAAS;KAC/C,QAAQ,CAAC,2DAA2D,CAAC;KACrE,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;;IAChC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAI1C,CAAA;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IACpE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,4BAA4B,QAAQ,YAAY,CAAC,CAAA;QAC9D,OAAM;IACR,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC,CAAA;IACvD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,iDAAiD,QAAQ,EAAE,CAAC,CAAA;QACzE,OAAM;IACR,CAAC;IAED,yEAAyE;IACzE,kFAAkF;IAClF,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IACxE,IAAI,cAAc,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,WAAW,GAAG,MAAA,cAAc,CAAC,IAAI,EAAE,0CAAE,QAAQ,CAAA;QACnD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9D,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QAC1B,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,EAAE;SAC3B,UAAU,CAAC,OAAO,CAAC;SACnB,GAAG,CAAC,SAAS,CAAC;SACd,UAAU,CAAC,WAAW,CAAC;SACvB,GAAG,EAAE,CAAA;IACR,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;;QACjC,MAAM,CAAC,GAAG,MAAA,GAAG,CAAC,IAAI,EAAE,0CAAE,KAAK,CAAA;QAC3B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;YACjE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,+CAA+C,SAAS,EAAE,CAAC,CAAA;QACvE,OAAM;IACR,CAAC;IAED,+EAA+E;IAC/E,MAAM,YAAY,GAAG,MAAA,cAAc,CAAC,IAAI,EAAE,0CAAE,oBAAoB,CAAA;IAChE,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,6BAA6B,SAAS,yCAAyC,CAAC,CAAA;QAC5F,OAAM;IACR,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAsC,CAAA;IAClE,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAA;IAEzF,MAAM,OAAO,GAAqC;QAChD,YAAY,EAAE;YACZ,KAAK,EAAE,6BAA6B;YACpC,IAAI,EAAE,4CAA4C;SACnD;QACD,IAAI,EAAE;YACJ,IAAI,EAAE,kBAAkB;YACxB,SAAS,EAAE,QAAQ;YACnB,WAAW,EAAE,UAAU;YACvB,IAAI;SACL;KACF,CAAA;IAED,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,UAAU,CAC1C,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACnB,KAAK,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,gCAAK,OAAO,KAAE,KAAK,GAA6B,CAAC,CACzE,CACF,CAAA;IAED,MAAM,QAAQ,GAAa,EAAE,CAAA;IAC7B,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QACpC,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACjC,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,QAAQ,CAAC,CAAA;IACzE,CAAC;IAED,0EAA0E;IAC1E,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;QAClD,cAAc,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;KAC7D,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,iDAAiD,EAAE,CAAC,CAAC,CAAC,CAAA;IAEnF,OAAO,CAAC,GAAG,CACT,sCAAsC,SAAS,eAAe,QAAQ,aAAa,UAAU,EAAE,CAChG,CAAA;AACH,CAAC,CAAC,CAAA"}
|
||||||
|
|
@ -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 }))));
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC;;;;;;;GAOG;AACU,QAAA,gBAAgB,GAAG,SAAS,CAAC,SAAS;KAChD,QAAQ,CAAC,qEAAqE,CAAC;KAC/E,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;;IAChC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,MAIjD,CAAA;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAsC,CAAA;IACnE,MAAM,QAAQ,GAAG,OAAO,WAAW,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAA;IAC/F,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,iDAAiD,SAAS,EAAE,CAAC,CAAA;QAC1E,OAAM;IACR,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IACpE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,6BAA6B,QAAQ,YAAY,CAAC,CAAA;QAC/D,OAAM;IACR,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAA;IACzD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,kDAAkD,QAAQ,EAAE,CAAC,CAAA;QAC1E,OAAM;IACR,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IAExE,+EAA+E;IAC/E,MAAM,YAAY,GAAG,MAAA,cAAc,CAAC,IAAI,EAAE,0CAAE,gBAAgB,CAAA;IAC5D,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,8BAA8B,SAAS,6BAA6B,CAAC,CAAA;QACjF,OAAM;IACR,CAAC;IAED,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,IAAI,cAAc,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,WAAW,GAAG,MAAA,cAAc,CAAC,IAAI,EAAE,0CAAE,QAAQ,CAAA;QACnD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9D,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QAC1B,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,EAAE;SAC3B,UAAU,CAAC,OAAO,CAAC;SACnB,GAAG,CAAC,SAAS,CAAC;SACd,UAAU,CAAC,WAAW,CAAC;SACvB,GAAG,EAAE,CAAA;IACR,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;;QACjC,MAAM,CAAC,GAAG,MAAA,GAAG,CAAC,IAAI,EAAE,0CAAE,KAAK,CAAA;QAC3B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;YACjE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,gDAAgD,SAAS,EAAE,CAAC,CAAA;QACxE,OAAM;IACR,CAAC;IAED,MAAM,OAAO,GAAqC;QAChD,YAAY,EAAE;YACZ,KAAK,EAAE,6BAA6B;YACpC,IAAI,EAAE,wBAAwB;SAC/B;QACD,IAAI,EAAE;YACJ,IAAI,EAAE,cAAc;YACpB,QAAQ;YACR,QAAQ;SACT;KACF,CAAA;IAED,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,UAAU,CAC1C,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACnB,KAAK,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,gCAAK,OAAO,KAAE,KAAK,GAA6B,CAAC,CACzE,CACF,CAAA;IAED,MAAM,QAAQ,GAAa,EAAE,CAAA;IAC7B,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QACpC,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACjC,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CAAC,+CAA+C,EAAE,QAAQ,CAAC,CAAA;IAC1E,CAAC;IAED,OAAO,CAAC,GAAG,CACT,uCAAuC,SAAS,eAAe,QAAQ,cAAc,QAAQ,EAAE,CAChG,CAAA;AACH,CAAC,CAAC,CAAA"}
|
{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":"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"}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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) },
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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}`
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue