From be160483d2af90b98829498bd2615b68520b8dd1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 13:54:22 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../di/domain/NotificationsDomainModule.kt | 35 +++- .../tangem/tap/routing/utils/ChildFactory.kt | 1 + .../com/tangem/common/routing/AppRoute.kt | 1 + .../common/ui/notifications/NotificationId.kt | 11 ++ .../local/preferences/PreferencesKeys.kt | 2 + .../DefaultNotificationsRepository.kt | 61 +----- .../DefaultPushNotificationsRepository.kt | 68 +++++++ .../notifications/di/NotificationsModule.kt | 6 + .../DefaultNotificationsRepositoryTest.kt | 175 +++++------------ .../DefaultPushNotificationsRepositoryTest.kt | 178 ++++++++++++++++++ .../notifications/GetApplicationIdUseCase.kt | 12 +- ...etworksAvailableForNotificationsUseCase.kt | 6 +- .../notifications/SendPushTokenUseCase.kt | 6 +- .../SetShouldShowNotificationUseCase.kt | 12 ++ .../ShouldShowNotificationUseCase.kt | 12 ++ .../repository/NotificationsRepository.kt | 44 +++-- .../repository/PushNotificationsRepository.kt | 20 ++ .../GetApplicationIdUseCaseTest.kt | 60 +++--- .../notifications/SendPushTokenUseCaseTest.kt | 16 +- .../component/ChooseManagedTokensComponent.kt | 1 + features/manage-tokens/impl/build.gradle.kts | 1 + .../model/ChooseManagedTokensModel.kt | 22 ++- .../entrypoint/model/SendEntryPointModel.kt | 28 ++- features/swap-v2/impl/build.gradle.kts | 1 + .../v2/impl/amount/model/SwapAmountModel.kt | 8 + 25 files changed, 519 insertions(+), 268 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt create mode 100644 data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt create mode 100644 data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt index c7b61fd1e0..b35ae23d95 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.notifications.* import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles import com.tangem.utils.notifications.PushNotificationsTokenProvider @@ -18,20 +19,22 @@ internal object NotificationsDomainModule { @Provides @Singleton - fun providesGetApplicationIdUseCase(notificationsRepository: NotificationsRepository): GetApplicationIdUseCase { + fun providesGetApplicationIdUseCase( + pushNotificationsRepository: PushNotificationsRepository, + ): GetApplicationIdUseCase { return GetApplicationIdUseCase( - notificationsRepository = notificationsRepository, + pushNotificationsRepository = pushNotificationsRepository, ) } @Provides @Singleton fun providesSendPushTokenUseCase( - notificationsRepository: NotificationsRepository, + pushNotificationsRepository: PushNotificationsRepository, pushNotificationsTokenProvider: PushNotificationsTokenProvider, ): SendPushTokenUseCase { return SendPushTokenUseCase( - notificationsRepository = notificationsRepository, + pushNotificationsRepository = pushNotificationsRepository, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } @@ -56,6 +59,26 @@ internal object NotificationsDomainModule { ) } + @Provides + @Singleton + fun providesShouldShowNotificationUseCase( + notificationsRepository: NotificationsRepository, + ): ShouldShowNotificationUseCase { + return ShouldShowNotificationUseCase( + notificationsRepository = notificationsRepository, + ) + } + + @Provides + @Singleton + fun providesSetShouldShowNotificationUseCase( + notificationsRepository: NotificationsRepository, + ): SetShouldShowNotificationUseCase { + return SetShouldShowNotificationUseCase( + notificationsRepository = notificationsRepository, + ) + } + @Provides @Singleton fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles { @@ -65,8 +88,8 @@ internal object NotificationsDomainModule { @Provides @Singleton fun provideGetNetworksAvailableForNotifications( - notificationsRepository: NotificationsRepository, + pushNotificationsRepository: PushNotificationsRepository, ): GetNetworksAvailableForNotificationsUseCase { - return GetNetworksAvailableForNotificationsUseCase(notificationsRepository = notificationsRepository) + return GetNetworksAvailableForNotificationsUseCase(pushNotificationsRepository = pushNotificationsRepository) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 9ecfe8e5c6..c18697013f 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -434,6 +434,7 @@ internal class ChildFactory @Inject constructor( initialCurrency = route.initialCurrency, selectedCurrency = route.selectedCurrency, source = ChooseManagedTokensComponent.Source.valueOf(route.source.name), + showSendViaSwapNotification = route.showSendViaSwapNotification, ), componentFactory = chooseManagedTokensComponentFactory, ) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index d1afb593f0..def139603e 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -133,6 +133,7 @@ sealed class AppRoute(val path: String) : Route { val initialCurrency: CryptoCurrency, val selectedCurrency: CryptoCurrency?, val source: Source, + val showSendViaSwapNotification: Boolean, ) : AppRoute(path = "/$source/choose_managed_tokens/$userWalletId/${initialCurrency.id.value}") { enum class Source { SendViaSwap, diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt new file mode 100644 index 0000000000..4b770200d0 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt @@ -0,0 +1,11 @@ +package com.tangem.common.ui.notifications + +/** + * NotificationId represents unique identifiers for notifications in the app. + * + * These ids can be used with [ShouldShowNotificationUseCase] and [SetShouldShowNotificationUseCase] + * to check or update the visibility state of notifications. + */ +enum class NotificationId(val key: String) { + SendViaSwapTokenSelectorNotification("SendViaSwapTokenSelectorNotificationKey"), +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 874a358083..d39a5c15a8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -137,6 +137,8 @@ object PreferencesKeys { val TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY by lazy { intPreferencesKey(name = "tronNetworkFeeNotificationShowCount") } + + fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key") // endregion // region Promo diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt index d1f92e8262..dcb85f6ef2 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -1,48 +1,22 @@ package com.tangem.data.notifications -import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter -import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody -import com.tangem.utils.info.AppInfoProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.* -import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.notifications.repository.NotificationsRepository -import com.tangem.domain.notifications.models.NotificationsEligibleNetwork -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import javax.inject.Inject -internal class DefaultNotificationsRepository @Inject constructor( - private val tangemTechApi: TangemTechApi, - private val appInfoProvider: AppInfoProvider, +class DefaultNotificationsRepository @Inject constructor( private val appPreferencesStore: AppPreferencesStore, - private val dispatchers: CoroutineDispatcherProvider, ) : NotificationsRepository { - override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) { - tangemTechApi.createApplicationId( - NotificationApplicationCreateBody( - platform = appInfoProvider.platform.lowercase(), - device = appInfoProvider.device, - systemVersion = appInfoProvider.osVersion, - language = appInfoProvider.language, - timezone = appInfoProvider.timezone, - version = appInfoProvider.appVersion, - pushToken = pushToken, - ), - ).getOrThrow().appId.let(::ApplicationId) + override suspend fun shouldShowNotification(key: String): Boolean { + return appPreferencesStore.getSyncOrDefault(PreferencesKeys.getShouldShowNotificationKey(key), true) } - override suspend fun saveApplicationId(appId: ApplicationId) { - appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value) - } - - override suspend fun getApplicationId(): ApplicationId? { - return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) - ?.let(::ApplicationId) + override suspend fun setShouldShowNotifications(key: String, value: Boolean) { + appPreferencesStore.store(PreferencesKeys.getShouldShowNotificationKey(key), value) } override suspend fun incrementTronTokenFeeNotificationShowCounter() { @@ -61,25 +35,4 @@ internal class DefaultNotificationsRepository @Inject constructor( default = 0, ) } - - override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { - withContext(dispatchers.io) { - tangemTechApi.updatePushTokenForApplicationId( - appId.value, - NotificationApplicationCreateBody( - pushToken = pushToken, - systemVersion = appInfoProvider.osVersion, - language = appInfoProvider.language, - timezone = appInfoProvider.timezone, - version = appInfoProvider.appVersion, - ), - ).getOrThrow() - } - } - - override suspend fun getEligibleNetworks(): List = withContext(dispatchers.io) { - tangemTechApi.getEligibleNetworksForPushNotifications().getOrThrow().mapNotNull { - NotificationsEligibleNetworkConverter.convert(it) - } - } } \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt new file mode 100644 index 0000000000..b881aff0c5 --- /dev/null +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt @@ -0,0 +1,68 @@ +package com.tangem.data.notifications + +import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody +import com.tangem.utils.info.AppInfoProvider +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.* +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.repository.PushNotificationsRepository +import com.tangem.domain.notifications.models.NotificationsEligibleNetwork +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultPushNotificationsRepository @Inject constructor( + private val tangemTechApi: TangemTechApi, + private val appInfoProvider: AppInfoProvider, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : PushNotificationsRepository { + + override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) { + tangemTechApi.createApplicationId( + NotificationApplicationCreateBody( + platform = appInfoProvider.platform.lowercase(), + device = appInfoProvider.device, + systemVersion = appInfoProvider.osVersion, + language = appInfoProvider.language, + timezone = appInfoProvider.timezone, + version = appInfoProvider.appVersion, + pushToken = pushToken, + ), + ).getOrThrow().appId.let(::ApplicationId) + } + + override suspend fun saveApplicationId(appId: ApplicationId) { + appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value) + } + + override suspend fun getApplicationId(): ApplicationId? { + return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) + ?.let(::ApplicationId) + } + + override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { + withContext(dispatchers.io) { + tangemTechApi.updatePushTokenForApplicationId( + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + systemVersion = appInfoProvider.osVersion, + language = appInfoProvider.language, + timezone = appInfoProvider.timezone, + version = appInfoProvider.appVersion, + ), + ).getOrThrow() + } + } + + override suspend fun getEligibleNetworks(): List = withContext(dispatchers.io) { + tangemTechApi.getEligibleNetworksForPushNotifications().getOrThrow().mapNotNull { + NotificationsEligibleNetworkConverter.convert(it) + } + } +} \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt index 555ac66802..18f26e5763 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt @@ -1,7 +1,9 @@ package com.tangem.data.notifications.di import com.tangem.data.notifications.DefaultNotificationsRepository +import com.tangem.data.notifications.DefaultPushNotificationsRepository import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -12,6 +14,10 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal interface NotificationsModule { + @Binds + @Singleton + fun bindPushNotificationsRepository(repository: DefaultPushNotificationsRepository): PushNotificationsRepository + @Binds @Singleton fun bindNotificationsRepository(repository: DefaultNotificationsRepository): NotificationsRepository diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt index 13d4dbb6b1..a92861eca6 100644 --- a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt @@ -1,30 +1,21 @@ package com.tangem.data.notifications -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.stringPreferencesKey -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.utils.info.AppInfoProvider +import com.google.common.truth.Truth.assertThat import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.google.common.truth.Truth.assertThat -import com.squareup.moshi.Moshi -import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.tangemTech.models.* -import com.tangem.domain.notifications.models.ApplicationId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test +import androidx.datastore.preferences.core.Preferences +import com.squareup.moshi.Moshi +import androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.flowOf class DefaultNotificationsRepositoryTest { - private val tangemTechApi: TangemTechApi = mockk() - private val appInfoProvider: AppInfoProvider = mockk() private val preferencesDataStore: DataStore = mockk() private val appPreferencesStore = AppPreferencesStore( moshi = Moshi.Builder().build(), @@ -32,147 +23,77 @@ class DefaultNotificationsRepositoryTest { preferencesDataStore = preferencesDataStore, ) private val repository = DefaultNotificationsRepository( - tangemTechApi = tangemTechApi, - appInfoProvider = appInfoProvider, appPreferencesStore = appPreferencesStore, - dispatchers = TestingCoroutineDispatcherProvider(), ) @Test - fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest { + fun `GIVEN shouldShowNotification returns true WHEN called THEN returns true`() = runTest { // GIVEN - val pushToken = "test-push-token" - val expectedAppId = ApplicationId("test-app-id") - val expectedAppIdResponse = NotificationApplicationIdResponse( - appId = expectedAppId.value, - ) - coEvery { appInfoProvider.platform } returns "android" - coEvery { appInfoProvider.device } returns "test-device" - coEvery { appInfoProvider.osVersion } returns "11" - coEvery { appInfoProvider.language } returns "en" - coEvery { appInfoProvider.appVersion } returns "5.21.1" - coEvery { appInfoProvider.timezone } returns "UTC" - coEvery { tangemTechApi.createApplicationId(any()) } returns ApiResponse.Success( - expectedAppIdResponse, - ) + val key = "test-key" + val preferences = mockk(relaxed = true) + every { preferences[PreferencesKeys.getShouldShowNotificationKey(key)] } returns true + coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN - val result = repository.createApplicationId(pushToken) + val result = repository.shouldShowNotification(key) // THEN - assertThat(result).isEqualTo(expectedAppId) - coVerify { - tangemTechApi.createApplicationId( - NotificationApplicationCreateBody( - platform = "android", - device = "test-device", - systemVersion = "11", - language = "en", - timezone = "UTC", - version = "5.21.1", - pushToken = pushToken, - ), - ) - } + assertThat(result).isTrue() } @Test - fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest { + fun `GIVEN shouldShowNotification returns false WHEN called THEN returns false`() = runTest { // GIVEN - val appId = ApplicationId("test-app-id") + val key = "test-key" val preferences = mockk(relaxed = true) - coEvery { preferencesDataStore.updateData(any()) } returns preferences + every { preferences[PreferencesKeys.getShouldShowNotificationKey(key)] } returns false + coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN - repository.saveApplicationId(appId) + val result = repository.shouldShowNotification(key) + + // THEN + assertThat(result).isFalse() + } + + @Test + fun `GIVEN setShouldShowNotifications WHEN called THEN stores value in preferences`() = runTest { + // GIVEN + val key = "test-key" + val value = false + coEvery { preferencesDataStore.updateData(any()) } returns mockk(relaxed = true) + + // WHEN + repository.setShouldShowNotifications(key, value) // THEN coVerify { preferencesDataStore.updateData(any()) } } @Test - fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest { + fun `GIVEN incrementTronTokenFeeNotificationShowCounter WHEN called THEN increments counter`() = runTest { // GIVEN - val expectedAppId = ApplicationId("test-app-id") + coEvery { preferencesDataStore.updateData(any()) } returns mockk(relaxed = true) + + // WHEN + repository.incrementTronTokenFeeNotificationShowCounter() + + // THEN + coVerify { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN getTronTokenFeeNotificationShowCounter WHEN called THEN returns counter value`() = runTest { + // GIVEN + val expectedCount = 5 val preferences = mockk(relaxed = true) - val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name) - every { preferences[key] } returns expectedAppId.value + every { preferences[PreferencesKeys.TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY] } returns expectedCount coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN - val result = repository.getApplicationId() + val result = repository.getTronTokenFeeNotificationShowCounter() // THEN - assertThat(result).isEqualTo(expectedAppId) - } - - @Test - fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { - // GIVEN - val appId = ApplicationId("test-app-id") - val pushToken = "test-push-token" - coEvery { appInfoProvider.device } returns "test-device" - coEvery { appInfoProvider.osVersion } returns "11" - coEvery { appInfoProvider.language } returns "en" - coEvery { appInfoProvider.appVersion } returns "5.21.1" - coEvery { appInfoProvider.timezone } returns "UTC" - coEvery { - tangemTechApi.updatePushTokenForApplicationId( - appId.value, - NotificationApplicationCreateBody( - pushToken = pushToken, - systemVersion = "11", - language = "en", - timezone = "UTC", - version = "5.21.1", - ), - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.sendPushToken(appId, pushToken) - - // THEN - coVerify { - tangemTechApi.updatePushTokenForApplicationId( - appId.value, - NotificationApplicationCreateBody( - pushToken = pushToken, - systemVersion = "11", - language = "en", - timezone = "UTC", - version = "5.21.1", - ), - ) - } - } - - @Test - fun `GIVEN eligible networks WHEN getEligibleNetworks THEN returns converted networks`() = runTest { - // GIVEN - val expectedNetworks = listOf( - CryptoNetworkResponse( - id = 1, - name = "Ethereum", - networkId = "ethereum", - ), - CryptoNetworkResponse( - id = 2, - name = "Bitcoin", - networkId = "bitcoin", - ), - ) - coEvery { tangemTechApi.getEligibleNetworksForPushNotifications() } returns ApiResponse.Success( - expectedNetworks, - ) - - // WHEN - val result = repository.getEligibleNetworks() - - // THEN - assertThat(result).hasSize(2) - assertThat(result[0]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[0])) - assertThat(result[1]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[1])) - coVerify { tangemTechApi.getEligibleNetworksForPushNotifications() } + assertThat(result).isEqualTo(expectedCount) } } \ No newline at end of file diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt new file mode 100644 index 0000000000..b423cb27ed --- /dev/null +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt @@ -0,0 +1,178 @@ +package com.tangem.data.notifications + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.utils.info.AppInfoProvider +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.Moshi +import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.tangemTech.models.* +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultPushNotificationsRepositoryTest { + private val tangemTechApi: TangemTechApi = mockk() + private val appInfoProvider: AppInfoProvider = mockk() + private val preferencesDataStore: DataStore = mockk() + private val appPreferencesStore = AppPreferencesStore( + moshi = Moshi.Builder().build(), + dispatchers = TestingCoroutineDispatcherProvider(), + preferencesDataStore = preferencesDataStore, + ) + private val repository = DefaultPushNotificationsRepository( + tangemTechApi = tangemTechApi, + appInfoProvider = appInfoProvider, + appPreferencesStore = appPreferencesStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest { + // GIVEN + val pushToken = "test-push-token" + val expectedAppId = ApplicationId("test-app-id") + val expectedAppIdResponse = NotificationApplicationIdResponse( + appId = expectedAppId.value, + ) + coEvery { appInfoProvider.platform } returns "android" + coEvery { appInfoProvider.device } returns "test-device" + coEvery { appInfoProvider.osVersion } returns "11" + coEvery { appInfoProvider.language } returns "en" + coEvery { appInfoProvider.appVersion } returns "5.21.1" + coEvery { appInfoProvider.timezone } returns "UTC" + coEvery { tangemTechApi.createApplicationId(any()) } returns ApiResponse.Success( + expectedAppIdResponse, + ) + + // WHEN + val result = repository.createApplicationId(pushToken) + + // THEN + assertThat(result).isEqualTo(expectedAppId) + coVerify { + tangemTechApi.createApplicationId( + NotificationApplicationCreateBody( + platform = "android", + device = "test-device", + systemVersion = "11", + language = "en", + timezone = "UTC", + version = "5.21.1", + pushToken = pushToken, + ), + ) + } + } + + @Test + fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest { + // GIVEN + val appId = ApplicationId("test-app-id") + val preferences = mockk(relaxed = true) + coEvery { preferencesDataStore.updateData(any()) } returns preferences + + // WHEN + repository.saveApplicationId(appId) + + // THEN + coVerify { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest { + // GIVEN + val expectedAppId = ApplicationId("test-app-id") + val preferences = mockk(relaxed = true) + val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name) + every { preferences[key] } returns expectedAppId.value + coEvery { preferencesDataStore.data } returns flowOf(preferences) + + // WHEN + val result = repository.getApplicationId() + + // THEN + assertThat(result).isEqualTo(expectedAppId) + } + + @Test + fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { + // GIVEN + val appId = ApplicationId("test-app-id") + val pushToken = "test-push-token" + coEvery { appInfoProvider.device } returns "test-device" + coEvery { appInfoProvider.osVersion } returns "11" + coEvery { appInfoProvider.language } returns "en" + coEvery { appInfoProvider.appVersion } returns "5.21.1" + coEvery { appInfoProvider.timezone } returns "UTC" + coEvery { + tangemTechApi.updatePushTokenForApplicationId( + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + systemVersion = "11", + language = "en", + timezone = "UTC", + version = "5.21.1", + ), + ) + } returns ApiResponse.Success(Unit) + + // WHEN + repository.sendPushToken(appId, pushToken) + + // THEN + coVerify { + tangemTechApi.updatePushTokenForApplicationId( + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + systemVersion = "11", + language = "en", + timezone = "UTC", + version = "5.21.1", + ), + ) + } + } + + @Test + fun `GIVEN eligible networks WHEN getEligibleNetworks THEN returns converted networks`() = runTest { + // GIVEN + val expectedNetworks = listOf( + CryptoNetworkResponse( + id = 1, + name = "Ethereum", + networkId = "ethereum", + ), + CryptoNetworkResponse( + id = 2, + name = "Bitcoin", + networkId = "bitcoin", + ), + ) + coEvery { tangemTechApi.getEligibleNetworksForPushNotifications() } returns ApiResponse.Success( + expectedNetworks, + ) + + // WHEN + val result = repository.getEligibleNetworks() + + // THEN + assertThat(result).hasSize(2) + assertThat(result[0]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[0])) + assertThat(result[1]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[1])) + coVerify { tangemTechApi.getEligibleNetworksForPushNotifications() } + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt index 963d3b99d7..289328d75d 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt @@ -2,25 +2,25 @@ package com.tangem.domain.notifications import arrow.core.Either import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock class GetApplicationIdUseCase( - private val notificationsRepository: NotificationsRepository, + private val pushNotificationsRepository: PushNotificationsRepository, ) { private val mutex = Mutex() suspend operator fun invoke(): Either = Either.catch { - val localApplicationId = notificationsRepository.getApplicationId() + val localApplicationId = pushNotificationsRepository.getApplicationId() if (localApplicationId != null) return@catch localApplicationId mutex.withLock { - val doubleCheckedId = notificationsRepository.getApplicationId() + val doubleCheckedId = pushNotificationsRepository.getApplicationId() if (doubleCheckedId != null) return@withLock doubleCheckedId - val newApplicationId = notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(newApplicationId) + val newApplicationId = pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(newApplicationId) newApplicationId } } diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt index edc3de5294..9264da1a16 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt @@ -2,13 +2,13 @@ package com.tangem.domain.notifications import arrow.core.Either import com.tangem.domain.notifications.models.NotificationsEligibleNetwork -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository class GetNetworksAvailableForNotificationsUseCase( - private val notificationsRepository: NotificationsRepository, + private val pushNotificationsRepository: PushNotificationsRepository, ) { suspend operator fun invoke(): Either> = Either.catch { - notificationsRepository.getEligibleNetworks() + pushNotificationsRepository.getEligibleNetworks() } } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt index 63247b9a7d..d33888f342 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt @@ -2,16 +2,16 @@ package com.tangem.domain.notifications import arrow.core.Either import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider class SendPushTokenUseCase( - private val notificationsRepository: NotificationsRepository, + private val pushNotificationsRepository: PushNotificationsRepository, private val pushNotificationsTokenProvider: PushNotificationsTokenProvider, ) { suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { val token = pushNotificationsTokenProvider.getToken() - notificationsRepository.sendPushToken(applicationId, token) + pushNotificationsRepository.sendPushToken(applicationId, token) } } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt new file mode 100644 index 0000000000..44413fe106 --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.notifications + +import com.tangem.domain.notifications.repository.NotificationsRepository + +class SetShouldShowNotificationUseCase( + private val notificationsRepository: NotificationsRepository, +) { + + suspend operator fun invoke(key: String, value: Boolean) { + notificationsRepository.setShouldShowNotifications(key, value) + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt new file mode 100644 index 0000000000..657ab7819d --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.notifications + +import com.tangem.domain.notifications.repository.NotificationsRepository + +class ShouldShowNotificationUseCase( + private val notificationsRepository: NotificationsRepository, +) { + + suspend operator fun invoke(key: String): Boolean { + return notificationsRepository.shouldShowNotification(key) + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt index a8c46854df..377fc84984 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt @@ -1,24 +1,40 @@ package com.tangem.domain.notifications.repository -import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.models.NotificationsEligibleNetwork - +/** + * Repository interface for managing local notification logic and state. + * + * This interface provides methods to check and update whether specific notifications should be shown, + * as well as to track the display count for certain notifications (e.g., Tron token fee). + * + * Note: This repository is responsible only for the local logic and state (such as preferences and counters) + * regarding notifications. It does **not** directly show or hide notifications to the user. + * The actual display and hiding of notifications in the UI is handled by [NotificationsUM], + * which uses this repository to determine the appropriate behavior. + */ interface NotificationsRepository { - @Throws - suspend fun createApplicationId(pushToken: String? = null): ApplicationId + /** + * Checks whether a notification with the given [key] should be shown to the user. + * @param key The unique identifier for the notification. + * @return true if the notification should be shown, false otherwise. + */ + suspend fun shouldShowNotification(key: String): Boolean - suspend fun saveApplicationId(appId: ApplicationId) - - suspend fun getApplicationId(): ApplicationId? + /** + * Sets whether a notification with the given [key] should be shown to the user. + * @param key The unique identifier for the notification. + * @param value true if the notification should be shown, false otherwise. + */ + suspend fun setShouldShowNotifications(key: String, value: Boolean) + /** + * Gets the number of times the Tron token fee notification has been shown. + * @return The current show counter for the Tron token fee notification. + */ suspend fun getTronTokenFeeNotificationShowCounter(): Int + /** + * Increments the counter tracking how many times the Tron token fee notification has been shown. + */ suspend fun incrementTronTokenFeeNotificationShowCounter() - - @Throws - suspend fun sendPushToken(appId: ApplicationId, pushToken: String) - - @Throws - suspend fun getEligibleNetworks(): List } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt new file mode 100644 index 0000000000..338a74b0ce --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.notifications.repository + +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.models.NotificationsEligibleNetwork + +interface PushNotificationsRepository { + + @Throws + suspend fun createApplicationId(pushToken: String? = null): ApplicationId + + suspend fun saveApplicationId(appId: ApplicationId) + + suspend fun getApplicationId(): ApplicationId? + + @Throws + suspend fun sendPushToken(appId: ApplicationId, pushToken: String) + + @Throws + suspend fun getEligibleNetworks(): List +} \ No newline at end of file diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt index 6b2984554b..e3a191f205 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt @@ -3,7 +3,7 @@ package com.tangem.domain.notifications import arrow.core.Either import com.google.common.truth.Truth.assertThat import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import io.mockk.coEvery import io.mockk.coVerify import io.mockk.coVerifyOrder @@ -15,14 +15,14 @@ import java.net.SocketTimeoutException class GetApplicationIdUseCaseTest { - private val notificationsRepository: NotificationsRepository = mockk() - private val useCase = GetApplicationIdUseCase(notificationsRepository) + private val pushNotificationsRepository: PushNotificationsRepository = mockk() + private val useCase = GetApplicationIdUseCase(pushNotificationsRepository) @Test fun `GIVEN local application ID exists WHEN invoke THEN return local application ID`() = runTest { // GIVEN val expectedApplicationId = ApplicationId("test-app-id") - coEvery { notificationsRepository.getApplicationId() } returns expectedApplicationId + coEvery { pushNotificationsRepository.getApplicationId() } returns expectedApplicationId // WHEN val result = useCase() @@ -30,10 +30,10 @@ class GetApplicationIdUseCaseTest { // THEN assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(expectedApplicationId) - coVerify(exactly = 1) { notificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.getApplicationId() } coVerify(inverse = true) { - notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(any()) + pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(any()) } } @@ -41,9 +41,9 @@ class GetApplicationIdUseCaseTest { fun `GIVEN local application ID does not exist WHEN invoke THEN create and save new application ID`() = runTest { // GIVEN val newApplicationId = ApplicationId("new-app-id") - coEvery { notificationsRepository.getApplicationId() } returns null - coEvery { notificationsRepository.createApplicationId() } returns newApplicationId - coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + coEvery { pushNotificationsRepository.getApplicationId() } returns null + coEvery { pushNotificationsRepository.createApplicationId() } returns newApplicationId + coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit // WHEN val result = useCase() @@ -52,10 +52,10 @@ class GetApplicationIdUseCaseTest { assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(newApplicationId) coVerifyOrder { - notificationsRepository.getApplicationId() - notificationsRepository.getApplicationId() - notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(newApplicationId) + pushNotificationsRepository.getApplicationId() + pushNotificationsRepository.getApplicationId() + pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(newApplicationId) } } @@ -63,7 +63,7 @@ class GetApplicationIdUseCaseTest { fun `GIVEN repository throws exception WHEN invoke THEN return Either Left with error`() = runTest { // GIVEN val expectedError = SocketTimeoutException("Test error") - coEvery { notificationsRepository.getApplicationId() } throws expectedError + coEvery { pushNotificationsRepository.getApplicationId() } throws expectedError // WHEN val result = useCase() @@ -71,10 +71,10 @@ class GetApplicationIdUseCaseTest { // THEN assertThat(result).isInstanceOf(Either.Left::class.java) assertThat((result as Either.Left).value).isEqualTo(expectedError) - coVerify(exactly = 1) { notificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.getApplicationId() } coVerify(inverse = true) { - notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(any()) + pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(any()) } } @@ -85,14 +85,14 @@ class GetApplicationIdUseCaseTest { val newApplicationId = ApplicationId("new-app-id") var isIdCreated = false - coEvery { notificationsRepository.getApplicationId() } answers { + coEvery { pushNotificationsRepository.getApplicationId() } answers { if (!isIdCreated) null else newApplicationId } - coEvery { notificationsRepository.createApplicationId() } answers { + coEvery { pushNotificationsRepository.createApplicationId() } answers { isIdCreated = true newApplicationId } - coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit // WHEN val results = coroutineScope { @@ -108,9 +108,9 @@ class GetApplicationIdUseCaseTest { assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(newApplicationId) } - coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() } - coVerify(exactly = 1) { notificationsRepository.createApplicationId() } - coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) } + coVerify(exactly = PARALLEL_COUNT + 1) { pushNotificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.createApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.saveApplicationId(newApplicationId) } } @Test @@ -119,14 +119,14 @@ class GetApplicationIdUseCaseTest { val newApplicationId = ApplicationId("new-app-id") var isIdCreated = false - coEvery { notificationsRepository.getApplicationId() } answers { + coEvery { pushNotificationsRepository.getApplicationId() } answers { if (!isIdCreated) null else newApplicationId } - coEvery { notificationsRepository.createApplicationId() } answers { + coEvery { pushNotificationsRepository.createApplicationId() } answers { isIdCreated = true newApplicationId } - coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit // WHEN val results = coroutineScope { @@ -143,9 +143,9 @@ class GetApplicationIdUseCaseTest { assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(newApplicationId) } - coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() } - coVerify(exactly = 1) { notificationsRepository.createApplicationId() } - coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) } + coVerify(exactly = PARALLEL_COUNT + 1) { pushNotificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.createApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.saveApplicationId(newApplicationId) } } companion object { diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt index 18d716fdf0..fe84d79f16 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt @@ -3,7 +3,7 @@ package com.tangem.domain.notifications import arrow.core.Either import com.google.common.truth.Truth.assertThat import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider import io.mockk.coEvery import io.mockk.coVerify @@ -14,16 +14,16 @@ import org.junit.Test class SendPushTokenUseCaseTest { - private lateinit var notificationsRepository: NotificationsRepository + private lateinit var pushNotificationsRepository: PushNotificationsRepository private lateinit var pushNotificationsTokenProvider: PushNotificationsTokenProvider private lateinit var sendPushTokenUseCase: SendPushTokenUseCase @Before fun setup() { - notificationsRepository = mockk() + pushNotificationsRepository = mockk() pushNotificationsTokenProvider = mockk() sendPushTokenUseCase = SendPushTokenUseCase( - notificationsRepository = notificationsRepository, + pushNotificationsRepository = pushNotificationsRepository, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } @@ -34,14 +34,14 @@ class SendPushTokenUseCaseTest { val applicationId = ApplicationId("test-app-id") val token = "test-token" coEvery { pushNotificationsTokenProvider.getToken() } returns token - coEvery { notificationsRepository.sendPushToken(applicationId, token) } returns Unit + coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } returns Unit // WHEN val result = sendPushTokenUseCase(applicationId) // THEN assertThat(result).isEqualTo(Either.Right(Unit)) - coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) } + coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, token) } } @Test @@ -51,13 +51,13 @@ class SendPushTokenUseCaseTest { val token = "test-token" val expectedError = RuntimeException("Network error") coEvery { pushNotificationsTokenProvider.getToken() } returns token - coEvery { notificationsRepository.sendPushToken(applicationId, token) } throws expectedError + coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } throws expectedError // WHEN val result = sendPushTokenUseCase(applicationId) // THEN assertThat(result).isEqualTo(Either.Left(expectedError)) - coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) } + coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, token) } } } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt index a12efc7654..d17cd2b862 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt @@ -12,6 +12,7 @@ interface ChooseManagedTokensComponent : ComposableContentComponent { val initialCurrency: CryptoCurrency, val selectedCurrency: CryptoCurrency?, val source: Source, + val showSendViaSwapNotification: Boolean, ) enum class Source { diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index e28cec1763..c6a2a28283 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.swap.models) + implementation(projects.domain.notifications) /* AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index 83d65cc3a8..fe98829fd5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.managetokens.choosetoken.model import androidx.annotation.StringRes import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.tangem.common.ui.notifications.NotificationId import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -15,6 +16,7 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.notifications.SetShouldShowNotificationUseCase import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM import com.tangem.features.managetokens.component.ChooseManagedTokensComponent @@ -43,6 +45,7 @@ internal class ChooseManagedTokensModel @Inject constructor( private val router: Router, override val dispatchers: CoroutineDispatcherProvider, private val uiMessageSender: UiMessageSender, + private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase, paramsContainer: ParamsContainer, manageTokensListManagerFactory: ManageTokensListManager.Factory, ) : Model() { @@ -107,18 +110,21 @@ internal class ChooseManagedTokensModel @Inject constructor( } private fun getNotification(): NotificationUM? { - return when (params.source) { - Source.SendViaSwap -> ChooseManagedTokensNotificationUM.SendViaSwap( - onCloseClick = ::removeNotification, - ) + return if (params.source == Source.SendViaSwap && params.showSendViaSwapNotification) { + ChooseManagedTokensNotificationUM.SendViaSwap(onCloseClick = ::removeNotification) + } else { + null } } private fun removeNotification() { - uiState.update { - it.copy( - notificationUM = null, - ) + modelScope.launch { + setShouldShowNotificationUseCase(NotificationId.SendViaSwapTokenSelectorNotification.key, false) + uiState.update { + it.copy( + notificationUM = null, + ) + } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt index 2f52fcdd3e..3bdbfcaf8d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt @@ -2,9 +2,11 @@ package com.tangem.features.send.v2.entrypoint.model import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.notifications.NotificationId import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendEntryPointComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger @@ -18,6 +20,7 @@ import jakarta.inject.Inject import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +@Suppress("LongParameterList") @ModelScoped internal class SendEntryPointModel @Inject constructor( paramsContainer: ParamsContainer, @@ -26,6 +29,7 @@ internal class SendEntryPointModel @Inject constructor( private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener, private val sendAmountUpdateTrigger: SendAmountUpdateTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, + private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, ) : Model(), SendComponent.ModelCallback, SendWithSwapComponent.ModelCallback { private val params: SendEntryPointComponent.Params = paramsContainer.require() @@ -36,15 +40,21 @@ internal class SendEntryPointModel @Inject constructor( private var swapChooseTokenListenerJobHolder = JobHolder() override fun onConvertToAnotherToken(lastAmount: String) { - appRouter.push( - AppRoute.ChooseManagedTokens( - userWalletId = params.userWalletId, - initialCurrency = params.cryptoCurrency, - selectedCurrency = null, - source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, - ), - ) - observeChooseSelectToken(lastAmount) + modelScope.launch { + val showSendViaSwapNotification = shouldShowNotificationUseCase( + NotificationId.SendViaSwapTokenSelectorNotification.key, + ) + appRouter.push( + AppRoute.ChooseManagedTokens( + userWalletId = params.userWalletId, + initialCurrency = params.cryptoCurrency, + selectedCurrency = null, + source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, + showSendViaSwapNotification = showSendViaSwapNotification, + ), + ) + observeChooseSelectToken(lastAmount) + } } override fun onCloseSwap(lastAmount: String) { diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index 5cb929fe7e..ab1ccbbe75 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.txhistory.models) implementation(projects.domain.txhistory) + implementation(projects.domain.notifications) implementation(projects.domain.feedback.models) implementation(projects.domain.feedback) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 149ccd0de1..fa265d5e1c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -9,6 +9,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.common.ui.notifications.NotificationId import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -22,6 +23,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.swap.models.SwapQuoteModel @@ -81,6 +83,7 @@ internal class SwapAmountModel @Inject constructor( private val swapAmountUpdateListener: SwapAmountUpdateListener, private val swapAmountReduceListener: SwapAmountReduceListener, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, + private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, ) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback { private val params: SwapAmountComponentParams = paramsContainer.require() @@ -221,6 +224,9 @@ internal class SwapAmountModel @Inject constructor( override fun onSelectTokenClick() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return modelScope.launch { + val showSendViaSwapNotification = shouldShowNotificationUseCase( + NotificationId.SendViaSwapTokenSelectorNotification.key, + ) val isEditMode = amountParams.currentRoute.firstOrNull()?.isEditMode == true val selectedCurrency = (uiState.value as? SwapAmountUM.Content)?.secondaryCryptoCurrencyStatus?.currency appRouter.push( @@ -229,6 +235,7 @@ internal class SwapAmountModel @Inject constructor( initialCurrency = primaryCryptoCurrency, selectedCurrency = selectedCurrency.takeIf { isEditMode }, source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, + showSendViaSwapNotification = showSendViaSwapNotification, ), ) } @@ -259,6 +266,7 @@ internal class SwapAmountModel @Inject constructor( private fun confirmSendWithSwapClose() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data + val callback = (params as? SwapAmountComponentParams.AmountParams)?.callback ?: return val primaryCryptoCurrencyStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus if (primaryCryptoCurrencyStatus != null) {