feat(notifications): deep link routing from FCM data extras, onMessageWritten includes author name + photo
- MainActivity: deepLinkRouteFromIntent resolves FCM data extras to navigation routes; pendingDeepLink state for onNewIntent - AppNavigation: LaunchedEffect waits for HOME route before navigating deep link (fixes race with onboarding) - onMessageWritten: includes author displayName + photoUrl in notification payload
This commit is contained in:
parent
06e4d609f2
commit
a8fbbaa286
|
|
@ -32,6 +32,8 @@ import app.closer.BuildConfig
|
|||
import app.closer.core.navigation.AppNavigation
|
||||
import app.closer.core.notifications.TokenRegistrar
|
||||
import app.closer.domain.model.AuthState
|
||||
import app.closer.notifications.PartnerNotificationPayload
|
||||
import app.closer.notifications.PartnerNotificationType
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
@ -55,10 +57,15 @@ class MainActivity : AppCompatActivity() {
|
|||
private val notificationPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
|
||||
|
||||
// Route to navigate to after a notification tap (set from the launch intent). Backed by state
|
||||
// so a tap while the app is already running (onNewIntent) also re-triggers navigation.
|
||||
private val pendingDeepLink = mutableStateOf<String?>(null)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
maybeRequestNotificationPermission()
|
||||
registerFcmToken()
|
||||
pendingDeepLink.value = deepLinkRouteFromIntent(intent)
|
||||
if (BuildConfig.DEBUG) attemptDebugAutoLogin()
|
||||
setContent {
|
||||
val settings by settingsRepository.settings.collectAsState(initial = AppSettings())
|
||||
|
|
@ -91,7 +98,10 @@ class MainActivity : AppCompatActivity() {
|
|||
onUnlocked = { sessionVerified = true }
|
||||
)
|
||||
} else {
|
||||
AppNavigation()
|
||||
AppNavigation(
|
||||
pendingDeepLink = pendingDeepLink.value,
|
||||
onDeepLinkConsumed = { pendingDeepLink.value = null }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -101,6 +111,28 @@ class MainActivity : AppCompatActivity() {
|
|||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
deepLinkRouteFromIntent(intent)?.let { pendingDeepLink.value = it }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a navigation route from a notification tap. When the app is backgrounded/closed the
|
||||
* OS shows the FCM `notification` block and, on tap, launches us with the message `data` as
|
||||
* plain intent extras (no deep-link Uri) — so we rebuild the route here. A real deep-link Uri
|
||||
* (from our own foreground-posted PendingIntent) is left for the NavHost to handle.
|
||||
*/
|
||||
private fun deepLinkRouteFromIntent(intent: Intent?): String? {
|
||||
intent ?: return null
|
||||
if (intent.data != null) return null
|
||||
val type = intent.getStringExtra("type") ?: return null
|
||||
val coupleId = intent.getStringExtra("couple_id") ?: ""
|
||||
val payload = PartnerNotificationPayload(
|
||||
questionId = intent.getStringExtra("question_id"),
|
||||
gameSessionId = intent.getStringExtra("game_session_id"),
|
||||
capsuleId = intent.getStringExtra("capsule_id"),
|
||||
challengeId = intent.getStringExtra("challenge_id"),
|
||||
avatarUrl = intent.getStringExtra("sender_avatar_url")
|
||||
)
|
||||
return PartnerNotificationType.fromRemoteType(type)?.routeFor(payload, coupleId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.navigation.NavGraph.Companion.findStartDestination
|
|||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import app.closer.ui.auth.ForgotPasswordScreen
|
||||
import app.closer.ui.answers.AnswerHistoryScreen
|
||||
import app.closer.ui.answers.AnswerRevealScreen
|
||||
|
|
@ -87,7 +88,9 @@ import app.closer.ui.games.WaitingForPartnerScreen
|
|||
@Composable
|
||||
fun AppNavigation(
|
||||
modifier: Modifier = Modifier,
|
||||
startDestination: String = AppRoute.ONBOARDING
|
||||
startDestination: String = AppRoute.ONBOARDING,
|
||||
pendingDeepLink: String? = null,
|
||||
onDeepLinkConsumed: () -> Unit = {}
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
|
@ -130,6 +133,22 @@ fun AppNavigation(
|
|||
}
|
||||
}
|
||||
|
||||
// A partner/chat notification was tapped while the app was backgrounded or closed. The OS
|
||||
// delivers the FCM payload as intent extras (not a deep-link Uri), so we resolve a route in
|
||||
// MainActivity and navigate here — but only once the user is past onboarding (authenticated +
|
||||
// on the main graph), otherwise the destination can't load and the tap appears to do nothing.
|
||||
LaunchedEffect(pendingDeepLink, currentRoute) {
|
||||
val link = pendingDeepLink ?: return@LaunchedEffect
|
||||
// Wait until the user has actually settled on Home (authenticated + onboarding finished).
|
||||
// Navigating during the onboarding→home transition races its popUpTo, which discards the
|
||||
// destination — the symptom of "the app opens but the message never loads".
|
||||
if (currentRoute == AppRoute.HOME) {
|
||||
kotlinx.coroutines.delay(350)
|
||||
navigateRoute(link)
|
||||
onDeepLinkConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
androidx.compose.foundation.layout.Box(
|
||||
modifier = androidx.compose.ui.Modifier.fillMaxSize()
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -81,9 +81,15 @@ export const onMessageWritten = functions.firestore
|
|||
return
|
||||
}
|
||||
|
||||
// The recipient sees the message from the author (their partner), so surface the author's
|
||||
// photo/name — the in-app chat bubble uses sender_avatar_url to show the partner's face.
|
||||
const authorDoc = await db.collection('users').doc(authorId).get()
|
||||
const authorPhotoUrl = (authorDoc.data()?.photoUrl as string | undefined) ?? ''
|
||||
const authorName = (authorDoc.data()?.displayName as string | undefined) ?? ''
|
||||
|
||||
const payload: admin.messaging.MessagingPayload = {
|
||||
notification: {
|
||||
title: 'Your partner sent a message',
|
||||
title: authorName ? `${authorName} sent a message` : 'Your partner sent a message',
|
||||
body: 'Tap to read and reply.',
|
||||
},
|
||||
data: {
|
||||
|
|
@ -91,6 +97,7 @@ export const onMessageWritten = functions.firestore
|
|||
couple_id: coupleId,
|
||||
thread_id: threadId,
|
||||
...(questionId ? { question_id: questionId } : {}),
|
||||
...(authorPhotoUrl ? { sender_avatar_url: authorPhotoUrl } : {}),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue