diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7abfebef75..eef20245e6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -137,6 +137,7 @@ dependencies { implementation(projects.domain.appTheme.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.pushNotificationPreferences) implementation(projects.domain.transaction) implementation(projects.domain.transaction.models) implementation(projects.domain.analytics) @@ -197,6 +198,7 @@ dependencies { implementation(projects.data.appCurrency) implementation(projects.data.appTheme) implementation(projects.data.balanceHiding) + implementation(projects.data.pushNotificationPreferences) implementation(projects.data.card) implementation(projects.data.common) implementation(projects.data.settings) diff --git a/app/src/main/java/com/tangem/tap/di/domain/PushNotificationPreferencesDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/PushNotificationPreferencesDomainModule.kt new file mode 100644 index 0000000000..548ea7d9df --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/PushNotificationPreferencesDomainModule.kt @@ -0,0 +1,40 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase +import com.tangem.domain.pushnotificationpreferences.PreloadWalletPushNotificationPreferencesUseCase +import com.tangem.domain.pushnotificationpreferences.UpdateWalletPushNotificationPreferenceUseCase +import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object PushNotificationPreferencesDomainModule { + + @Provides + @Singleton + fun providesPreloadWalletPushNotificationPreferencesUseCase( + repository: WalletPushNotificationPreferencesRepository, + ): PreloadWalletPushNotificationPreferencesUseCase { + return PreloadWalletPushNotificationPreferencesUseCase(repository = repository) + } + + @Provides + @Singleton + fun providesObserveWalletPushNotificationPreferencesUseCase( + repository: WalletPushNotificationPreferencesRepository, + ): ObserveWalletPushNotificationPreferencesUseCase { + return ObserveWalletPushNotificationPreferencesUseCase(repository = repository) + } + + @Provides + @Singleton + fun providesUpdateWalletPushNotificationPreferenceUseCase( + repository: WalletPushNotificationPreferencesRepository, + ): UpdateWalletPushNotificationPreferenceUseCase { + return UpdateWalletPushNotificationPreferenceUseCase(repository = repository) + } +} \ No newline at end of file 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 9a515ad755..32d08ffdaf 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 @@ -50,6 +50,17 @@ interface TangemTechApi { @Body userTokens: UserTokensResponse, ): ApiResponse + @GET("/v1/wallets/{wallet_id}/notification-preferences") + suspend fun getPushNotificationPreferences( + @Path("wallet_id") walletId: String, + ): ApiResponse + + @PUT("/v1/wallets/{wallet_id}/notification-preferences") + suspend fun updatePushNotificationPreferences( + @Path("wallet_id") walletId: String, + @Body body: PushNotificationPreferencesBody, + ): ApiResponse + // region Referral /** Returns referral status by [walletId] */ @GET("v1/referral/{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 new file mode 100644 index 0000000000..9d95473183 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferenceState.kt @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000000..01a4c76dcd --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferencesBody.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +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") + 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 new file mode 100644 index 0000000000..25606b8a6e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PushNotificationPreferencesResponse.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +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, +) \ No newline at end of file diff --git a/data/push-notification-preferences/build.gradle.kts b/data/push-notification-preferences/build.gradle.kts new file mode 100644 index 0000000000..a13bc05f6f --- /dev/null +++ b/data/push-notification-preferences/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + 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.data.pushnotificationpreferences" +} + +dependencies { + /** Domain */ + implementation(projects.domain.pushNotificationPreferences) + implementation(projects.domain.models) + + /** Core */ + implementation(projects.core.datasource) + implementation(projects.core.utils) + + /** Other */ + implementation(deps.androidx.datastore) + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /** Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) + testImplementation(deps.test.turbine) + testImplementation(deps.moshi) + testImplementation(deps.moshi.kotlin) +} \ 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 new file mode 100644 index 0000000000..a7997de172 --- /dev/null +++ b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepository.kt @@ -0,0 +1,110 @@ +package com.tangem.data.pushnotificationpreferences + +import arrow.core.Either +import com.tangem.datasource.api.tangemTech.TangemTechApi +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 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.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. + */ +internal class DefaultWalletPushNotificationPreferencesRepository( + private val appPreferencesStore: AppPreferencesStore, + @Suppress("unused") private val tangemTechApi: TangemTechApi, + private val cache: RuntimeSharedStore>, + private val dispatchers: CoroutineDispatcherProvider, +) : WalletPushNotificationPreferencesRepository { + + 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) + } + } + } + + override fun observePreferences(userWalletId: UserWalletId): Flow = cache.get() + .onStart { preload(userWalletId) } + .map { it[userWalletId.stringValue] } + .filterNotNull() + .distinctUntilChanged() + + override suspend fun updatePreference( + userWalletId: UserWalletId, + category: PushNotificationCategory, + isEnabled: Boolean, + ): Either = Either.catch { + val current = cache.getSyncOrNull()?.get(userWalletId.stringValue) ?: loadDefaults(userWalletId) + val updated = applyCategory(current, category, isEnabled) + 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() + } + cache.update(default = emptyMap()) { it + (userWalletId.stringValue to updated) } + } + + private fun applyCategory( + current: WalletPushNotificationPreferences, + category: PushNotificationCategory, + isEnabled: Boolean, + ): WalletPushNotificationPreferences = when (category) { + PushNotificationCategory.TransactionAlerts -> current.copy( + transactionAlerts = current.transactionAlerts.copy(isEnabled = isEnabled), + ) + PushNotificationCategory.OffersUpdates -> current.copy( + offersUpdates = current.offersUpdates.copy(isEnabled = isEnabled), + ) + PushNotificationCategory.PriceAlerts -> current.copy( + priceAlerts = current.priceAlerts.copy(isEnabled = isEnabled), + ) + } + + // 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), + ) + } +} \ 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 new file mode 100644 index 0000000000..34a5e99cd3 --- /dev/null +++ b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/converters/PushNotificationPreferencesConverter.kt @@ -0,0 +1,21 @@ +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 +import com.tangem.utils.converter.Converter + +internal object PushNotificationPreferencesConverter : + Converter { + + override fun convert(value: PushNotificationPreferencesResponse): WalletPushNotificationPreferences = + WalletPushNotificationPreferences( + transactionAlerts = value.transactionAlerts.toDomain(), + offersUpdates = value.offersUpdates.toDomain(), + priceAlerts = value.priceAlerts.toDomain(), + ) + + 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 new file mode 100644 index 0000000000..b82e635254 --- /dev/null +++ b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/di/PushNotificationPreferencesModule.kt @@ -0,0 +1,31 @@ +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 +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object PushNotificationPreferencesModule { + + @Singleton + @Provides + fun providesWalletPushNotificationPreferencesRepository( + appPreferencesStore: AppPreferencesStore, + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + ): WalletPushNotificationPreferencesRepository = DefaultWalletPushNotificationPreferencesRepository( + appPreferencesStore = appPreferencesStore, + tangemTechApi = tangemTechApi, + cache = RuntimeSharedStore(), + dispatchers = dispatchers, + ) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..4622ed62c6 --- /dev/null +++ b/data/push-notification-preferences/src/test/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepositoryTest.kt @@ -0,0 +1,141 @@ +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.tangemTech.TangemTechApi +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.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.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()) + + repository.preload(userWalletId) + + repository.observePreferences(userWalletId).test { + assertThat(awaitItem()).isEqualTo(defaults(transactionAlertsEnabled = true)) + } + } + + @Test + fun `GIVEN preload already done WHEN preload called again THEN no-op`() = runTest { + coEvery { preferencesDataStore.data } returns flowOf(emptyPreferences()) + + repository.preload(userWalletId) + repository.updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, isEnabled = false) + repository.preload(userWalletId) + + repository.observePreferences(userWalletId).test { + val item = awaitItem() + assertThat(item.offersUpdates.isEnabled).isFalse() + } + } + + @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() + } + } + + @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() + } + } + + @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), + ) +} \ No newline at end of file diff --git a/domain/push-notification-preferences/build.gradle.kts b/domain/push-notification-preferences/build.gradle.kts new file mode 100644 index 0000000000..4838f201d0 --- /dev/null +++ b/domain/push-notification-preferences/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + 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.pushnotificationpreferences" +} + +dependencies { + /** Domain */ + implementation(projects.domain.models) + + /** Other */ + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/ObserveWalletPushNotificationPreferencesUseCase.kt b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/ObserveWalletPushNotificationPreferencesUseCase.kt new file mode 100644 index 0000000000..b741e26a9c --- /dev/null +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/ObserveWalletPushNotificationPreferencesUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.pushnotificationpreferences + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences +import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository +import kotlinx.coroutines.flow.Flow + +class ObserveWalletPushNotificationPreferencesUseCase( + private val repository: WalletPushNotificationPreferencesRepository, +) { + + operator fun invoke(userWalletId: UserWalletId): Flow = + repository.observePreferences(userWalletId) +} \ No newline at end of file diff --git a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/PreloadWalletPushNotificationPreferencesUseCase.kt b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/PreloadWalletPushNotificationPreferencesUseCase.kt new file mode 100644 index 0000000000..4eaf96b84b --- /dev/null +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/PreloadWalletPushNotificationPreferencesUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.pushnotificationpreferences + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository + +class PreloadWalletPushNotificationPreferencesUseCase( + private val repository: WalletPushNotificationPreferencesRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId) = repository.preload(userWalletId) +} \ No newline at end of file diff --git a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/UpdateWalletPushNotificationPreferenceUseCase.kt b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/UpdateWalletPushNotificationPreferenceUseCase.kt new file mode 100644 index 0000000000..1bfbc5f488 --- /dev/null +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/UpdateWalletPushNotificationPreferenceUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.pushnotificationpreferences + +import arrow.core.Either +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pushnotificationpreferences.models.PushNotificationCategory +import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository + +class UpdateWalletPushNotificationPreferenceUseCase( + private val repository: WalletPushNotificationPreferencesRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + category: PushNotificationCategory, + isEnabled: Boolean, + ): Either = repository.updatePreference( + userWalletId = userWalletId, + category = category, + isEnabled = isEnabled, + ) +} \ No newline at end of file diff --git a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/PushNotificationCategory.kt b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/PushNotificationCategory.kt new file mode 100644 index 0000000000..f172325cf2 --- /dev/null +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/PushNotificationCategory.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.pushnotificationpreferences.models + +enum class PushNotificationCategory { + TransactionAlerts, + OffersUpdates, + PriceAlerts, +} \ 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 new file mode 100644 index 0000000000..248d916d41 --- /dev/null +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/PushNotificationPreference.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.pushnotificationpreferences.models + +data class PushNotificationPreference( + val isEnabled: Boolean, + val isVisible: Boolean, +) \ No newline at end of file diff --git a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/WalletPushNotificationPreferences.kt b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/WalletPushNotificationPreferences.kt new file mode 100644 index 0000000000..bbb773d931 --- /dev/null +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/WalletPushNotificationPreferences.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.pushnotificationpreferences.models + +data class WalletPushNotificationPreferences( + val transactionAlerts: PushNotificationPreference, + val offersUpdates: PushNotificationPreference, + val priceAlerts: PushNotificationPreference, +) \ No newline at end of file diff --git a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/repository/WalletPushNotificationPreferencesRepository.kt b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/repository/WalletPushNotificationPreferencesRepository.kt new file mode 100644 index 0000000000..cde8d5a050 --- /dev/null +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/repository/WalletPushNotificationPreferencesRepository.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.pushnotificationpreferences.repository + +import arrow.core.Either +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pushnotificationpreferences.models.PushNotificationCategory +import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences +import kotlinx.coroutines.flow.Flow + +/** Per-wallet push notification preferences. In-memory cache, not persisted. */ +interface WalletPushNotificationPreferencesRepository { + + /** Warms up the cache for [userWalletId]. No-op if already cached. */ + suspend fun preload(userWalletId: UserWalletId) + + fun observePreferences(userWalletId: UserWalletId): Flow + + /** Updates a single [category]; full-replace PUT under the hood. On failure cache is untouched. */ + suspend fun updatePreference( + userWalletId: UserWalletId, + category: PushNotificationCategory, + isEnabled: Boolean, + ): Either +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 9aad1ef665..53bfe57f10 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -119,6 +119,7 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.notifications) + implementation(projects.domain.pushNotificationPreferences) implementation(projects.domain.transaction) implementation(projects.domain.yieldSupply) implementation(projects.domain.yieldSupply.models) @@ -136,6 +137,7 @@ dependencies { implementation(projects.features.onboardingV2.api) implementation(projects.features.onramp.api) implementation(projects.features.pushNotifications.api) + implementation(projects.features.pushNotificationSettings.api) implementation(projects.features.swap.api) implementation(projects.features.tester.api) implementation(projects.features.tokendetails.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index f85b700a44..993e9512ba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -47,6 +47,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWal import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletContentFetcher import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.domain.pushnotificationpreferences.PreloadWalletPushNotificationPreferencesUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -61,6 +62,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejec import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeatureToggles import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles @@ -94,6 +96,7 @@ internal class WalletModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, private val walletNameMigrationUseCase: WalletNameMigrationUseCase, + private val preloadWalletPushNotificationPreferencesUseCase: PreloadWalletPushNotificationPreferencesUseCase, private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, private val walletImageResolver: WalletImageResolver, private val onrampStatusFactory: OnrampStatusFactory, @@ -122,6 +125,7 @@ internal class WalletModel @Inject constructor( private val uiMessageSender: UiMessageSender, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val walletFeatureToggles: WalletFeatureToggles, + private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles, private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, @@ -146,6 +150,7 @@ internal class WalletModel @Inject constructor( maybeMigrateNames() maybeSetWalletFirstTimeUsage() + preloadPushNotificationPreferences() updateYieldSupplyApy() subscribeToUserWalletsUpdates() subscribeOnBalanceHiding() @@ -192,6 +197,19 @@ internal class WalletModel @Inject constructor( } } + private fun preloadPushNotificationPreferences() { + if (!pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled) return + getWalletsUseCase() + .map { wallets -> wallets.map(UserWallet::walletId) } + .distinctUntilChanged() + .onEach { walletIds -> + walletIds.forEach { walletId -> + modelScope.launch { preloadWalletPushNotificationPreferencesUseCase(walletId) } + } + } + .launchIn(modelScope) + } + private fun maybeSetWalletFirstTimeUsage() { modelScope.launch { setWalletFirstTimeUsageUseCase() diff --git a/settings.gradle.kts b/settings.gradle.kts index 64a025b52b..e3d58b1f57 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -357,6 +357,7 @@ include(":domain:app-theme") include(":domain:app-theme:models") include(":domain:balance-hiding") include(":domain:balance-hiding:models") +include(":domain:push-notification-preferences") include(":domain:transaction") include(":domain:transaction:models") include(":domain:analytics") @@ -412,6 +413,7 @@ include(":data:account") include(":data:app-currency") include(":data:app-theme") include(":data:balance-hiding") +include(":data:push-notification-preferences") include(":data:common") include(":data:card") include(":data:tokens")