From 54e612cf30f2baccfcec4684d8ae5b773bedadbb Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Jan 2026 11:16:20 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../di/domain/NotificationsDomainModule.kt | 10 +++ .../tangem/tap/features/main/MainViewModel.kt | 25 ++++++- .../DefaultPushNotificationsRepository.kt | 33 ++++++++- .../DefaultPushNotificationsRepositoryTest.kt | 74 ++++++++++++++++++- .../models/NotificationsError.kt | 8 ++ .../ClearApplicationIdUseCase.kt | 12 +++ .../notifications/SendPushTokenUseCase.kt | 6 +- .../repository/PushNotificationsRepository.kt | 7 +- .../notifications/SendPushTokenUseCaseTest.kt | 43 ++++++++++- 9 files changed, 200 insertions(+), 18 deletions(-) create mode 100644 domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/NotificationsError.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/ClearApplicationIdUseCase.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 5e28ca4628..256d103aa3 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 @@ -83,4 +83,14 @@ internal object NotificationsDomainModule { ): GetNetworksAvailableForNotificationsUseCase { return GetNetworksAvailableForNotificationsUseCase(pushNotificationsRepository = pushNotificationsRepository) } + + @Provides + @Singleton + fun provideClearApplicationIdUseCase( + pushNotificationsRepository: PushNotificationsRepository, + ): ClearApplicationIdUseCase { + return ClearApplicationIdUseCase( + pushNotificationsRepository = pushNotificationsRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 9ea074747f..6768c39ec9 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -21,9 +21,11 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.notifications.ClearApplicationIdUseCase import com.tangem.domain.notifications.GetApplicationIdUseCase import com.tangem.domain.notifications.SendPushTokenUseCase import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.models.NotificationsError import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds @@ -72,6 +74,7 @@ internal class MainViewModel @Inject constructor( private val getApplicationIdUseCase: GetApplicationIdUseCase, private val subscribeOnWalletsUseCase: GetSavedWalletsCountUseCase, private val associateWalletsWithApplicationIdUseCase: AssociateWalletsWithApplicationIdUseCase, + private val clearApplicationIdUseCase: ClearApplicationIdUseCase, private val updateRemoteWalletsInfoUseCase: UpdateRemoteWalletsInfoUseCase, private val sendPushTokenUseCase: SendPushTokenUseCase, private val apiConfigsManager: ApiConfigsManager, @@ -104,7 +107,7 @@ internal class MainViewModel @Inject constructor( launch { fetchStakingOptions() } - launch { initPushNotifications() } + launch(dispatchers.default) { initPushNotifications() } } viewModelScope.launch { incrementAppLaunchCounterUseCase() } @@ -398,13 +401,27 @@ internal class MainViewModel @Inject constructor( private suspend fun initPushNotifications() { getApplicationIdUseCase() .onRight { applicationId -> - sendPushTokenUseCase(applicationId = applicationId) - associateWalletsWithApplicationId(applicationId = applicationId) - updateRemoteWalletsInfoUseCase(applicationId = applicationId) + sendPushTokenUseCase(applicationId = applicationId).onLeft { error -> + if (error is NotificationsError.ApplicationIdNotFound) { + clearApplicationIdUseCase() + getApplicationIdUseCase().onRight { newApplicationId -> + associateAndUpdateWallets(applicationId = newApplicationId) + } + } else { + associateAndUpdateWallets(applicationId = applicationId) + } + }.onRight { + associateAndUpdateWallets(applicationId = applicationId) + } } .onLeft(Timber::e) } + private suspend fun associateAndUpdateWallets(applicationId: ApplicationId) { + associateWalletsWithApplicationId(applicationId = applicationId) + updateRemoteWalletsInfoUseCase(applicationId = applicationId) + } + private fun associateWalletsWithApplicationId(applicationId: ApplicationId) { subscribeOnWalletsUseCase() .onEach { wallets -> 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 index f01830c332..80e3c7c27f 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt @@ -1,6 +1,12 @@ package com.tangem.data.notifications +import androidx.datastore.preferences.core.edit +import arrow.core.Either +import arrow.core.left +import arrow.core.right import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody @@ -9,6 +15,7 @@ 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.models.NotificationsError import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.notifications.models.NotificationsEligibleNetwork import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -47,9 +54,15 @@ internal class DefaultPushNotificationsRepository @Inject constructor( ?.let(::ApplicationId) } - override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { + override suspend fun clearApplicationId() { + appPreferencesStore.edit { mutablePreferences -> + mutablePreferences.remove(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) + } + } + + override suspend fun sendPushToken(appId: ApplicationId, pushToken: String): Either = withContext(dispatchers.io) { - tangemTechApi.updatePushTokenForApplicationId( + val response = tangemTechApi.updatePushTokenForApplicationId( appId.value, NotificationApplicationCreateBody( pushToken = pushToken, @@ -58,9 +71,21 @@ internal class DefaultPushNotificationsRepository @Inject constructor( timezone = appInfoProvider.timezone, version = appInfoProvider.appVersion, ), - ).getOrThrow() + ) + when (response) { + is ApiResponse.Success -> Unit.right() + is ApiResponse.Error -> { + val cause = response.cause + if (cause is ApiResponseError.HttpException && + cause.code == ApiResponseError.HttpException.Code.NOT_FOUND + ) { + NotificationsError.ApplicationIdNotFound.left() + } else { + NotificationsError.DataError(cause.message ?: "Unknown error").left() + } + } + } } - } override suspend fun getEligibleNetworks(): List = withContext(dispatchers.io) { tangemTechApi.getEligibleNetworksForPushNotifications().getOrThrow().mapNotNull { 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 index b423cb27ed..e45646997d 100644 --- a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt @@ -10,9 +10,12 @@ 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 arrow.core.Either import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.models.* import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.models.NotificationsError import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify @@ -107,7 +110,20 @@ class DefaultPushNotificationsRepositoryTest { } @Test - fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { + fun `GIVEN stored application id WHEN clearApplicationId THEN removes it from preferences`() = runTest { + // GIVEN + val preferences = mockk(relaxed = true) + coEvery { preferencesDataStore.updateData(any()) } returns preferences + + // WHEN + repository.clearApplicationId() + + // THEN + coVerify { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN application id and push token WHEN sendPushToken succeeds THEN returns Right Unit`() = runTest { // GIVEN val appId = ApplicationId("test-app-id") val pushToken = "test-push-token" @@ -130,9 +146,10 @@ class DefaultPushNotificationsRepositoryTest { } returns ApiResponse.Success(Unit) // WHEN - repository.sendPushToken(appId, pushToken) + val result = repository.sendPushToken(appId, pushToken) // THEN + assertThat(result).isEqualTo(Either.Right(Unit)) coVerify { tangemTechApi.updatePushTokenForApplicationId( appId.value, @@ -147,6 +164,59 @@ class DefaultPushNotificationsRepositoryTest { } } + @Test + fun `GIVEN application id not found WHEN sendPushToken THEN returns Left ApplicationIdNotFound`() = runTest { + // GIVEN + val appId = ApplicationId("test-app-id") + val pushToken = "test-push-token" + 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(any(), any()) + } returns (ApiResponse.Error( + ApiResponseError.HttpException( + code = ApiResponseError.HttpException.Code.NOT_FOUND, + message = "Not found", + errorBody = null, + ), + ) as ApiResponse) + + // WHEN + val result = repository.sendPushToken(appId, pushToken) + + // THEN + assertThat(result).isEqualTo(Either.Left(NotificationsError.ApplicationIdNotFound)) + } + + @Test + fun `GIVEN api error WHEN sendPushToken THEN returns Left DataError`() = runTest { + // GIVEN + val appId = ApplicationId("test-app-id") + val pushToken = "test-push-token" + val errorMessage = "Server error" + 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(any(), any()) + } returns (ApiResponse.Error( + ApiResponseError.HttpException( + code = ApiResponseError.HttpException.Code.BAD_REQUEST, + message = errorMessage, + errorBody = null, + ), + ) as ApiResponse) + + // WHEN + val result = repository.sendPushToken(appId, pushToken) + + // THEN + assertThat(result).isEqualTo(Either.Left(NotificationsError.DataError(errorMessage))) + } + @Test fun `GIVEN eligible networks WHEN getEligibleNetworks THEN returns converted networks`() = runTest { // GIVEN diff --git a/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/NotificationsError.kt b/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/NotificationsError.kt new file mode 100644 index 0000000000..7afb20b813 --- /dev/null +++ b/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/NotificationsError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.notifications.models + +sealed class NotificationsError { + + data class DataError(val message: String) : NotificationsError() + + data object ApplicationIdNotFound : NotificationsError() +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/ClearApplicationIdUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/ClearApplicationIdUseCase.kt new file mode 100644 index 0000000000..8457094d6f --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/ClearApplicationIdUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.notifications + +import com.tangem.domain.notifications.repository.PushNotificationsRepository + +class ClearApplicationIdUseCase( + private val pushNotificationsRepository: PushNotificationsRepository, +) { + + suspend operator fun invoke() { + pushNotificationsRepository.clearApplicationId() + } +} \ 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 d33888f342..d9284982c7 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 @@ -1,7 +1,9 @@ package com.tangem.domain.notifications import arrow.core.Either +import arrow.core.raise.either import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.models.NotificationsError import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider @@ -10,8 +12,8 @@ class SendPushTokenUseCase( private val pushNotificationsTokenProvider: PushNotificationsTokenProvider, ) { - suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { + suspend operator fun invoke(applicationId: ApplicationId): Either = either { val token = pushNotificationsTokenProvider.getToken() - pushNotificationsRepository.sendPushToken(applicationId, token) + pushNotificationsRepository.sendPushToken(applicationId, token).bind() } } \ 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 index 338a74b0ce..53f7b08f65 100644 --- 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 @@ -1,7 +1,9 @@ package com.tangem.domain.notifications.repository +import arrow.core.Either import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.models.NotificationsEligibleNetwork +import com.tangem.domain.notifications.models.NotificationsError interface PushNotificationsRepository { @@ -12,8 +14,9 @@ interface PushNotificationsRepository { suspend fun getApplicationId(): ApplicationId? - @Throws - suspend fun sendPushToken(appId: ApplicationId, pushToken: String) + suspend fun clearApplicationId() + + suspend fun sendPushToken(appId: ApplicationId, pushToken: String): Either @Throws suspend fun getEligibleNetworks(): List 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 fe84d79f16..ccd193de72 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,6 +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.models.NotificationsError import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider import io.mockk.coEvery @@ -34,7 +35,7 @@ class SendPushTokenUseCaseTest { val applicationId = ApplicationId("test-app-id") val token = "test-token" coEvery { pushNotificationsTokenProvider.getToken() } returns token - coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } returns Unit + coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } returns Either.Right(Unit) // WHEN val result = sendPushTokenUseCase(applicationId) @@ -45,13 +46,13 @@ class SendPushTokenUseCaseTest { } @Test - fun `GIVEN repository throws error WHEN invoke THEN returns error`() = runTest { + fun `GIVEN repository returns DataError WHEN invoke THEN returns DataError`() = runTest { // GIVEN val applicationId = ApplicationId("test-app-id") val token = "test-token" - val expectedError = RuntimeException("Network error") + val expectedError = NotificationsError.DataError("Network error") coEvery { pushNotificationsTokenProvider.getToken() } returns token - coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } throws expectedError + coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } returns Either.Left(expectedError) // WHEN val result = sendPushTokenUseCase(applicationId) @@ -60,4 +61,38 @@ class SendPushTokenUseCaseTest { assertThat(result).isEqualTo(Either.Left(expectedError)) coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, token) } } + + @Test + fun `GIVEN repository returns ApplicationIdNotFound WHEN invoke THEN returns ApplicationIdNotFound`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val token = "test-token" + coEvery { pushNotificationsTokenProvider.getToken() } returns token + coEvery { + pushNotificationsRepository.sendPushToken(applicationId, token) + } returns Either.Left(NotificationsError.ApplicationIdNotFound) + + // WHEN + val result = sendPushTokenUseCase(applicationId) + + // THEN + assertThat(result).isEqualTo(Either.Left(NotificationsError.ApplicationIdNotFound)) + coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, token) } + } + + @Test + fun `GIVEN empty token WHEN invoke THEN sends empty token to repository`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val emptyToken = "" + coEvery { pushNotificationsTokenProvider.getToken() } returns emptyToken + coEvery { pushNotificationsRepository.sendPushToken(applicationId, emptyToken) } returns Either.Right(Unit) + + // WHEN + val result = sendPushTokenUseCase(applicationId) + + // THEN + assertThat(result).isEqualTo(Either.Right(Unit)) + coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, emptyToken) } + } } \ No newline at end of file