From bf3de8137bea5990fce572245dad6d7638156257 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 25 Jun 2026 12:34:43 -0500 Subject: [PATCH] fix(notif): deep-link results-ready pushes to per-session results/replay screen (E-003) --- .../PartnerNotificationManager.kt | 24 +++++++++++++++---- functions/src/couples/acceptInviteCallable.ts | 1 + functions/src/couples/onCoupleLeave.ts | 1 + functions/src/dates/createDateMatch.ts | 1 + functions/src/games/onGameSessionUpdate.ts | 15 ++++++++---- functions/src/notifications/gameRetention.ts | 6 +++++ functions/src/questions/onAnswerWritten.ts | 6 ++++- functions/src/questions/onMessageWritten.ts | 7 +++++- functions/src/users/onUserDelete.ts | 1 + 9 files changed, 51 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/app/closer/notifications/PartnerNotificationManager.kt b/app/src/main/java/app/closer/notifications/PartnerNotificationManager.kt index ad4eef99..313081e6 100644 --- a/app/src/main/java/app/closer/notifications/PartnerNotificationManager.kt +++ b/app/src/main/java/app/closer/notifications/PartnerNotificationManager.kt @@ -274,11 +274,10 @@ enum class PartnerNotificationType( PARTNER_STARTED_GAME -> gameRouteForType(payload.gameType) ?: AppRoute.PLAY PARTNER_COMPLETED_PART -> gameRouteForType(payload.gameType) ?: AppRoute.PLAY // Results-ready means the session is COMPLETED, so the plain game route would show "start a - // new game" (getActiveSession returns only active sessions). The correct target is the - // per-session results/replay route — but that needs the server to also send game_session_id - // in the FCM data (currently it sends only game_type). Until that server change ships, route - // to the Play hub rather than a misleading setup screen. E-003 (results-ready follow-up). - GAME_RESULTS_READY -> AppRoute.PLAY + // new game" (getActiveSession returns only active sessions). Deep link to the per-session + // results/replay route instead, using game_session_id + game_type from the FCM data. Falls + // back to the hub only if the server didn't send the session id. E-003 (results-ready). + GAME_RESULTS_READY -> gameResultsRouteFor(payload.gameType, payload.gameSessionId) ?: AppRoute.PLAY CHALLENGE_WAITING -> AppRoute.CONNECTION_CHALLENGES CAPSULE_UNLOCKED -> AppRoute.MEMORY_LANE GENTLE_REMINDER -> AppRoute.DAILY_QUESTION @@ -355,3 +354,18 @@ private fun gameRouteForType(gameType: String?): String? = when (gameType) { GameType.DESIRE_SYNC -> AppRoute.DESIRE_SYNC else -> null } + +/** + * The per-session results/replay route for a COMPLETED game, so a "results ready" push opens the + * actual results (not the game's setup screen). Needs both the game type and the session id. E-003. + */ +private fun gameResultsRouteFor(gameType: String?, sessionId: String?): String? { + if (sessionId.isNullOrBlank()) return null + return when (gameType) { + GameType.WHEEL -> AppRoute.wheelComplete(sessionId) + GameType.THIS_OR_THAT -> AppRoute.thisOrThatReplay(sessionId) + GameType.HOW_WELL -> AppRoute.howWellReplay(sessionId) + GameType.DESIRE_SYNC -> AppRoute.desireSyncReplay(sessionId) + else -> null + } +} diff --git a/functions/src/couples/acceptInviteCallable.ts b/functions/src/couples/acceptInviteCallable.ts index 600b9e33..47e191de 100644 --- a/functions/src/couples/acceptInviteCallable.ts +++ b/functions/src/couples/acceptInviteCallable.ts @@ -196,6 +196,7 @@ async function notifyPartnerJoined( title: 'Your partner joined!', body: "You're connected. Time to answer tonight's question together.", }, + android: { notification: { channelId: 'partner_activity' } }, // E-OBS data: { type: 'partner_joined', couple_id: coupleId, diff --git a/functions/src/couples/onCoupleLeave.ts b/functions/src/couples/onCoupleLeave.ts index ea2ac4bf..93c88500 100644 --- a/functions/src/couples/onCoupleLeave.ts +++ b/functions/src/couples/onCoupleLeave.ts @@ -104,6 +104,7 @@ export const onCoupleLeave = functions.firestore title: notificationPayload.title, body: notificationPayload.body, }, + android: { notification: { channelId: 'partner_activity' } }, // E-OBS data: { type: notificationPayload.type, }, diff --git a/functions/src/dates/createDateMatch.ts b/functions/src/dates/createDateMatch.ts index d2ae42df..c7a4b14e 100644 --- a/functions/src/dates/createDateMatch.ts +++ b/functions/src/dates/createDateMatch.ts @@ -68,6 +68,7 @@ async function notifyDateMatch( title: "It's a match!", body: "You both want to go on this date. Time to make it happen.", }, + android: { notification: { channelId: 'partner_activity' } }, // E-OBS data: { type: 'date_match', couple_id: coupleId, diff --git a/functions/src/games/onGameSessionUpdate.ts b/functions/src/games/onGameSessionUpdate.ts index a015b480..7c735a54 100644 --- a/functions/src/games/onGameSessionUpdate.ts +++ b/functions/src/games/onGameSessionUpdate.ts @@ -65,7 +65,7 @@ export const onGameSessionUpdate = functions.firestore await notifyPartner( db, messaging, recipientId, starterName, gameType, 'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId, - starterAvatar + starterAvatar, sessionId ) return } @@ -81,12 +81,12 @@ export const onGameSessionUpdate = functions.firestore await notifyPartner( db, messaging, partnerA, partnerBName, gt, 'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId, - avatarB + avatarB, sessionId ) await notifyPartner( db, messaging, partnerB, partnerAName, gt, 'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId, - avatarA + avatarA, sessionId ) return } @@ -104,7 +104,8 @@ async function notifyPartner( notificationType: string, body: string, coupleId: string, - senderAvatarUrl?: string + senderAvatarUrl?: string, + sessionId?: string ): Promise { const title = notificationType === 'partner_finished_game' @@ -161,10 +162,16 @@ async function notifyPartner( title: notificationPayload.title, body: notificationPayload.body, }, + // Put backgrounded notifications on the Games channel instead of the FCM fallback channel, + // so importance/sound and the per-category toggle apply. E-OBS. + android: { notification: { channelId: 'game_activity' } }, data: { type: notificationPayload.type, couple_id: coupleId, game_type: gameType, + // Lets the client deep link a results-ready push to the per-session results/replay screen + // (a completed session isn't returned by getActiveSession). E-003 results-ready. + ...(sessionId ? { game_session_id: sessionId } : {}), ...(senderAvatarUrl && senderAvatarUrl.length > 0 ? { sender_avatar_url: senderAvatarUrl } : {}), diff --git a/functions/src/notifications/gameRetention.ts b/functions/src/notifications/gameRetention.ts index de55b202..e8e8af1f 100644 --- a/functions/src/notifications/gameRetention.ts +++ b/functions/src/notifications/gameRetention.ts @@ -191,6 +191,12 @@ async function sendNotification( title: notification.title, body: notification.body, }, + // E-OBS: challenge reminders → Reminders channel; capsule-unlocked → partner-activity channel. + android: { + notification: { + channelId: notification.type === 'challenge_day_ready' ? 'reminders' : 'partner_activity', + }, + }, data: { type: notification.type, ...notification.data, diff --git a/functions/src/questions/onAnswerWritten.ts b/functions/src/questions/onAnswerWritten.ts index 00efcf72..c543e687 100644 --- a/functions/src/questions/onAnswerWritten.ts +++ b/functions/src/questions/onAnswerWritten.ts @@ -103,7 +103,11 @@ export const onAnswerWritten = functions.firestore const sendResults = await Promise.allSettled( tokens.map((token) => - admin.messaging().send({ ...payload, token } as admin.messaging.Message) + admin.messaging().send({ + ...payload, + token, + android: { notification: { channelId: 'partner_activity' } }, // E-OBS + } as admin.messaging.Message) ) ) diff --git a/functions/src/questions/onMessageWritten.ts b/functions/src/questions/onMessageWritten.ts index f2000a53..32ef876a 100644 --- a/functions/src/questions/onMessageWritten.ts +++ b/functions/src/questions/onMessageWritten.ts @@ -94,7 +94,12 @@ export const onMessageWritten = functions.firestore const sendResults = await Promise.allSettled( tokens.map((token) => - admin.messaging().send({ ...payload, token } as admin.messaging.Message) + admin.messaging().send({ + ...payload, + token, + // E-OBS: backgrounded delivery on the Chat/partner channel, not the FCM fallback channel. + android: { notification: { channelId: 'partner_activity' } }, + } as admin.messaging.Message) ) ) diff --git a/functions/src/users/onUserDelete.ts b/functions/src/users/onUserDelete.ts index 3ef0af95..80681092 100644 --- a/functions/src/users/onUserDelete.ts +++ b/functions/src/users/onUserDelete.ts @@ -118,6 +118,7 @@ async function notifyPartner( title: 'Your partner deleted their account', body: 'You are no longer paired. Tap to create a new invite.', }, + android: { notification: { channelId: 'partner_activity' } }, // E-OBS data: { type: 'partner_deleted_account' }, }) )