From cb7c14272c995715929c553355040d5e0012698b Mon Sep 17 00:00:00 2001 From: Joseph Sameh <20220099@stud.fci-cu.edu.eg> Date: Wed, 8 Jul 2026 03:10:38 +0300 Subject: [PATCH] Handle notification clicks and actionId routing Add notification click handling and route changes for FinancialAction flows. Key changes: - Replace FinancialActionRoute.payload with actionId and update related screens, ViewModel, and CategoriesFeatureApiImpl. - App: navigate to FinancialActionRoute when push payload contains actionId; otherwise map notification "type" (via NotificationType.fromStringOrDefault) to appropriate feature routes (Achievements, Goals, Profile, ScheduledPayments, Statistics, Notifications). - Add NotificationClickState to buffer and emit notification click payloads and consume pending payloads on startup. - Register KMPNotifier listener in initKoin to forward clicks to NotificationClickState. - Add NotificationType.fromStringOrDefault helper. - Set Android MainActivity launchMode to singleTask. --- androidApp/src/main/AndroidManifest.xml | 1 + .../com/spendoo/categories/api/Routes.kt | 2 +- .../api/CategoriesFeatureApiImpl.kt | 15 ++-- .../FinancialActionScreen.kt | 4 +- .../FinancialActionUiState.kt | 2 +- .../FinancialActionViewModel.kt | 6 +- .../domain/entity/NotificationType.kt | 8 ++- .../src/commonMain/kotlin/com/spendoo/App.kt | 70 +++++++++++++++++-- .../com/spendoo/NotificationClickState.kt | 22 ++++++ .../commonMain/kotlin/com/spendoo/initKoin.kt | 8 +++ 10 files changed, 116 insertions(+), 22 deletions(-) create mode 100644 spendooApp/src/commonMain/kotlin/com/spendoo/NotificationClickState.kt diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index f33c9dc..19a5e5a 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ diff --git a/categoriesApi/src/commonMain/kotlin/com/spendoo/categories/api/Routes.kt b/categoriesApi/src/commonMain/kotlin/com/spendoo/categories/api/Routes.kt index 7b96be2..35dd0a1 100644 --- a/categoriesApi/src/commonMain/kotlin/com/spendoo/categories/api/Routes.kt +++ b/categoriesApi/src/commonMain/kotlin/com/spendoo/categories/api/Routes.kt @@ -19,7 +19,7 @@ data class ScheduledPaymentDetailsRoute(val paymentId: String) : NavKey data class TransactionDetailsRoute(val transactionId: String) : NavKey @Serializable -data class FinancialActionRoute(val tile: String, val body: String, val payload: Map) : NavKey +data class FinancialActionRoute(val tile: String, val body: String, val actionId: String) : NavKey @Serializable data object TopSpendingCategoriesRoute : NavKey diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/api/CategoriesFeatureApiImpl.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/api/CategoriesFeatureApiImpl.kt index 719ce8f..2b754d8 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/api/CategoriesFeatureApiImpl.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/api/CategoriesFeatureApiImpl.kt @@ -7,22 +7,23 @@ import androidx.navigation3.scene.DialogSceneStrategy import com.spendoo.categories.api.AddTransactionRoute import com.spendoo.categories.api.CategoriesFeatureApi import com.spendoo.categories.api.CategoriesRoute +import com.spendoo.categories.api.CategoryOffersRoute +import com.spendoo.categories.api.EditTransactionRoute import com.spendoo.categories.api.FinancialActionRoute import com.spendoo.categories.api.ScheduledPaymentDetailsRoute import com.spendoo.categories.api.ScheduledPaymentsRoute +import com.spendoo.categories.api.TopSpendingCategoriesRoute import com.spendoo.categories.api.TransactionDetailsRoute import com.spendoo.categories.presentation.screen.addTransactionBottomSheet.AddTransactionBottomSheet import com.spendoo.categories.presentation.screen.categories.CategoriesScreen +import com.spendoo.categories.presentation.screen.categoryOffers.CategoryOffersScreen +import com.spendoo.categories.presentation.screen.editTransactionBottomSheet.EditTransactionBottomSheet import com.spendoo.categories.presentation.screen.financialActionScreen.FinancialActionScreen import com.spendoo.categories.presentation.screen.scheduledPaymentDetails.ScheduledPaymentDetailsScreen import com.spendoo.categories.presentation.screen.scheduledPayments.ScheduledPaymentsScreen -import com.spendoo.categories.presentation.screen.transactionDetails.TransactionDetailsScreen import com.spendoo.categories.presentation.screen.topSpendingCategories.TopSpendingCategoriesScreen -import com.spendoo.categories.presentation.screen.categoryOffers.CategoryOffersScreen -import com.spendoo.categories.presentation.screen.editTransactionBottomSheet.EditTransactionBottomSheet -import com.spendoo.categories.api.TopSpendingCategoriesRoute -import com.spendoo.categories.api.CategoryOffersRoute -import com.spendoo.categories.api.EditTransactionRoute +import com.spendoo.categories.presentation.screen.transactionDetails.TransactionDetailsScreen + class CategoriesFeatureApiImpl : CategoriesFeatureApi { override fun invoke(): (NavKey) -> NavEntry { @@ -44,7 +45,7 @@ class CategoriesFeatureApiImpl : CategoriesFeatureApi { FinancialActionScreen( tile = route.tile, body = route.body, - payload = route.payload + actionId = route.actionId ) } entry { TopSpendingCategoriesScreen() } diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionScreen.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionScreen.kt index 0b771b3..784bff9 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionScreen.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionScreen.kt @@ -22,9 +22,9 @@ import spendoo.designsystem.generated.resources.ok fun FinancialActionScreen( tile: String, body: String, - payload: Map, + actionId: String, viewModel: FinancialActionViewModel = - koinViewModel(parameters = { parametersOf(tile, body, payload) }) + koinViewModel(parameters = { parametersOf(tile, body, actionId) }) ) { val state by viewModel.state.collectAsStateWithLifecycle() diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionUiState.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionUiState.kt index b95bdfb..10ed161 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionUiState.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionUiState.kt @@ -5,6 +5,6 @@ import com.spendoo.designsystem.components.button.AppButtonState data class FinancialActionUiState( val tile: String = "", val body: String = "", - val payload: Map = emptyMap(), + val actionId: String = "", val buttonState: AppButtonState = AppButtonState.Enabled ) \ No newline at end of file diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionViewModel.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionViewModel.kt index 04c8681..3c2710a 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionViewModel.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionViewModel.kt @@ -11,7 +11,7 @@ import spendoo.designsystem.generated.resources.unknown_error class FinancialActionViewModel( private val navTile: String, private val navBody: String, - private val navPayload: Map, + private val navActionId: String, private val budgetActionRepository: BudgetActionRepository ) : BaseViewModel(FinancialActionUiState()), FinancialActionInteractionListener { @@ -21,7 +21,7 @@ class FinancialActionViewModel( copy( tile = navTile, body = navBody, - payload = navPayload + actionId = navActionId ) } } @@ -31,7 +31,7 @@ class FinancialActionViewModel( } override fun onExecute() { - val actionId = navPayload["actionId"] ?: return + val actionId = state.value.actionId tryToCall( onStart = { updateState { copy(buttonState = AppButtonState.Loading) } diff --git a/notificationsDomain/src/commonMain/kotlin/com/spendoo/notifications/domain/entity/NotificationType.kt b/notificationsDomain/src/commonMain/kotlin/com/spendoo/notifications/domain/entity/NotificationType.kt index d284821..8199a81 100644 --- a/notificationsDomain/src/commonMain/kotlin/com/spendoo/notifications/domain/entity/NotificationType.kt +++ b/notificationsDomain/src/commonMain/kotlin/com/spendoo/notifications/domain/entity/NotificationType.kt @@ -6,5 +6,11 @@ enum class NotificationType { USER_FOLLOW, PAYMENT_REMINDER, ALERT, - SYSTEM + SYSTEM; + + companion object { + fun fromStringOrDefault(value: String?): NotificationType { + return entries.find { it.name.equals(value, ignoreCase = true) } ?: SYSTEM + } + } } diff --git a/spendooApp/src/commonMain/kotlin/com/spendoo/App.kt b/spendooApp/src/commonMain/kotlin/com/spendoo/App.kt index 5ec3b1e..476e30c 100644 --- a/spendooApp/src/commonMain/kotlin/com/spendoo/App.kt +++ b/spendooApp/src/commonMain/kotlin/com/spendoo/App.kt @@ -14,16 +14,24 @@ import com.mmk.kmpnotifier.push.PushListener import com.mmk.kmpnotifier.push.firebase.addPushListener import com.spendoo.appEntryPoint.EntryPoint import com.spendoo.categories.api.FinancialActionRoute +import com.spendoo.categories.api.ScheduledPaymentsRoute import com.spendoo.designsystem.navigation.effector.Effector import com.spendoo.designsystem.theme.theme.SpendooTheme +import com.spendoo.goals.api.AchievementsRoute +import com.spendoo.goals.api.GoalsRoute +import com.spendoo.home.api.NotificationsRoute +import com.spendoo.identity.api.ProfileRoute import com.spendoo.identity.domain.repository.AuthenticationRepository import com.spendoo.identity.domain.repository.SettingsRepository import com.spendoo.identity.domain.util.AppLanguage import com.spendoo.identity.domain.util.AppLocalizer import com.spendoo.identity.domain.util.AppTheme +import com.spendoo.notifications.domain.entity.NotificationType +import com.spendoo.statistics.api.StatisticsRoute import com.spendoo.util.SetSystemBarsAppearance import com.spendoo.util.toStringMap import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.koin.compose.koinInject @@ -61,25 +69,39 @@ fun App( body: String?, data: PayloadData ) { - if (data.isEmpty()) { - KMPNotifier.localNotifier.notify { - this.title = title.orEmpty() - this.body = body.orEmpty() - } - } else { + val actionId = data["actionId"] as? String + if (actionId != null) { coroutineScope.launch(exceptionHandler) { effector.navigate( FinancialActionRoute( tile = title.orEmpty(), body = body.orEmpty(), - payload = data.toStringMap() + actionId = actionId ), forceNavigate = true ) } + } else { + KMPNotifier.localNotifier.notify { + this.title = title.orEmpty() + this.body = body.orEmpty() + this.payloadData = data.toStringMap() + } } } }) + + + NotificationClickState.consumePendingPayload()?.let { data -> + handleNotificationClick(data, coroutineScope, exceptionHandler, effector) + } + + coroutineScope.launch { + NotificationClickState.clickFlow.collect { data -> + NotificationClickState.consumePendingPayload() + handleNotificationClick(data, coroutineScope, exceptionHandler, effector) + } + } } val languageCode = if (currentLanguage == AppLanguage.DEFAULT) { @@ -97,3 +119,37 @@ fun App( } ) } + +private fun handleNotificationClick( + data: PayloadData, + coroutineScope: CoroutineScope, + exceptionHandler: CoroutineExceptionHandler, + effector: Effector +) { + val actionId = data["actionId"] as? String + coroutineScope.launch(exceptionHandler) { + if (actionId != null) { + effector.navigate( + FinancialActionRoute( + tile = (data["title"] as? String) ?: "Notification", + body = (data["body"] as? String).orEmpty(), + actionId = actionId + ), + forceNavigate = true + ) + } else { + val type = data["type"] as? String + val notificationType = NotificationType.fromStringOrDefault(type) + effector.navigate( + when (notificationType) { + NotificationType.ACHIEVEMENT -> AchievementsRoute + NotificationType.GOAL -> GoalsRoute + NotificationType.USER_FOLLOW -> ProfileRoute + NotificationType.PAYMENT_REMINDER -> ScheduledPaymentsRoute + NotificationType.ALERT -> StatisticsRoute() + NotificationType.SYSTEM -> NotificationsRoute + }, forceNavigate = true + ) + } + } +} diff --git a/spendooApp/src/commonMain/kotlin/com/spendoo/NotificationClickState.kt b/spendooApp/src/commonMain/kotlin/com/spendoo/NotificationClickState.kt new file mode 100644 index 0000000..02b9dc8 --- /dev/null +++ b/spendooApp/src/commonMain/kotlin/com/spendoo/NotificationClickState.kt @@ -0,0 +1,22 @@ +package com.spendoo + +import com.mmk.kmpnotifier.notification.PayloadData +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +object NotificationClickState { + private var pendingPayload: PayloadData? = null + private val _clickFlow = MutableSharedFlow(extraBufferCapacity = 1) + val clickFlow = _clickFlow.asSharedFlow() + + fun onNotificationClicked(data: PayloadData) { + pendingPayload = data + _clickFlow.tryEmit(data) + } + + fun consumePendingPayload(): PayloadData? { + val payload = pendingPayload + pendingPayload = null + return payload + } +} diff --git a/spendooApp/src/commonMain/kotlin/com/spendoo/initKoin.kt b/spendooApp/src/commonMain/kotlin/com/spendoo/initKoin.kt index 04500ca..d1b91d4 100644 --- a/spendooApp/src/commonMain/kotlin/com/spendoo/initKoin.kt +++ b/spendooApp/src/commonMain/kotlin/com/spendoo/initKoin.kt @@ -1,5 +1,7 @@ package com.spendoo +import com.mmk.kmpnotifier.KMPNotifier +import com.mmk.kmpnotifier.notification.PayloadData import com.spendoo.di.apiModule import com.spendoo.di.appModule import com.spendoo.di.featureModule @@ -9,6 +11,12 @@ import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration fun initKoin(config: KoinAppDeclaration? = null) { + KMPNotifier.addListener(object : KMPNotifier.Listener { + override fun onNotificationClicked(data: PayloadData) { + NotificationClickState.onNotificationClicked(data) + } + }) + startKoin { config?.invoke(this)