Updated on 2026-08-14
This commit is contained in:
parent
03dce384bd
commit
c4603c4cbd
13 changed files with 171 additions and 39 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -154,7 +154,7 @@ interface TangemTechApi {
|
|||
suspend fun updatePushTokenForApplicationId(
|
||||
@Path("application_id") applicationId: String,
|
||||
@Body body: NotificationApplicationCreateBody,
|
||||
): ApiResponse<String>
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@PATCH("user-wallets/wallets/{wallet_id}/notify")
|
||||
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ class DefaultNotificationsRepositoryTest {
|
|||
timezone = null,
|
||||
),
|
||||
)
|
||||
} returns ApiResponse.Success(appId.value)
|
||||
} returns ApiResponse.Success(Unit)
|
||||
|
||||
// WHEN
|
||||
repository.sendPushToken(appId, pushToken)
|
||||
|
|
|
|||
|
|
@ -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<Throwable, Unit> = Either.catch {
|
||||
val applicationId = getApplicationIdUseCase().getOrNull()
|
||||
?: error("Application ID not found")
|
||||
suspend operator fun invoke(applicationId: ApplicationId): Either<Throwable, Unit> = Either.catch {
|
||||
val token = pushNotificationsTokenProvider.getToken()
|
||||
notificationsRepository.sendPushToken(applicationId, token)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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<Throwable, Unit> = Either.catch {
|
||||
val wallets = userWalletsListManager.userWalletsSync
|
||||
walletsRepository.associateWallets(applicationId.value, wallets)
|
||||
}
|
||||
suspend operator fun invoke(applicationId: ApplicationId, wallets: List<UserWallet>): Either<Throwable, Unit> =
|
||||
Either.catch {
|
||||
walletsRepository.associateWallets(applicationId.value, wallets)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<UserWallet>> {
|
||||
return userWalletsListManager.userWallets
|
||||
.filter {
|
||||
userWalletsListManager.asLockable() ?: return@filter false
|
||||
return@filter if (userWalletsListManager.isLockedSync.not()) {
|
||||
it.isNotEmpty()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UpdateWalletError, Unit> = either {
|
||||
suspend operator fun invoke(applicationId: ApplicationId): Either<Throwable, Unit> = Either.catch {
|
||||
val walletsInfo = walletsRepository.getWalletsInfo(applicationId.value)
|
||||
userWalletsSyncDelegate.syncWallets(walletsInfo).bind()
|
||||
userWalletsSyncDelegate.syncWallets(walletsInfo)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserWalletsListManager.Lockable>()
|
||||
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<UserWalletsListManager.Lockable>()
|
||||
val wallets = listOf(mockk<UserWallet>(), mockk<UserWallet>())
|
||||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue