Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-27 11:49:45 +04:00
parent fe2d1c8cbe
commit 10599d6770
6 changed files with 284 additions and 6 deletions

View file

@ -333,10 +333,14 @@ internal object WalletsDomainModule {
fun providesUpdateRemoteWalletsInfoUseCase(
walletsRepository: WalletsRepository,
userWalletsSyncDelegate: UserWalletsSyncDelegate,
userWalletsListRepository: UserWalletsListRepository,
generateWalletNameUseCase: GenerateWalletNameUseCase,
): UpdateRemoteWalletsInfoUseCase {
return UpdateRemoteWalletsInfoUseCase(
walletsRepository = walletsRepository,
userWalletsSyncDelegate = userWalletsSyncDelegate,
generateWalletNameUseCase = generateWalletNameUseCase,
userWalletsListRepository = userWalletsListRepository,
)
}

View file

@ -75,7 +75,7 @@ class UserTokensSaver(
val conversionData = appsFlyerConversionStore.get()
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy(
walletName = userWallet.name,
walletName = userWallet.name.takeIf { it.isNotBlank() },
walletType = WalletType.from(userWallet),
refcode = conversionData?.refcode,
campaign = conversionData?.campaign,

View file

@ -96,7 +96,7 @@ class UserTokensSaverTest {
val userWalletId = UserWalletId("1234567890abcdef")
val userWallet = mockk<UserWallet.Cold> {
every { this@mockk.walletId } returns userWalletId
every { this@mockk.name } returns "Wallet"
every { this@mockk.name } returns ""
}
val response = UserTokensResponse(
@ -104,7 +104,7 @@ class UserTokensSaverTest {
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
walletName = userWallet.name,
walletName = null,
walletType = WalletType.COLD,
)
val enrichedResponse = UserTokensResponse(
@ -112,12 +112,13 @@ class UserTokensSaverTest {
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.MANUAL,
tokens = emptyList(),
walletName = userWallet.name,
walletName = null,
walletType = WalletType.COLD,
)
val error = ApiResponseError.UnknownException(Exception("API Error"))
var onFailSendCalled = false
every { accountsFeatureToggles.isFeatureEnabled } returns true
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
coEvery { enricher(userWalletId, response) } returns enrichedResponse
coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>

View file

@ -2,7 +2,7 @@ package com.tangem.domain.wallets.models
import com.tangem.domain.models.wallet.UserWalletId
class UserWalletRemoteInfo(
data class UserWalletRemoteInfo(
val walletId: UserWalletId,
val name: String,
val isNotificationsEnabled: Boolean,

View file

@ -1,6 +1,10 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.repository.WalletsRepository
@ -8,10 +12,34 @@ import com.tangem.domain.wallets.repository.WalletsRepository
class UpdateRemoteWalletsInfoUseCase(
private val walletsRepository: WalletsRepository,
private val userWalletsSyncDelegate: UserWalletsSyncDelegate,
private val userWalletsListRepository: UserWalletsListRepository,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
) {
suspend operator fun invoke(applicationId: ApplicationId): Either<Throwable, Unit> = Either.catch {
val walletsInfo = walletsRepository.getWalletsInfo(applicationId.value)
val userWalletsMap = userWalletsListRepository.userWallets.value?.associateBy { it.walletId }.orEmpty()
val remoteData = walletsRepository.getWalletsInfo(applicationId.value)
val walletsInfo = remoteData.map { walletRemoteInfo ->
if (walletRemoteInfo.name.isBlank()) {
walletRemoteInfo.copy(name = generateName(walletRemoteInfo.walletId, userWalletsMap))
} else {
walletRemoteInfo
}
}.filter { it.name.isNotBlank() }
userWalletsSyncDelegate.syncWallets(walletsInfo)
}
private fun generateName(userWalletId: UserWalletId, userWalletsMap: Map<UserWalletId, UserWallet>): String {
val userWallet = userWalletsMap[userWalletId] ?: return ""
return when (userWallet) {
is UserWallet.Hot -> generateWalletNameUseCase.invokeForHot()
is UserWallet.Cold -> generateWalletNameUseCase(
userWallet.scanResponse.productType,
card = userWallet.scanResponse.card,
isStartToCoin = userWallet.scanResponse.cardTypesResolver.isStart2Coin(),
)
}
}
}

View file

@ -0,0 +1,245 @@
package com.tangem.domain.wallets.usecase
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.models.UserWalletRemoteInfo
import com.tangem.domain.wallets.repository.WalletsRepository
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
class UpdateRemoteWalletsInfoUseCaseTest {
private lateinit var useCase: UpdateRemoteWalletsInfoUseCase
private lateinit var walletsRepository: WalletsRepository
private lateinit var userWalletsSyncDelegate: UserWalletsSyncDelegate
private lateinit var userWalletListRepository: UserWalletsListRepository
private lateinit var generateWalletNameUseCase: GenerateWalletNameUseCase
@Before
fun setup() {
walletsRepository = mockk()
userWalletsSyncDelegate = mockk()
userWalletListRepository = mockk {
every { userWallets } returns MutableStateFlow<List<UserWallet>?>(emptyList())
}
generateWalletNameUseCase = mockk()
useCase = UpdateRemoteWalletsInfoUseCase(
walletsRepository = walletsRepository,
userWalletsSyncDelegate = userWalletsSyncDelegate,
userWalletsListRepository = userWalletListRepository,
generateWalletNameUseCase = generateWalletNameUseCase,
)
}
@Test
fun `GIVEN wallets with non-empty names WHEN invoke THEN return Right with Unit`() = runTest {
// GIVEN
val applicationId = ApplicationId("test-app-id")
val walletsInfo = listOf(
UserWalletRemoteInfo(UserWalletId("0A0B0C0D"), "Wallet 1", true),
UserWalletRemoteInfo(UserWalletId("0E0F1011"), "Wallet 2", true),
)
coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns walletsInfo
coEvery { userWalletsSyncDelegate.syncWallets(walletsInfo) } returns Unit.right()
// WHEN
val result = useCase(applicationId)
// THEN
assertThat(result.isRight()).isTrue()
coVerify { walletsRepository.getWalletsInfo(applicationId.value) }
coVerify { userWalletsSyncDelegate.syncWallets(walletsInfo) }
}
@Test
fun `GIVEN wallets with empty names WHEN invoke THEN filter out wallets with empty names`() = runTest {
// GIVEN
val applicationId = ApplicationId("test-app-id")
val walletWithName = UserWalletRemoteInfo(UserWalletId("0A0B0C0D"), "Wallet 1", true)
val walletWithEmptyName = UserWalletRemoteInfo(UserWalletId("0E0F1011"), "", true)
val walletsInfo = listOf(walletWithName, walletWithEmptyName)
val filteredWallets = listOf(walletWithName)
coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns walletsInfo
coEvery { userWalletsSyncDelegate.syncWallets(filteredWallets) } returns Unit.right()
// WHEN
val result = useCase(applicationId)
// THEN
assertThat(result.isRight()).isTrue()
coVerify { userWalletsSyncDelegate.syncWallets(filteredWallets) }
}
@Test
fun `GIVEN empty wallets list WHEN invoke THEN sync with empty list`() = runTest {
// GIVEN
val applicationId = ApplicationId("test-app-id")
val walletsInfo = emptyList<UserWalletRemoteInfo>()
coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns walletsInfo
coEvery { userWalletsSyncDelegate.syncWallets(walletsInfo) } returns Unit.right()
// WHEN
val result = useCase(applicationId)
// THEN
assertThat(result.isRight()).isTrue()
coVerify { userWalletsSyncDelegate.syncWallets(emptyList()) }
}
@Test
fun `GIVEN repository throws exception WHEN invoke THEN return Left with Throwable`() = runTest {
// GIVEN
val applicationId = ApplicationId("test-app-id")
val exception = RuntimeException("Network error")
coEvery { walletsRepository.getWalletsInfo(applicationId.value) } throws exception
// WHEN
val result = useCase(applicationId)
// THEN
assertThat(result.isLeft()).isTrue()
result.onLeft { throwable ->
assertThat(throwable).isEqualTo(exception)
}
}
@Test
fun `GIVEN syncWallets throws exception WHEN invoke THEN return Left with Throwable`() = runTest {
// GIVEN
val applicationId = ApplicationId("test-app-id")
val walletsInfo = listOf(
UserWalletRemoteInfo(UserWalletId("0A0B0C0D"), "Wallet 1", true),
)
val exception = RuntimeException("Sync error")
coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns walletsInfo
coEvery { userWalletsSyncDelegate.syncWallets(walletsInfo) } throws exception
// WHEN
val result = useCase(applicationId)
// THEN
assertThat(result.isLeft()).isTrue()
result.onLeft { throwable ->
assertThat(throwable).isEqualTo(exception)
}
}
@Test
fun `GIVEN all wallets have empty names WHEN invoke THEN sync with empty list`() = runTest {
// GIVEN
val applicationId = ApplicationId("test-app-id")
val walletsInfo = listOf(
UserWalletRemoteInfo(UserWalletId("0A0B0C0D"), "", true),
UserWalletRemoteInfo(UserWalletId("0E0F1011"), "", true),
)
coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns walletsInfo
coEvery { userWalletsSyncDelegate.syncWallets(emptyList()) } returns Unit.right()
// WHEN
val result = useCase(applicationId)
// THEN
assertThat(result.isRight()).isTrue()
coVerify { userWalletsSyncDelegate.syncWallets(emptyList()) }
}
@Test
fun `GIVEN hot wallet with blank name WHEN invoke THEN generate name using invokeForHot`() = runTest {
// GIVEN
val applicationId = ApplicationId("test-app-id")
val walletId = UserWalletId("0A0B0C0D")
val hotWallet = mockk<UserWallet.Hot> {
every { this@mockk.walletId } returns walletId
}
every { userWalletListRepository.userWallets } returns MutableStateFlow(listOf(hotWallet))
val remoteWalletWithBlankName = UserWalletRemoteInfo(walletId, "", true)
val generatedName = "Wallet"
every { generateWalletNameUseCase.invokeForHot() } returns generatedName
val expectedWallet = UserWalletRemoteInfo(walletId, generatedName, true)
coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns listOf(remoteWalletWithBlankName)
coEvery { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) } returns Unit.right()
// WHEN
val result = useCase(applicationId)
// THEN
assertThat(result.isRight()).isTrue()
coVerify { generateWalletNameUseCase.invokeForHot() }
coVerify { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) }
}
@Test
fun `GIVEN cold wallet with blank name WHEN invoke THEN generate name using productType`() = runTest {
// GIVEN
val applicationId = ApplicationId("test-app-id")
val walletId = UserWalletId("0A0B0C0D")
val mockScanResponse = mockk<ScanResponse>(relaxed = true) {
every { productType } returns ProductType.Wallet2
every { card } returns mockk(relaxed = true)
}
val coldWallet = mockk<UserWallet.Cold> {
every { this@mockk.walletId } returns walletId
every { scanResponse } returns mockScanResponse
}
every { userWalletListRepository.userWallets } returns MutableStateFlow(listOf(coldWallet))
val remoteWalletWithBlankName = UserWalletRemoteInfo(walletId, "", true)
val generatedName = "Wallet"
every { generateWalletNameUseCase.invoke(any(), any(), any()) } returns generatedName
val expectedWallet = UserWalletRemoteInfo(walletId, generatedName, true)
coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns listOf(remoteWalletWithBlankName)
coEvery { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) } returns Unit.right()
// WHEN
val result = useCase(applicationId)
// THEN
assertThat(result.isRight()).isTrue()
coVerify { generateWalletNameUseCase.invoke(any(), any(), any()) }
coVerify { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) }
}
@Test
fun `GIVEN wallet with blank name not in repository WHEN invoke THEN filter it out`() = runTest {
// GIVEN
val applicationId = ApplicationId("test-app-id")
val knownWalletId = UserWalletId("0A0B0C0D")
val unknownWalletId = UserWalletId("0E0F1011")
val hotWallet = mockk<UserWallet.Hot> {
every { this@mockk.walletId } returns knownWalletId
}
every { userWalletListRepository.userWallets } returns MutableStateFlow(listOf(hotWallet))
every { generateWalletNameUseCase.invokeForHot() } returns "Wallet"
val knownWalletRemote = UserWalletRemoteInfo(knownWalletId, "", true)
val unknownWalletRemote = UserWalletRemoteInfo(unknownWalletId, "", true)
val expectedWallet = UserWalletRemoteInfo(knownWalletId, "Wallet", true)
coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns listOf(knownWalletRemote, unknownWalletRemote)
coEvery { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) } returns Unit.right()
// WHEN
val result = useCase(applicationId)
// THEN
assertThat(result.isRight()).isTrue()
coVerify { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) }
}
}