From 428f1076f091289f50cd652942ece92fd3be6490 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jun 2026 08:40:33 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 6 +- .../models/PushNotificationPreferenceState.kt | 10 - .../models/PushNotificationPreferencesBody.kt | 10 +- .../PushNotificationPreferencesResponse.kt | 6 +- ...etPushNotificationPreferencesRepository.kt | 121 +++++---- .../PushNotificationPreferencesConverter.kt | 10 +- .../di/PushNotificationPreferencesModule.kt | 3 - ...shNotificationPreferencesRepositoryTest.kt | 253 ++++++++++++------ .../models/PushNotificationPreference.kt | 1 - .../model/PushNotificationSettingsModel.kt | 1 - .../PushNotificationSettingsModelTest.kt | 12 +- 11 files changed, 248 insertions(+), 185 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferenceState.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 7530892018..244998c682 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -52,16 +52,16 @@ interface TangemTechApi { @Body userTokens: UserTokensResponse, ): ApiResponse - @GET("/v1/wallets/{wallet_id}/notification-preferences") + @GET("/api/v1/notification-preferences/{wallet_id}") suspend fun getPushNotificationPreferences( @Path("wallet_id") walletId: String, ): ApiResponse - @PUT("/v1/wallets/{wallet_id}/notification-preferences") + @PUT("/api/v1/notification-preferences/{wallet_id}") suspend fun updatePushNotificationPreferences( @Path("wallet_id") walletId: String, @Body body: PushNotificationPreferencesBody, - ): ApiResponse + ): ApiResponse // region Referral /** Returns referral status by [walletId] */ diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferenceState.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferenceState.kt deleted file mode 100644 index 9d95473183..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferenceState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.datasource.api.tangemTech.models - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class PushNotificationPreferenceState( - @Json(name = "isEnabled") val isEnabled: Boolean, - @Json(name = "isVisible") val isVisible: Boolean, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferencesBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferencesBody.kt index 01a4c76dcd..9b7ce52ebe 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferencesBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferencesBody.kt @@ -5,10 +5,10 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class PushNotificationPreferencesBody( - @Json(name = "transactionAlerts") - val areTransactionAlertsEnabled: Boolean, - @Json(name = "offersUpdates") - val areOffersUpdatesEnabled: Boolean, - @Json(name = "priceAlerts") + @Json(name = "transactionEventsEnabled") + val areTransactionEventsEnabled: Boolean, + @Json(name = "offerUpdatesEnabled") + val areOfferUpdatesEnabled: Boolean, + @Json(name = "priceAlertsEnabled") val arePriceAlertsEnabled: Boolean, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferencesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferencesResponse.kt index 25606b8a6e..e67a3dbcb5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferencesResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferencesResponse.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class PushNotificationPreferencesResponse( - @Json(name = "transactionAlerts") val transactionAlerts: PushNotificationPreferenceState, - @Json(name = "offersUpdates") val offersUpdates: PushNotificationPreferenceState, - @Json(name = "priceAlerts") val priceAlerts: PushNotificationPreferenceState, + @Json(name = "transactionEventsEnabled") val areTransactionEventsEnabled: Boolean, + @Json(name = "offerUpdatesEnabled") val areOfferUpdatesEnabled: Boolean, + @Json(name = "priceAlertsEnabled") val arePriceAlertsEnabled: Boolean, ) \ No newline at end of file diff --git a/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepository.kt b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepository.kt index c5b207e92b..7f8be9280c 100644 --- a/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepository.kt +++ b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepository.kt @@ -1,54 +1,49 @@ package com.tangem.data.pushnotificationpreferences import arrow.core.Either +import com.tangem.data.pushnotificationpreferences.converters.PushNotificationPreferencesConverter +import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.PushNotificationPreferencesBody import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectMapSync import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pushnotificationpreferences.models.PushNotificationCategory -import com.tangem.domain.pushnotificationpreferences.models.PushNotificationPreference import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext /** - * In-memory cache implementation of [WalletPushNotificationPreferencesRepository]. - * - * Mock-mode (current): defaults are computed locally and writes are kept in-memory only. - * Real-mode (when Variant C BE is ready): replace TODO blocks with [TangemTechApi] calls. - * - * Defaults for existing users (until BE migration runs): TX read from - * [PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] (default true), Offers&Updates = true, Price Alerts = false, - * isVisible = true for all three. + * Preferences are cached in-memory (non-persistent); writes are full-replace PUTs and the server echo + * is cached as the source of truth. */ internal class DefaultWalletPushNotificationPreferencesRepository( - private val appPreferencesStore: AppPreferencesStore, - @Suppress("unused") private val tangemTechApi: TangemTechApi, + private val tangemTechApi: TangemTechApi, private val cache: RuntimeSharedStore>, private val dispatchers: CoroutineDispatcherProvider, ) : WalletPushNotificationPreferencesRepository { + private val walletMutexes = ConcurrentHashMap() + override suspend fun preload(userWalletId: UserWalletId) { - if (cache.getSyncOrNull()?.containsKey(userWalletId.stringValue) == true) return - val preferences = withContext(dispatchers.io) { - // TODO: uncomment when api is ready - // val response = tangemTechApi.getPushNotificationPreferences(userWalletId.stringValue).getOrThrow() - // PushNotificationPreferencesConverter.convert(response) - loadDefaults(userWalletId) - } - cache.update(default = emptyMap()) { current -> - if (current.containsKey(userWalletId.stringValue)) { - current - } else { - current + (userWalletId.stringValue to preferences) + if (isCached(userWalletId)) return + mutexFor(userWalletId).withLock { + if (isCached(userWalletId)) return + val preferences = fetch(userWalletId) + cache.update(default = emptyMap()) { current -> + if (current.containsKey(userWalletId.stringValue)) { + current + } else { + current + (userWalletId.stringValue to preferences) + } } } } @@ -64,9 +59,11 @@ internal class DefaultWalletPushNotificationPreferencesRepository( category: PushNotificationCategory, isEnabled: Boolean, ): Either = Either.catch { - val current = cache.getSyncOrNull()?.get(userWalletId.stringValue) ?: loadDefaults(userWalletId) - val updated = current.withCategory(category, isEnabled) - putAndCommit(userWalletId, updated) + mutexFor(userWalletId).withLock { + val current = currentOrFetch(userWalletId) + val updated = current.withCategory(category, isEnabled) + putAndCommit(userWalletId, updated) + } } override suspend fun setAllPreferences( @@ -75,39 +72,45 @@ internal class DefaultWalletPushNotificationPreferencesRepository( offersUpdates: Boolean, priceAlerts: Boolean, ): Either = Either.catch { - val current = cache.getSyncOrNull()?.get(userWalletId.stringValue) ?: loadDefaults(userWalletId) - val updated = current.copy( - transactionAlerts = current.transactionAlerts.copy(isEnabled = transactionAlerts), - offersUpdates = current.offersUpdates.copy(isEnabled = offersUpdates), - priceAlerts = current.priceAlerts.copy(isEnabled = priceAlerts), - ) - putAndCommit(userWalletId, updated) + mutexFor(userWalletId).withLock { + val current = currentOrFetch(userWalletId) + val updated = current.copy( + transactionAlerts = current.transactionAlerts.copy(isEnabled = transactionAlerts), + offersUpdates = current.offersUpdates.copy(isEnabled = offersUpdates), + priceAlerts = current.priceAlerts.copy(isEnabled = priceAlerts), + ) + putAndCommit(userWalletId, updated) + } } + // Cache, or a freshly fetched server snapshot, so a full-replace PUT never carries fabricated defaults. + private suspend fun currentOrFetch(userWalletId: UserWalletId): WalletPushNotificationPreferences = + cache.getSyncOrNull()?.get(userWalletId.stringValue) ?: fetch(userWalletId) + + private suspend fun fetch(userWalletId: UserWalletId): WalletPushNotificationPreferences = + withContext(dispatchers.io) { + val response = tangemTechApi.getPushNotificationPreferences(userWalletId.stringValue).getOrThrow() + PushNotificationPreferencesConverter.convert(response) + } + + private suspend fun isCached(userWalletId: UserWalletId): Boolean = + cache.getSyncOrNull()?.containsKey(userWalletId.stringValue) == true + + private fun mutexFor(userWalletId: UserWalletId): Mutex = + walletMutexes.computeIfAbsent(userWalletId.stringValue) { Mutex() } + private suspend fun putAndCommit(userWalletId: UserWalletId, updated: WalletPushNotificationPreferences) { - withContext(dispatchers.io) { - // TODO: uncomment when api is ready - // tangemTechApi.updatePushNotificationPreferences( - // walletId = userWalletId.stringValue, - // body = PushNotificationPreferencesBody( - // areTransactionAlertsEnabled = updated.transactionAlerts.isEnabled, - // areOffersUpdatesEnabled = updated.offersUpdates.isEnabled, - // arePriceAlertsEnabled = updated.priceAlerts.isEnabled, - // ), - // ).getOrThrow() + val applied = withContext(dispatchers.io) { + val response = tangemTechApi.updatePushNotificationPreferences( + walletId = userWalletId.stringValue, + body = PushNotificationPreferencesBody( + areTransactionEventsEnabled = updated.transactionAlerts.isEnabled, + areOfferUpdatesEnabled = updated.offersUpdates.isEnabled, + arePriceAlertsEnabled = updated.priceAlerts.isEnabled, + ), + ).getOrThrow() + PushNotificationPreferencesConverter.convert(response) } - cache.update(default = emptyMap()) { it + (userWalletId.stringValue to updated) } - } - - // TODO remove when api is ready, use api methods to load real settings - private suspend fun loadDefaults(userWalletId: UserWalletId): WalletPushNotificationPreferences { - val areTransactionAlertsEnabled = appPreferencesStore - .getObjectMapSync(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY)[userWalletId.stringValue] != - false - return WalletPushNotificationPreferences( - transactionAlerts = PushNotificationPreference(isEnabled = areTransactionAlertsEnabled, isVisible = true), - offersUpdates = PushNotificationPreference(isEnabled = true, isVisible = true), - priceAlerts = PushNotificationPreference(isEnabled = false, isVisible = true), - ) + cache.update(default = emptyMap()) { it + (userWalletId.stringValue to applied) } } } \ No newline at end of file diff --git a/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/converters/PushNotificationPreferencesConverter.kt b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/converters/PushNotificationPreferencesConverter.kt index 34a5e99cd3..5c2bfea3db 100644 --- a/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/converters/PushNotificationPreferencesConverter.kt +++ b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/converters/PushNotificationPreferencesConverter.kt @@ -1,6 +1,5 @@ package com.tangem.data.pushnotificationpreferences.converters -import com.tangem.datasource.api.tangemTech.models.PushNotificationPreferenceState import com.tangem.datasource.api.tangemTech.models.PushNotificationPreferencesResponse import com.tangem.domain.pushnotificationpreferences.models.PushNotificationPreference import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences @@ -11,11 +10,8 @@ internal object PushNotificationPreferencesConverter : override fun convert(value: PushNotificationPreferencesResponse): WalletPushNotificationPreferences = WalletPushNotificationPreferences( - transactionAlerts = value.transactionAlerts.toDomain(), - offersUpdates = value.offersUpdates.toDomain(), - priceAlerts = value.priceAlerts.toDomain(), + transactionAlerts = PushNotificationPreference(isEnabled = value.areTransactionEventsEnabled), + offersUpdates = PushNotificationPreference(isEnabled = value.areOfferUpdatesEnabled), + priceAlerts = PushNotificationPreference(isEnabled = value.arePriceAlertsEnabled), ) - - private fun PushNotificationPreferenceState.toDomain(): PushNotificationPreference = - PushNotificationPreference(isEnabled = isEnabled, isVisible = isVisible) } \ No newline at end of file diff --git a/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/di/PushNotificationPreferencesModule.kt b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/di/PushNotificationPreferencesModule.kt index b82e635254..bcdafa677d 100644 --- a/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/di/PushNotificationPreferencesModule.kt +++ b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/di/PushNotificationPreferencesModule.kt @@ -3,7 +3,6 @@ package com.tangem.data.pushnotificationpreferences.di import com.tangem.data.pushnotificationpreferences.DefaultWalletPushNotificationPreferencesRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -19,11 +18,9 @@ internal object PushNotificationPreferencesModule { @Singleton @Provides fun providesWalletPushNotificationPreferencesRepository( - appPreferencesStore: AppPreferencesStore, tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, ): WalletPushNotificationPreferencesRepository = DefaultWalletPushNotificationPreferencesRepository( - appPreferencesStore = appPreferencesStore, tangemTechApi = tangemTechApi, cache = RuntimeSharedStore(), dispatchers = dispatchers, diff --git a/data/push-notification-preferences/src/test/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepositoryTest.kt b/data/push-notification-preferences/src/test/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepositoryTest.kt index c9334a143a..20ec904473 100644 --- a/data/push-notification-preferences/src/test/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepositoryTest.kt +++ b/data/push-notification-preferences/src/test/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepositoryTest.kt @@ -1,141 +1,220 @@ package com.tangem.data.pushnotificationpreferences -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.emptyPreferences import app.cash.turbine.test import arrow.core.Either import com.google.common.truth.Truth.assertThat -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.PushNotificationPreferencesBody +import com.tangem.datasource.api.tangemTech.models.PushNotificationPreferencesResponse import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pushnotificationpreferences.models.PushNotificationCategory import com.tangem.domain.pushnotificationpreferences.models.PushNotificationPreference import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test class DefaultWalletPushNotificationPreferencesRepositoryTest { private val tangemTechApi: TangemTechApi = mockk() - private val preferencesDataStore: DataStore = mockk() - private val appPreferencesStore = AppPreferencesStore( - moshi = Moshi.Builder().build(), - dispatchers = TestingCoroutineDispatcherProvider(), - preferencesDataStore = preferencesDataStore, - ) private val userWalletId = UserWalletId(stringValue = "0011223344556677") private val otherWalletId = UserWalletId(stringValue = "ffeeddccbbaa9988") private val repository = DefaultWalletPushNotificationPreferencesRepository( - appPreferencesStore = appPreferencesStore, tangemTechApi = tangemTechApi, cache = RuntimeSharedStore(), dispatchers = TestingCoroutineDispatcherProvider(), ) @Test - fun `GIVEN no prior state WHEN preload THEN cache contains defaults`() = runTest { - coEvery { preferencesDataStore.data } returns flowOf(emptyPreferences()) + fun `GIVEN server returns prefs WHEN preload THEN cache holds converted server state`() = runTest { + // Arrange + stubGet(userWalletId, transaction = true, offers = true, price = false) + // Act repository.preload(userWalletId) + // Assert repository.observePreferences(userWalletId).test { - assertThat(awaitItem()).isEqualTo(defaults(transactionAlertsEnabled = true)) + assertThat(awaitItem()).isEqualTo(prefs(transaction = true, offers = true, price = false)) } + coVerify(exactly = 1) { tangemTechApi.getPushNotificationPreferences(userWalletId.stringValue) } } @Test - fun `GIVEN preload already done WHEN preload called again THEN no-op`() = runTest { - coEvery { preferencesDataStore.data } returns flowOf(emptyPreferences()) + fun `GIVEN already preloaded WHEN preload called again THEN no second GET`() = runTest { + // Arrange + stubGet(userWalletId, transaction = true, offers = true, price = false) + // Act + repository.preload(userWalletId) + repository.preload(userWalletId) + + // Assert + coVerify(exactly = 1) { tangemTechApi.getPushNotificationPreferences(userWalletId.stringValue) } + } + + @Test + fun `GIVEN cache miss WHEN updatePreference THEN fetches baseline AND sends full-replace PUT AND caches echo`() = + runTest { + // Arrange + stubGet(userWalletId, transaction = true, offers = true, price = false) + stubPut(userWalletId, transaction = true, offers = true, price = true) + + // Act + val result = repository.updatePreference( + userWalletId = userWalletId, + category = PushNotificationCategory.PriceAlerts, + isEnabled = true, + ) + + // Assert + assertThat(result).isInstanceOf(Either.Right::class.java) + // The full-replace body changes only the tapped category on top of the server baseline. + coVerify(exactly = 1) { + tangemTechApi.updatePushNotificationPreferences( + userWalletId.stringValue, + PushNotificationPreferencesBody( + areTransactionEventsEnabled = true, + areOfferUpdatesEnabled = true, + arePriceAlertsEnabled = true, + ), + ) + } + repository.observePreferences(userWalletId).test { + assertThat(awaitItem()).isEqualTo(prefs(transaction = true, offers = true, price = true)) + } + } + + @Test + fun `GIVEN preloaded state WHEN updatePreference THEN only the tapped category changes in the PUT body`() = runTest { + // Arrange + stubGet(userWalletId, transaction = true, offers = true, price = false) + stubPut(userWalletId, transaction = true, offers = false, price = false) + + // Act repository.preload(userWalletId) repository.updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, isEnabled = false) + + // Assert + coVerify(exactly = 1) { + tangemTechApi.updatePushNotificationPreferences( + userWalletId.stringValue, + PushNotificationPreferencesBody( + areTransactionEventsEnabled = true, + areOfferUpdatesEnabled = false, + arePriceAlertsEnabled = false, + ), + ) + } + } + + @Test + fun `GIVEN write fails WHEN updatePreference THEN returns Left`() = runTest { + // Arrange + stubGet(userWalletId, transaction = true, offers = true, price = false) + repository.preload(userWalletId) + coEvery { tangemTechApi.updatePushNotificationPreferences(any(), any()) } throws IllegalStateException("boom") + + // Act + val result = repository.updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, isEnabled = false) + + // Assert + assertThat(result).isInstanceOf(Either.Left::class.java) + } + + @Test + fun `GIVEN different wallets WHEN observed THEN each keeps its own server state`() = runTest { + // Arrange + stubGet(userWalletId, transaction = false, offers = false, price = false) + stubGet(otherWalletId, transaction = true, offers = true, price = true) + + // Assert + repository.observePreferences(userWalletId).test { + assertThat(awaitItem()).isEqualTo(prefs(transaction = false, offers = false, price = false)) + } + repository.observePreferences(otherWalletId).test { + assertThat(awaitItem()).isEqualTo(prefs(transaction = true, offers = true, price = true)) + } + } + + @Test + fun `GIVEN concurrent collectors WHEN preload races THEN a single GET is issued`() = runTest { + // Arrange + val gate = CompletableDeferred() + coEvery { tangemTechApi.getPushNotificationPreferences(userWalletId.stringValue) } coAnswers { + gate.await() + ApiResponse.Success(PushNotificationPreferencesResponse(true, true, false)) + } + + // Act + launch { repository.preload(userWalletId) } + runCurrent() + launch { repository.preload(userWalletId) } + runCurrent() + gate.complete(Unit) + advanceUntilIdle() + + // Assert + coVerify(exactly = 1) { tangemTechApi.getPushNotificationPreferences(userWalletId.stringValue) } + } + + @Test + fun `GIVEN concurrent writes WHEN updatePreference races THEN serialized so no update is lost`() = runTest { + // Arrange + stubGet(userWalletId, transaction = true, offers = true, price = false) + val gate = CompletableDeferred() + coEvery { tangemTechApi.updatePushNotificationPreferences(eq(userWalletId.stringValue), any()) } coAnswers { + val body = arg(1) + gate.await() + ApiResponse.Success( + PushNotificationPreferencesResponse( + body.areTransactionEventsEnabled, + body.areOfferUpdatesEnabled, + body.arePriceAlertsEnabled, + ), + ) + } repository.preload(userWalletId) + // Act + launch { repository.updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, isEnabled = false) } + runCurrent() + launch { repository.updatePreference(userWalletId, PushNotificationCategory.PriceAlerts, isEnabled = true) } + runCurrent() + gate.complete(Unit) + advanceUntilIdle() + + // Assert + coVerify(exactly = 2) { tangemTechApi.updatePushNotificationPreferences(eq(userWalletId.stringValue), any()) } repository.observePreferences(userWalletId).test { - val item = awaitItem() - assertThat(item.offersUpdates.isEnabled).isFalse() + assertThat(awaitItem()).isEqualTo(prefs(transaction = true, offers = false, price = true)) } } - @Test - fun `GIVEN cache miss WHEN updatePreference THEN loads defaults and applies update`() = runTest { - coEvery { preferencesDataStore.data } returns flowOf(emptyPreferences()) - - val result = repository.updatePreference( - userWalletId = userWalletId, - category = PushNotificationCategory.PriceAlerts, - isEnabled = true, - ) - - assertThat(result).isInstanceOf(Either.Right::class.java) - repository.observePreferences(userWalletId).test { - val item = awaitItem() - assertThat(item.priceAlerts.isEnabled).isTrue() - assertThat(item.offersUpdates.isEnabled).isTrue() - assertThat(item.transactionAlerts.isEnabled).isTrue() - } + private fun stubGet(id: UserWalletId, transaction: Boolean, offers: Boolean, price: Boolean) { + coEvery { tangemTechApi.getPushNotificationPreferences(id.stringValue) } returns + ApiResponse.Success(PushNotificationPreferencesResponse(transaction, offers, price)) } - @Test - fun `GIVEN preloaded state WHEN updatePreference for each category THEN updates only that category`() = runTest { - coEvery { preferencesDataStore.data } returns flowOf(emptyPreferences()) - - repository.preload(userWalletId) - repository.updatePreference(userWalletId, PushNotificationCategory.TransactionAlerts, isEnabled = false) - repository.updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, isEnabled = false) - repository.updatePreference(userWalletId, PushNotificationCategory.PriceAlerts, isEnabled = true) - - repository.observePreferences(userWalletId).test { - val item = awaitItem() - assertThat(item.transactionAlerts.isEnabled).isFalse() - assertThat(item.offersUpdates.isEnabled).isFalse() - assertThat(item.priceAlerts.isEnabled).isTrue() - } + private fun stubPut(id: UserWalletId, transaction: Boolean, offers: Boolean, price: Boolean) { + coEvery { tangemTechApi.updatePushNotificationPreferences(eq(id.stringValue), any()) } returns + ApiResponse.Success(PushNotificationPreferencesResponse(transaction, offers, price)) } - @Test - fun `GIVEN no subscription yet WHEN observePreferences subscribed THEN triggers preload and emits defaults`() = - runTest { - coEvery { preferencesDataStore.data } returns flowOf(emptyPreferences()) - - repository.observePreferences(userWalletId).test { - val item = awaitItem() - assertThat(item).isEqualTo(defaults(transactionAlertsEnabled = true)) - } - } - - @Test - fun `GIVEN updates for different wallets WHEN observed independently THEN each wallet has its own state`() = - runTest { - coEvery { preferencesDataStore.data } returns flowOf(emptyPreferences()) - - repository.updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, isEnabled = false) - repository.updatePreference(otherWalletId, PushNotificationCategory.PriceAlerts, isEnabled = true) - - repository.observePreferences(userWalletId).test { - val item = awaitItem() - assertThat(item.offersUpdates.isEnabled).isFalse() - assertThat(item.priceAlerts.isEnabled).isFalse() - } - repository.observePreferences(otherWalletId).test { - val item = awaitItem() - assertThat(item.offersUpdates.isEnabled).isTrue() - assertThat(item.priceAlerts.isEnabled).isTrue() - } - } - - private fun defaults(transactionAlertsEnabled: Boolean) = WalletPushNotificationPreferences( - transactionAlerts = PushNotificationPreference(isEnabled = transactionAlertsEnabled, isVisible = true), - offersUpdates = PushNotificationPreference(isEnabled = true, isVisible = true), - priceAlerts = PushNotificationPreference(isEnabled = false, isVisible = true), + private fun prefs(transaction: Boolean, offers: Boolean, price: Boolean) = WalletPushNotificationPreferences( + transactionAlerts = PushNotificationPreference(isEnabled = transaction), + offersUpdates = PushNotificationPreference(isEnabled = offers), + priceAlerts = PushNotificationPreference(isEnabled = price), ) } \ No newline at end of file diff --git a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/PushNotificationPreference.kt b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/PushNotificationPreference.kt index 248d916d41..d5642bf159 100644 --- a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/PushNotificationPreference.kt +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/PushNotificationPreference.kt @@ -2,5 +2,4 @@ package com.tangem.domain.pushnotificationpreferences.models data class PushNotificationPreference( val isEnabled: Boolean, - val isVisible: Boolean, ) \ No newline at end of file diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt index cbc841a27c..5e4e88cda0 100644 --- a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt +++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt @@ -163,7 +163,6 @@ internal class PushNotificationSettingsModel @Inject constructor( return TOGGLE_ORDER .asSequence() .map { id -> id.spec(prefs) } - .filter { it.preference.isVisible } .map { spec -> ToggleUM( id = spec.id, diff --git a/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt b/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt index 500cfcf9b6..a747c404c4 100644 --- a/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt +++ b/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt @@ -286,14 +286,14 @@ class PushNotificationSettingsModelTest { } private fun allFalse() = WalletPushNotificationPreferences( - transactionAlerts = PushNotificationPreference(isEnabled = false, isVisible = true), - offersUpdates = PushNotificationPreference(isEnabled = false, isVisible = true), - priceAlerts = PushNotificationPreference(isEnabled = false, isVisible = true), + transactionAlerts = PushNotificationPreference(isEnabled = false), + offersUpdates = PushNotificationPreference(isEnabled = false), + priceAlerts = PushNotificationPreference(isEnabled = false), ) private fun anyOn() = WalletPushNotificationPreferences( - transactionAlerts = PushNotificationPreference(isEnabled = true, isVisible = true), - offersUpdates = PushNotificationPreference(isEnabled = false, isVisible = true), - priceAlerts = PushNotificationPreference(isEnabled = false, isVisible = true), + transactionAlerts = PushNotificationPreference(isEnabled = true), + offersUpdates = PushNotificationPreference(isEnabled = false), + priceAlerts = PushNotificationPreference(isEnabled = false), ) } \ No newline at end of file