diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 37e399bc70..0c9d9ac315 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -111,6 +111,7 @@ dependencies { implementation(projects.domain.networks) implementation(projects.domain.quotes) implementation(projects.domain.notifications) + implementation(projects.domain.notifications.models) implementation(projects.domain.notifications.toggles) implementation(projects.common) 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 02a9b8be72..6167cea5a0 100644 --- a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt @@ -3,8 +3,9 @@ package com.tangem.tap.data import com.google.firebase.messaging.FirebaseMessaging import com.tangem.utils.notifications.PushNotificationsTokenProvider import kotlinx.coroutines.tasks.await +import javax.inject.Inject -internal class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider { +internal class FirebasePushNotificationsTokenProvider @Inject constructor() : PushNotificationsTokenProvider { override suspend fun getToken(): String { return FirebaseMessaging.getInstance().token.await() } 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 706ef792e1..14df43efb0 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 @@ -31,12 +31,10 @@ internal object NotificationsDomainModule { @Singleton fun providesSendPushTokenUseCase( notificationsRepository: NotificationsRepository, - getApplicationIdUseCase: GetApplicationIdUseCase, pushNotificationsTokenProvider: PushNotificationsTokenProvider, ): SendPushTokenUseCase { return SendPushTokenUseCase( notificationsRepository = notificationsRepository, - getApplicationIdUseCase = getApplicationIdUseCase, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index b153b0e0de..1fbf5cb42e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -225,4 +225,24 @@ internal object WalletsDomainModule { userWalletsSyncDelegate = userWalletsSyncDelegate, ) } + + @Provides + @Singleton + fun providesGetSavedWalletChangesIdUseCase( + userWalletsListManager: UserWalletsListManager, + ): GetSavedWalletChangesUseCase { + return GetSavedWalletChangesUseCase( + userWalletsListManager = userWalletsListManager, + ) + } + + @Provides + @Singleton + fun providesAssociateWalletsWithApplicationIdUseCase( + walletsRepository: WalletsRepository, + ): AssociateWalletsWithApplicationIdUseCase { + return AssociateWalletsWithApplicationIdUseCase( + walletsRepository = walletsRepository, + ) + } } \ 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 d2b77135d2..7dee2ef45c 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 @@ -11,7 +11,6 @@ import com.tangem.core.analytics.models.event.TechAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.deeplink.DeepLinksRegistry -import com.tangem.core.ui.BuildConfig import com.tangem.core.ui.R import com.tangem.core.ui.coil.ImagePreloader import com.tangem.core.ui.extensions.resourceReference @@ -24,6 +23,10 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase +import com.tangem.domain.notifications.GetApplicationIdUseCase +import com.tangem.domain.notifications.SendPushTokenUseCase +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.promo.GetStoryContentUseCase @@ -33,6 +36,9 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase +import com.tangem.domain.wallets.usecase.GetSavedWalletChangesUseCase +import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.features.onramp.deeplink.OnrampDeepLink import com.tangem.tap.common.extensions.setContext @@ -40,6 +46,7 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.wallet.BuildConfig import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* @@ -71,6 +78,12 @@ internal class MainViewModel @Inject constructor( private val onboardingRepository: OnboardingRepository, private val deepLinksRegistry: DeepLinksRegistry, private val onrampDeepLinkFactory: OnrampDeepLink.Factory, + private val notificationsToggles: NotificationsFeatureToggles, + private val getApplicationIdUseCase: GetApplicationIdUseCase, + private val subscribeOnWalletsUseCase: GetSavedWalletChangesUseCase, + private val associateWalletsWithApplicationIdUseCase: AssociateWalletsWithApplicationIdUseCase, + private val updateRemoteWalletsInfoUseCase: UpdateRemoteWalletsInfoUseCase, + private val sendPushTokenUseCase: SendPushTokenUseCase, private val apiConfigsManager: ApiConfigsManager, routingFeatureToggle: RoutingFeatureToggle, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -96,6 +109,8 @@ internal class MainViewModel @Inject constructor( launch { fetchAppCurrenciesUseCase() } launch { fetchStakingTokens() } + + launch { initPushNotifications() } } viewModelScope.launch { incrementAppLaunchCounterUseCase() } @@ -370,4 +385,20 @@ internal class MainViewModel @Inject constructor( private fun initializeDeepLinks() { deepLinksRegistry.register(onrampDeepLinkFactory.create(viewModelScope)) } + + private suspend fun initPushNotifications() { + if (notificationsToggles.isNotificationsEnabled) { + getApplicationIdUseCase().onRight { applicationId -> + sendPushTokenUseCase(applicationId = applicationId) + associateWalletsWithApplicationId(applicationId = applicationId) + updateRemoteWalletsInfoUseCase(applicationId = applicationId) + }.onLeft { Timber.e(it.toString()) } + } + } + + private fun associateWalletsWithApplicationId(applicationId: ApplicationId) { + subscribeOnWalletsUseCase().onEach { wallets -> + associateWalletsWithApplicationIdUseCase(applicationId, wallets) + }.launchIn(viewModelScope) + } } \ 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 d4db8f1fa1..81b01211dc 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 @@ -154,7 +154,7 @@ interface TangemTechApi { suspend fun updatePushTokenForApplicationId( @Path("application_id") applicationId: String, @Body body: NotificationApplicationCreateBody, - ): ApiResponse + ): ApiResponse @PATCH("user-wallets/wallets/{wallet_id}/notify") suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse 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 691dff0714..46c37951b1 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 @@ -121,7 +121,7 @@ class DefaultNotificationsRepositoryTest { timezone = null, ), ) - } returns ApiResponse.Success(appId.value) + } returns ApiResponse.Success(Unit) // WHEN repository.sendPushToken(appId, pushToken) 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 45617cf6e4..63247b9a7d 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,18 +1,16 @@ package com.tangem.domain.notifications import arrow.core.Either +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider class SendPushTokenUseCase( private val notificationsRepository: NotificationsRepository, - private val getApplicationIdUseCase: GetApplicationIdUseCase, private val pushNotificationsTokenProvider: PushNotificationsTokenProvider, ) { - suspend operator fun invoke(): Either = Either.catch { - val applicationId = getApplicationIdUseCase().getOrNull() - ?: error("Application ID not found") + suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { val token = pushNotificationsTokenProvider.getToken() notificationsRepository.sendPushToken(applicationId, token) } 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 f079a021d1..18d716fdf0 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 @@ -15,18 +15,15 @@ import org.junit.Test class SendPushTokenUseCaseTest { private lateinit var notificationsRepository: NotificationsRepository - private lateinit var getApplicationIdUseCase: GetApplicationIdUseCase private lateinit var pushNotificationsTokenProvider: PushNotificationsTokenProvider private lateinit var sendPushTokenUseCase: SendPushTokenUseCase @Before fun setup() { notificationsRepository = mockk() - getApplicationIdUseCase = mockk() pushNotificationsTokenProvider = mockk() sendPushTokenUseCase = SendPushTokenUseCase( notificationsRepository = notificationsRepository, - getApplicationIdUseCase = getApplicationIdUseCase, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } @@ -36,42 +33,28 @@ class SendPushTokenUseCaseTest { // GIVEN val applicationId = ApplicationId("test-app-id") val token = "test-token" - coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId) coEvery { pushNotificationsTokenProvider.getToken() } returns token coEvery { notificationsRepository.sendPushToken(applicationId, token) } returns Unit // WHEN - val result = sendPushTokenUseCase() + val result = sendPushTokenUseCase(applicationId) // THEN assertThat(result).isEqualTo(Either.Right(Unit)) coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) } } - @Test - fun `GIVEN application ID is not found WHEN invoke THEN throws error`() = runTest { - // GIVEN - coEvery { getApplicationIdUseCase() } returns Either.Left(Throwable("Application ID not found")) - - // WHEN & THEN - val result = sendPushTokenUseCase() - assertThat(result.isLeft()).isTrue() - assertThat(result.fold({ it.message }, { null })).isEqualTo("Application ID not found") - coVerify(exactly = 0) { notificationsRepository.sendPushToken(any(), any()) } - } - @Test fun `GIVEN repository throws error WHEN invoke THEN returns error`() = runTest { // GIVEN val applicationId = ApplicationId("test-app-id") val token = "test-token" val expectedError = RuntimeException("Network error") - coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId) coEvery { pushNotificationsTokenProvider.getToken() } returns token coEvery { notificationsRepository.sendPushToken(applicationId, token) } throws expectedError // WHEN - val result = sendPushTokenUseCase() + val result = sendPushTokenUseCase(applicationId) // THEN assertThat(result).isEqualTo(Either.Left(expectedError)) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt index e67bf5b744..664a03af37 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt @@ -2,16 +2,15 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository class AssociateWalletsWithApplicationIdUseCase( - private val userWalletsListManager: UserWalletsListManager, private val walletsRepository: WalletsRepository, ) { - suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { - val wallets = userWalletsListManager.userWalletsSync - walletsRepository.associateWallets(applicationId.value, wallets) - } + suspend operator fun invoke(applicationId: ApplicationId, wallets: List): Either = + Either.catch { + walletsRepository.associateWallets(applicationId.value, wallets) + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCase.kt new file mode 100644 index 0000000000..afcfe40a89 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCase.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.wallets.legacy.isLockedSync +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter + +class GetSavedWalletChangesUseCase( + private val userWalletsListManager: UserWalletsListManager, +) { + + operator fun invoke(): Flow> { + return userWalletsListManager.userWallets + .filter { + userWalletsListManager.asLockable() ?: return@filter false + return@filter if (userWalletsListManager.isLockedSync.not()) { + it.isNotEmpty() + } else { + false + } + } + .distinctUntilChanged() + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt index 5305089cd7..8ae0a158b0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt @@ -1,10 +1,8 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import arrow.core.raise.either import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate -import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.repository.WalletsRepository class UpdateRemoteWalletsInfoUseCase( @@ -12,8 +10,8 @@ class UpdateRemoteWalletsInfoUseCase( private val userWalletsSyncDelegate: UserWalletsSyncDelegate, ) { - suspend operator fun invoke(applicationId: ApplicationId): Either = either { + suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { val walletsInfo = walletsRepository.getWalletsInfo(applicationId.value) - userWalletsSyncDelegate.syncWallets(walletsInfo).bind() + userWalletsSyncDelegate.syncWallets(walletsInfo) } } \ No newline at end of file diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCaseTest.kt new file mode 100644 index 0000000000..25e0cb17db --- /dev/null +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCaseTest.kt @@ -0,0 +1,76 @@ +package com.tangem.domain.wallets.usecase + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.wallets.legacy.isLockedSync +import com.tangem.domain.wallets.models.UserWallet +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class GetSavedWalletChangesUseCaseTest { + + private lateinit var useCase: GetSavedWalletChangesUseCase + private lateinit var userWalletsListManager: UserWalletsListManager + + @Before + fun setup() { + userWalletsListManager = mockk() + useCase = GetSavedWalletChangesUseCase(userWalletsListManager) + mockkStatic("com.tangem.domain.wallets.legacy.UserWalletsListManagerExtensionsKt") + } + + @Test + fun `GIVEN manager is not lockable WHEN invoke THEN return empty list`() = runTest { + // GIVEN + every { userWalletsListManager.isLockable } returns false + every { userWalletsListManager.userWallets } returns flowOf(emptyList()) + every { userWalletsListManager.isLockedSync } returns false + every { userWalletsListManager.asLockable() } returns null + + // WHEN + val result = useCase().firstOrNull() + + // THEN + assertThat(result).isNull() + } + + @Test + fun `GIVEN manager is locked WHEN invoke THEN return empty list`() = runTest { + // GIVEN + val mockLockable = mockk() + every { userWalletsListManager.isLockable } returns true + every { userWalletsListManager.userWallets } returns flowOf(emptyList()) + every { userWalletsListManager.isLockedSync } returns true + every { userWalletsListManager.asLockable() } returns mockLockable + + // WHEN + val result = useCase().firstOrNull() + + // THEN + assertThat(result).isNull() + } + + @Test + fun `GIVEN manager is not locked and has wallets WHEN invoke THEN return wallets list`() = runTest { + // GIVEN + val mockLockable = mockk() + val wallets = listOf(mockk(), mockk()) + every { userWalletsListManager.isLockable } returns true + every { userWalletsListManager.userWallets } returns flowOf(wallets) + every { userWalletsListManager.isLockedSync } returns false + every { userWalletsListManager.asLockable() } returns mockLockable + + // WHEN + val result = useCase().firstOrNull() + + // THEN + assertThat(result).isEqualTo(wallets) + } +} \ No newline at end of file