From 6c8b59c569d032a30612c346d0f8e5ddcedd424d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Apr 2025 16:08:31 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../FirebasePushNotificationsTokenProvider.kt | 2 +- .../local/preferences/PreferencesKeys.kt | 2 + .../DefaultNotificationsRepository.kt | 12 -- .../DefaultNotificationsRepositoryTest.kt | 40 ---- data/wallets/build.gradle.kts | 8 + .../data/wallets/DefaultWalletsRepository.kt | 47 ++++- .../wallets/DefaultWalletsRepositoryTest.kt | 176 ++++++++++++++++++ domain/notifications/build.gradle.kts | 20 +- .../repository/NotificationsRepository.kt | 6 - .../wallets/repository/WalletsRepository.kt | 6 + .../GetIsNotificationsEnabledUseCase.kt | 17 ++ .../usecase/SetNotificationsEnabledUseCase.kt | 18 ++ 12 files changed, 288 insertions(+), 66 deletions(-) create mode 100644 data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsNotificationsEnabledUseCase.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt index eaf9730f21..02a9b8be72 100644 --- a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt @@ -4,7 +4,7 @@ import com.google.firebase.messaging.FirebaseMessaging import com.tangem.utils.notifications.PushNotificationsTokenProvider import kotlinx.coroutines.tasks.await -class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider { +internal class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider { override suspend fun getToken(): String { return FirebaseMessaging.getInstance().token.await() } 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 5177c8d4aa..83c7a513f0 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 @@ -125,6 +125,8 @@ object PreferencesKeys { // region Notifications val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") } + + val NOTIFICATIONS_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "notificationsEnabledStates") } // endregion fun getShouldShowStoriesKey(storyId: String) = booleanPreferencesKey("shouldShowStories_$storyId") 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 8648ace195..57d8a6344a 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 @@ -44,14 +44,6 @@ internal class DefaultNotificationsRepository @Inject constructor( return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) } - override suspend fun setNotificationsEnabledForWallet(walletId: String, enabled: Boolean) = - withContext(dispatchers.io) { - tangemTechApi.setNotificationsEnabled( - walletId = walletId, - body = WalletBody(notifyStatus = enabled), - ).getOrThrow() - } - override suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) = withContext(dispatchers.io) { tangemTechApi.associateApplicationIdWithWallets( @@ -62,10 +54,6 @@ internal class DefaultNotificationsRepository @Inject constructor( ).getOrThrow() } - override suspend fun isNotificationsEnabledForWallet(walletId: String): Boolean = withContext(dispatchers.io) { - tangemTechApi.getWalletById(walletId).getOrThrow().notifyStatus - } - override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { tangemTechApi.updateWallet( walletId, 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 65c9a1220d..305732cde6 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 @@ -103,26 +103,6 @@ class DefaultNotificationsRepositoryTest { assertThat(result).isEqualTo(expectedAppId) } - @Test - fun `GIVEN wallet id and enabled status WHEN setNotificationsEnabledForWallet THEN updates notification status`() = - runTest { - // GIVEN - val walletId = "test-wallet-id" - val enabled = true - coEvery { - tangemTechApi.setNotificationsEnabled( - walletId, - WalletBody(notifyStatus = enabled), - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.setNotificationsEnabledForWallet(walletId, enabled) - - // THEN - coVerify { tangemTechApi.setNotificationsEnabled(walletId, WalletBody(notifyStatus = enabled)) } - } - @Test fun `GIVEN application id and wallet list WHEN associateApplicationIdWithWallets THEN associates them`() = runTest { // GIVEN @@ -142,26 +122,6 @@ class DefaultNotificationsRepositoryTest { coVerify { tangemTechApi.associateApplicationIdWithWallets(appId, wallets.map { WalletIdBody(it) }) } } - @Test - fun `GIVEN wallet id WHEN isNotificationsEnabledForWallet THEN returns notification status`() = runTest { - // GIVEN - val walletId = "test-wallet-id" - val expectedStatus = true - coEvery { tangemTechApi.getWalletById(walletId) } returns ApiResponse.Success( - WalletResponse( - notifyStatus = expectedStatus, - name = "Test Wallet", - id = walletId, - ), - ) - - // WHEN - val result = repository.isNotificationsEnabledForWallet(walletId) - - // THEN - assertThat(result).isEqualTo(expectedStatus) - } - @Test fun `GIVEN wallet id and name WHEN setWalletName THEN updates wallet name`() = runTest { // GIVEN diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index 46c3ad34e3..e84f33b3bc 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -34,4 +34,12 @@ dependencies { /** Other deps */ implementation(deps.androidx.datastore) implementation(deps.arrow.core) + + /** tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) + testImplementation(deps.moshi) + testImplementation(deps.moshi.kotlin) } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 35bc64c215..39096c2551 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.MarkUserWalletWasCreatedBody import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status +import com.tangem.datasource.api.tangemTech.models.WalletBody import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -21,11 +22,9 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.WEEK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext typealias SeedPhraseNotificationsStatuses = Map @@ -231,4 +230,44 @@ internal class DefaultWalletsRepository( ) } } + + override suspend fun isNotificationsEnabled(userWalletId: UserWalletId, force: Boolean): Boolean = + withContext(dispatchers.io) { + if (force) { + return@withContext loadAndSaveNotificationsEnabled(userWalletId) + } + + val localValue = appPreferencesStore.getObjectMap(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY) + .map { it[userWalletId.stringValue] } + .firstOrNull() + + localValue ?: loadAndSaveNotificationsEnabled(userWalletId) + } + + override suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) { + withContext(dispatchers.io) { + tangemTechApi.setNotificationsEnabled( + walletId = userWalletId.stringValue, + body = WalletBody(notifyStatus = isEnabled), + ).getOrThrow() + setNotificationsEnabledLocally(userWalletId, isEnabled) + } + } + + private suspend fun loadAndSaveNotificationsEnabled(userWalletId: UserWalletId): Boolean { + val walletResponse = tangemTechApi.getWalletById(walletId = userWalletId.stringValue).getOrThrow() + val isEnabled = walletResponse.notifyStatus + setNotificationsEnabledLocally(userWalletId, isEnabled) + return isEnabled + } + + private suspend fun setNotificationsEnabledLocally(userWalletId: UserWalletId, isEnabled: Boolean) { + appPreferencesStore.editData { + it.setObjectMap( + key = PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY, + value = it.getObjectMap(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY) + .plus(userWalletId.stringValue to isEnabled), + ) + } + } } \ No newline at end of file diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt new file mode 100644 index 0000000000..28f07454c2 --- /dev/null +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -0,0 +1,176 @@ +package com.tangem.data.wallets + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import com.squareup.moshi.Moshi +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.WalletBody +import com.tangem.datasource.api.tangemTech.models.WalletResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import io.mockk.coEvery +import io.mockk.coVerify +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +class DefaultWalletsRepositoryTest { + private lateinit var repository: DefaultWalletsRepository + private val preferencesDataStore = mockk>(relaxed = true) + private val appPreferenceStore = AppPreferencesStore( + moshi = Moshi.Builder().build(), + dispatchers = TestingCoroutineDispatcherProvider(), + preferencesDataStore = preferencesDataStore, + ) + private lateinit var tangemTechApi: TangemTechApi + private lateinit var dispatchers: CoroutineDispatcherProvider + + private val testWalletId = UserWalletId("1234567890abcdef") + private val testWalletResponse = WalletResponse( + id = testWalletId.stringValue, + notifyStatus = true, + ) + + @Before + fun setup() { + tangemTechApi = mockk() + dispatchers = TestingCoroutineDispatcherProvider() + repository = DefaultWalletsRepository( + appPreferencesStore = appPreferenceStore, + tangemTechApi = tangemTechApi, + userWalletsStore = mockk(), + seedPhraseNotificationVisibilityStore = mockk(), + dispatchers = dispatchers, + ) + } + + @Test + fun `GIVEN force is true WHEN isNotificationsEnabled THEN should fetch from API and update local storage`() = + runTest { + // GIVEN + coEvery { tangemTechApi.getWalletById(testWalletId.stringValue) } returns ApiResponse.Success( + testWalletResponse, + ) + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // WHEN + val result = repository.isNotificationsEnabled(testWalletId, force = true) + + // THEN + assertThat(result).isTrue() + coVerify(exactly = 1) { + tangemTechApi.getWalletById(testWalletId.stringValue) + } + coVerify(exactly = 0) { + tangemTechApi.setNotificationsEnabled(any(), any()) + } + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN local storage has value WHEN isNotificationsEnabled THEN should return local value`() = runTest { + // GIVEN + val expectedPreferences = """{"${testWalletId.stringValue}":true}""" + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns expectedPreferences + coEvery { tangemTechApi.getWalletById(any()) } returns ApiResponse.Success(testWalletResponse) + + // WHEN + val result = repository.isNotificationsEnabled(testWalletId, force = false) + + // THEN + assertThat(result).isTrue() + coVerify(inverse = true) { + tangemTechApi.getWalletById(any()) + tangemTechApi.setNotificationsEnabled(any(), any()) + } + } + + @Test + fun `GIVEN local storage is empty WHEN isNotificationsEnabled THEN should fetch from API and update local storage`() = runTest { + // GIVEN + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" + coEvery { tangemTechApi.getWalletById(testWalletId.stringValue) } returns ApiResponse.Success( + testWalletResponse, + ) + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // WHEN + val result = repository.isNotificationsEnabled(testWalletId, force = false) + + // THEN + assertThat(result).isTrue() + coVerify(exactly = 1) { + tangemTechApi.getWalletById(testWalletId.stringValue) + } + coVerify(exactly = 0) { + tangemTechApi.setNotificationsEnabled(any(), any()) + } + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN enabled status WHEN setNotificationsEnabled THEN should update API and local storage`() = runTest { + // GIVEN + coEvery { + tangemTechApi.setNotificationsEnabled( + eq(testWalletId.stringValue), + eq(WalletBody(notifyStatus = true)), + ) + } returns ApiResponse.Success(Unit) + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // WHEN + repository.setNotificationsEnabled(testWalletId, isEnabled = true) + + // THEN + coVerify(exactly = 1) { + tangemTechApi.setNotificationsEnabled( + eq(testWalletId.stringValue), + eq(WalletBody(notifyStatus = true)), + ) + } + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN disabled status WHEN setNotificationsEnabled THEN should update API and local storage`() = runTest { + // GIVEN + coEvery { + tangemTechApi.setNotificationsEnabled( + eq(testWalletId.stringValue), + eq(WalletBody(notifyStatus = false)), + ) + } returns ApiResponse.Success(Unit) + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // WHEN + repository.setNotificationsEnabled(testWalletId, isEnabled = false) + + // THEN + coVerifyOrder { + tangemTechApi.setNotificationsEnabled( + eq(testWalletId.stringValue), + eq(WalletBody(notifyStatus = false)), + ) + preferencesDataStore.updateData(any()) + } + coVerify(exactly = 1) { + tangemTechApi.setNotificationsEnabled( + eq(testWalletId.stringValue), + eq(WalletBody(notifyStatus = false)), + ) + } + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } +} \ No newline at end of file diff --git a/domain/notifications/build.gradle.kts b/domain/notifications/build.gradle.kts index eba62986ce..3c44620a5f 100644 --- a/domain/notifications/build.gradle.kts +++ b/domain/notifications/build.gradle.kts @@ -1,17 +1,31 @@ plugins { - alias(deps.plugins.kotlin.jvm) - alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } +android { + namespace = "com.tangem.domain.notifications" +} + dependencies { implementation(projects.core.utils) implementation(projects.domain.core) + implementation(projects.domain.wallets.models) implementation(projects.domain.notifications.models) + implementation(projects.domain.wallets) - /* Tests */ + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // end + + // region Tests testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) testImplementation(deps.test.truth) testImplementation(deps.test.mockk) + // end } \ 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 5fecc4b7e4..18575a904c 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 @@ -11,15 +11,9 @@ interface NotificationsRepository { suspend fun getApplicationId(): String? - @Throws - suspend fun setNotificationsEnabledForWallet(walletId: String, enabled: Boolean) - @Throws suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) - @Throws - suspend fun isNotificationsEnabledForWallet(walletId: String): Boolean - @Throws suspend fun setWalletName(walletId: String, walletName: String) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 6ec22a4eaf..541e601c54 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -37,4 +37,10 @@ interface WalletsRepository { suspend fun enableNFT(userWalletId: UserWalletId) suspend fun disableNFT(userWalletId: UserWalletId) + + @Throws + suspend fun isNotificationsEnabled(userWalletId: UserWalletId, force: Boolean): Boolean + + @Throws + suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsNotificationsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsNotificationsEnabledUseCase.kt new file mode 100644 index 0000000000..9db6370c5c --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsNotificationsEnabledUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository + +class GetIsNotificationsEnabledUseCase( + private val walletsRepository: WalletsRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, force: Boolean): Either = Either.catch { + walletsRepository.isNotificationsEnabled( + userWalletId = userWalletId, + force = force, + ) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt new file mode 100644 index 0000000000..574bca9f93 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository + +class SetNotificationsEnabledUseCase( + private val walletsRepository: WalletsRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, isEnabled: Boolean): Either = + Either.catch { + walletsRepository.setNotificationsEnabled( + userWalletId = userWalletId, + isEnabled = isEnabled, + ) + } +} \ No newline at end of file