Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-14 15:35:19 +04:00
commit a3216e8e23
57 changed files with 532 additions and 468 deletions

View file

@ -6,9 +6,9 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.account.usecase.*
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.feature.referral.data.ExternalReferralRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -94,13 +94,9 @@ internal object AccountDomainModule {
@Provides
@Singleton
fun provideIsAccountsModeEnabledUseCase(
userWalletsListRepository: UserWalletsListRepository,
accountsCRUDRepository: AccountsCRUDRepository,
multiAccountListSupplier: MultiAccountListSupplier,
): IsAccountsModeEnabledUseCase {
return IsAccountsModeEnabledUseCase(
userWalletsListRepository = userWalletsListRepository,
crudRepository = accountsCRUDRepository,
)
return IsAccountsModeEnabledUseCase(multiAccountListSupplier = multiAccountListSupplier)
}
@Provides

View file

@ -7,6 +7,7 @@ import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.IsMemoRequiredUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
@ -210,6 +211,14 @@ internal object WalletsDomainModule {
return ValidateWalletMemoUseCase(walletAddressServiceRepository = walletAddressServiceRepository)
}
@Provides
@Singleton
fun providesIsMemoRequiredUseCase(
walletAddressServiceRepository: WalletAddressServiceRepository,
): IsMemoRequiredUseCase {
return IsMemoRequiredUseCase(walletAddressServiceRepository = walletAddressServiceRepository)
}
@Provides
@Singleton
fun providesParseSharedAddressUseCase(

View file

@ -80,6 +80,10 @@
"name": "SOLANA_TX_HISTORY_ENABLED",
"version": "undefined"
},
{
"name": "SOLANA_SCALED_UI_AMOUNT_ENABLED",
"version": "undefined"
},
{
"name": "ADD_AND_MANAGE_TOKENS_ENABLED",
"version": "undefined"

View file

@ -4,6 +4,7 @@ import arrow.core.Option
import arrow.core.some
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.MultiAccountListProducer
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.loadAndGet
import com.tangem.domain.core.flow.FlowProducerTools
@ -22,7 +23,7 @@ import kotlinx.coroutines.flow.*
* @property params params
* @property flowProducerTools tools for producing flows
* @property userWalletsListRepository repository for getting user wallets
* @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet
* @property singleAccountListSupplier supplier for getting [AccountList] per wallet
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
@ -31,7 +32,7 @@ internal class DefaultMultiAccountListProducer @AssistedInject constructor(
@Assisted val params: Unit,
override val flowProducerTools: FlowProducerTools,
private val userWalletsListRepository: UserWalletsListRepository,
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiAccountListProducer {
@ -44,7 +45,7 @@ internal class DefaultMultiAccountListProducer @AssistedInject constructor(
.distinctUntilChanged()
.flatMapLatest { ids ->
combine(
flows = ids.map(walletAccountListFlowFactory::create),
flows = ids.map(singleAccountListSupplier::invoke),
transform = ::listOf,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.account.producer
import com.google.common.truth.Truth
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.domain.models.TokensSortType
@ -28,14 +29,14 @@ import org.junit.jupiter.api.TestInstance
class DefaultMultiAccountListProducerTest {
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk()
private val singleAccountListSupplier: SingleAccountListSupplier = mockk()
private val flowProducerTools: FlowProducerTools = mockk()
private val producer = DefaultMultiAccountListProducer(
params = Unit,
flowProducerTools = flowProducerTools,
userWalletsListRepository = userWalletsListRepository,
walletAccountListFlowFactory = walletAccountListFlowFactory,
singleAccountListSupplier = singleAccountListSupplier,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@ -46,7 +47,7 @@ class DefaultMultiAccountListProducerTest {
@AfterEach
fun tearDownEach() {
clearMocks(userWalletsListRepository, walletAccountListFlowFactory)
clearMocks(userWalletsListRepository, singleAccountListSupplier)
}
@Test
@ -56,7 +57,7 @@ class DefaultMultiAccountListProducerTest {
every { userWalletsListRepository.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWalletId)
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
every { singleAccountListSupplier.invoke(userWalletId) } returns flowOf(accountList)
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -68,7 +69,7 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -82,7 +83,7 @@ class DefaultMultiAccountListProducerTest {
val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull()
every { singleAccountListSupplier.invoke(userWalletId) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
@ -101,10 +102,10 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -117,7 +118,7 @@ class DefaultMultiAccountListProducerTest {
val accountList = AccountList.empty(userWalletId)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull()
every { singleAccountListSupplier.invoke(userWalletId) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
@ -136,10 +137,10 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -151,7 +152,7 @@ class DefaultMultiAccountListProducerTest {
every { userWalletsListRepository.userWallets } returns userWalletsFlow
val exception = RuntimeException("Converter error")
every { walletAccountListFlowFactory.create(userWalletId) } throws exception
every { singleAccountListSupplier.invoke(userWalletId) } throws exception
// Act
val actual = producer.produceWithFallback().let(::getEmittedValues)
@ -163,7 +164,7 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -183,7 +184,7 @@ class DefaultMultiAccountListProducerTest {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
}
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
coVerify(inverse = true) { singleAccountListSupplier.invoke(any<UserWalletId>()) }
}
@Test
@ -192,7 +193,7 @@ class DefaultMultiAccountListProducerTest {
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
every { walletAccountListFlowFactory.create(userWalletId) } returns emptyFlow()
every { singleAccountListSupplier.invoke(userWalletId) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -203,7 +204,7 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -219,8 +220,8 @@ class DefaultMultiAccountListProducerTest {
every { userWalletsListRepository.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWalletId)
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
every { walletAccountListFlowFactory.create(userWalletId2) } returns emptyFlow()
every { singleAccountListSupplier.invoke(userWalletId) } returns flowOf(accountList)
every { singleAccountListSupplier.invoke(userWalletId2) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -231,8 +232,8 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
walletAccountListFlowFactory.create(userWalletId2)
singleAccountListSupplier.invoke(userWalletId)
singleAccountListSupplier.invoke(userWalletId2)
}
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.OrderData
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.OnboardingRepository
@ -21,7 +22,11 @@ import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.security.isSecurityExposed
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import javax.inject.Inject
import kotlin.time.Duration.Companion.minutes
private const val TAG = "PaymentAccountStatusFetcher"
@ -147,53 +152,103 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
}
private suspend fun proceedWithOrderId(account: Account.Payment, orderId: String): PaymentAccountStatusValue {
// Step 1: Check KYC status first
val customerInfo = onboardingRepository.getCustomerInfo(account.userWalletId).fold(
ifLeft = { error ->
logger.e("proceedWithOrderId KYC check ${account.userWalletId} error: $error")
return error.mapToPaymentAccountStatus()
},
ifRight = { it },
)
logger.i("proceedWithOrderId ${account.userWalletId} kycStatus: ${customerInfo.kycStatus}")
when (customerInfo.kycStatus) {
KycStatus.PENDING,
KycStatus.INIT,
KycStatus.REJECTED,
-> return customerInfo.mapToPaymentAccountStatus(account.userWalletId)
KycStatus.APPROVED -> Unit // proceed to order check
}
// Step 2: Check order status
return customerOrderRepository.getOrderData(userWalletId = account.userWalletId, orderId = orderId).fold(
ifLeft = { error ->
logger.e("proceedWithOrderId ${account.userWalletId} orderId: $orderId error: $error")
error.mapToPaymentAccountStatus()
},
ifRight = { orderData ->
logger.i("proceedWithOrderId $account.userWalletId: $orderId status: ${orderData.status}")
logger.i("proceedWithOrderId ${account.userWalletId}: $orderId status: ${orderData.status}")
when (orderData.status) {
// Kyc is passed and user waits for order creation -> no need to get customer info
OrderStatus.CANCELED -> handleCanceledOrder(account, orderData)
OrderStatus.COMPLETED -> handleCompletedOrder(account)
OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable
OrderStatus.NEW,
OrderStatus.PROCESSING,
-> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
OrderStatus.CANCELED -> {
onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId)
.fold(
ifLeft = {
PaymentAccountStatusValue.Error.CardIssueFailed(
customerId = orderData.customerId,
)
},
ifRight = { customerInfo ->
if (customerInfo.kycStatus == KycStatus.REJECTED) {
customerInfo.mapToPaymentAccountStatus(account.userWalletId)
} else {
PaymentAccountStatusValue.Error.CardIssueFailed(
customerId = orderData.customerId,
)
}
},
)
-> {
paymentAccountStatusesStore.store(
userWalletId = account.userWalletId,
status = AccountStatus.Payment(
account = account,
value = PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL),
),
)
// Start polling for terminal state
pollOrderStatus(account = account, orderId = orderId)
}
OrderStatus.COMPLETED -> {
// Order was completed -> clear order id and get customer info
onboardingRepository.clearOrderId(account.userWalletId)
onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId)
.fold(
ifLeft = { it.mapToPaymentAccountStatus() },
ifRight = { it.mapToPaymentAccountStatus(account.userWalletId) },
)
}
OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable
}
},
)
}
private suspend fun pollOrderStatus(account: Account.Payment, orderId: String): PaymentAccountStatusValue {
while (currentCoroutineContext().isActive) {
delay(1.minutes)
val result = customerOrderRepository.getOrderData(
userWalletId = account.userWalletId,
orderId = orderId,
)
result.fold(
ifLeft = { error ->
logger.e("pollOrderStatus ${account.userWalletId} orderId: $orderId error: $error")
// Continue polling on transient errors
},
ifRight = { orderData ->
logger.i("pollOrderStatus ${account.userWalletId}: $orderId status: ${orderData.status}")
when (orderData.status) {
OrderStatus.CANCELED -> return handleCanceledOrder(account, orderData)
OrderStatus.COMPLETED -> return handleCompletedOrder(account)
OrderStatus.NEW,
OrderStatus.PROCESSING,
OrderStatus.UNKNOWN,
-> Unit // Continue polling
}
},
)
}
return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
}
private suspend fun handleCanceledOrder(
account: Account.Payment,
orderData: OrderData,
): PaymentAccountStatusValue {
onboardingRepository.clearOrderId(account.userWalletId)
return PaymentAccountStatusValue.Error.CardIssueFailed(orderData.customerId)
}
private suspend fun handleCompletedOrder(account: Account.Payment): PaymentAccountStatusValue {
onboardingRepository.clearOrderId(account.userWalletId)
return onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId)
.fold(
ifLeft = { it.mapToPaymentAccountStatus() },
ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus(account.userWalletId) },
)
}
private fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
val cardInfo = this.cardInfo
val productInstance = this.productInstance

View file

@ -41,8 +41,8 @@ data class AccountList private constructor(
get() = accounts.first { it is Account.CryptoPortfolio && it.isMainAccount } as Account.CryptoPortfolio
/** Returns true if more accounts can be added (the maximum number of accounts has not been reached) */
val canAddMoreAccounts: Boolean
get() = accounts.size < MAX_ACCOUNTS_COUNT
val canAddMoreCryptoAccounts: Boolean
get() = accounts.filterIsInstance<Account.CryptoPortfolio>().size < MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT
/** Returns the number of active accounts in the list */
val activeAccounts: Int
@ -102,7 +102,7 @@ data class AccountList private constructor(
return accounts.flatMap { account ->
when (account) {
is Account.CryptoPortfolio -> account.cryptoCurrencies
is Account.Payment -> TODO("[REDACTED_JIRA]")
is Account.Payment -> emptyList()
}
}
}
@ -151,6 +151,11 @@ data class AccountList private constructor(
override fun toString(): String = "$tag: The number of accounts must not exceed 20"
}
data object ExceedsMaxPaymentAccountsCount : Error {
override fun toString(): String =
"$tag: The number of payment accounts must not exceed $MAX_PAYMENT_ACCOUNTS_COUNT"
}
@Serializable
data object DuplicateAccountIds : Error {
override fun toString(): String = "$tag: Account list contains duplicate account IDs"
@ -169,7 +174,8 @@ data class AccountList private constructor(
companion object {
const val MAX_ACCOUNTS_COUNT = 20
const val MAX_PAYMENT_ACCOUNTS_COUNT = 1
const val MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT = 20
const val MAX_ARCHIVED_ACCOUNTS_COUNT = 1000
private const val MAX_MAIN_ACCOUNTS_COUNT = 1
@ -191,7 +197,11 @@ data class AccountList private constructor(
): Either<Error, AccountList> = either {
ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList }
ensure(accounts.size <= MAX_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount }
val paymentAccounts = accounts.filterIsInstance<Account.Payment>()
ensure(paymentAccounts.size <= MAX_PAYMENT_ACCOUNTS_COUNT) { Error.ExceedsMaxPaymentAccountsCount }
val cryptoAccounts = accounts.filterIsInstance<Account.CryptoPortfolio>()
ensure(cryptoAccounts.size <= MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount }
val mainAccountsCount = accounts.mainAccountsCount()
ensure(mainAccountsCount == MAX_MAIN_ACCOUNTS_COUNT) {

View file

@ -1,65 +1,35 @@
package com.tangem.domain.account.usecase
import arrow.core.Option
import arrow.core.getOrElse
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.loadAndGet
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/**
* Use case to determine if the accounts mode is enabled.
* Accounts mode is considered enabled if there are at least two accounts in any of the user wallets that support
* multiple currencies.
* Accounts mode is considered enabled if any [AccountList] produced for the user's wallets contains at least two
* active accounts.
*
* @property crudRepository repository to perform CRUD operations on accounts.
* @property userWalletsListRepository repository to get the list of user wallets.
* @property multiAccountListSupplier supplier that provides a list of [AccountList]s for all user wallets
*
[REDACTED_AUTHOR]
*/
class IsAccountsModeEnabledUseCase(
private val crudRepository: AccountsCRUDRepository,
private val userWalletsListRepository: UserWalletsListRepository,
private val multiAccountListSupplier: MultiAccountListSupplier,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(): Flow<Boolean> {
return userWalletsListRepository.loadAndGet()
.flatMapLatest { userWallets ->
val totalAccountsCountList = getTotalAccountsCountList(userWallets)
combine(flows = totalAccountsCountList) { it.toList().isModeEnabled() }
}
.onEmpty { emit(false) }
return multiAccountListSupplier.invoke()
.map { accountsList -> accountsList.map(AccountList::activeAccounts).isModeEnabled() }
.distinctUntilChanged()
}
suspend fun invokeSync(): Boolean {
return userWalletsListRepository.userWallets.value.orEmpty()
.map { userWallet ->
// If the wallet does not support multiple currencies, we consider its account count as 0
if (!userWallet.isMultiCurrency) return@map 0
crudRepository.getTotalActiveAccountsCountSync(userWalletId = userWallet.walletId).getOrZero()
}
.isModeEnabled()
return multiAccountListSupplier.getSyncOrNull(Unit)
?.map(AccountList::activeAccounts)
?.isModeEnabled() == true
}
private fun getTotalAccountsCountList(userWallets: List<UserWallet>): List<Flow<Int>> {
return userWallets
.map { userWallet ->
// If the wallet does not support multiple currencies, we consider its account count as 0
if (!userWallet.isMultiCurrency) return@map flowOf(0)
crudRepository.getTotalActiveAccountsCount(userWalletId = userWallet.walletId)
.map { maybeCount -> maybeCount.getOrZero() }
}
}
private fun Option<Int>.getOrZero(): Int = getOrElse { 0 }
private fun List<Int>.isModeEnabled(): Boolean = any { it >= 2 }
}

View file

@ -45,8 +45,8 @@ internal class AccountListTest {
val fullAccountList = MockAccounts.fullAccountList
// Act & Assert
Truth.assertThat(accountList.canAddMoreAccounts).isTrue()
Truth.assertThat(fullAccountList.canAddMoreAccounts).isFalse()
Truth.assertThat(accountList.canAddMoreCryptoAccounts).isTrue()
Truth.assertThat(fullAccountList.canAddMoreCryptoAccounts).isFalse()
}
@Test

View file

@ -1,15 +1,12 @@
package com.tangem.domain.account.usecase
import arrow.core.none
import arrow.core.some
import com.google.common.truth.Truth
import com.tangem.domain.account.repository.AccountsCRUDRepository
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.models.wallet.isMultiCurrency
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@ -18,21 +15,16 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@Suppress("UnusedFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class IsAccountsModeEnabledUseCaseTest {
private val accountsCRUDRepository: AccountsCRUDRepository = mockk()
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val multiAccountListSupplier: MultiAccountListSupplier = mockk()
private val useCase = IsAccountsModeEnabledUseCase(
crudRepository = accountsCRUDRepository,
userWalletsListRepository = userWalletsListRepository,
)
private val useCase = IsAccountsModeEnabledUseCase(multiAccountListSupplier = multiAccountListSupplier)
@AfterEach
fun tearDown() {
clearMocks(userWalletsListRepository, accountsCRUDRepository)
clearMocks(multiAccountListSupplier)
}
@Nested
@ -40,90 +32,55 @@ class IsAccountsModeEnabledUseCaseTest {
inner class Invoke {
@Test
fun `returns false when loadAndGet emits one wallet with isMultiCurrency false`() = runTest {
fun `returns false when supplier emits empty list`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = false)
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
every { multiAccountListSupplier.invoke() } returns flowOf(emptyList())
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isFalse()
coVerifyOrder {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
}
verify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCount(any()) }
}
@Test
fun `returns true when loadAndGet emits one wallet with isMultiCurrency true`() = runTest {
fun `returns false when supplier emits account list with one account`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(2.some())
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isTrue()
coVerifyOrder {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId)
}
}
@Test
fun `returns false when loadAndGet emits one wallet with isMultiCurrency true and None counts`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(none())
val accountList = createAccountList(activeAccounts = 1)
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountList))
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isFalse()
coVerifyOrder {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId)
}
}
@Test
fun `returns true when loadAndGet emits two wallets, one isMultiCurrency false, one true`() = runTest {
fun `returns true when supplier emits account list with two accounts`() = runTest {
// Arrange
val wallet1 = createUserWallet(isMultiCurrency = false)
val wallet2 = createUserWallet(isMultiCurrency = true)
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet1, wallet2))
every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } returns flowOf(2.some())
val accountList = createAccountList(activeAccounts = 2)
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountList))
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isTrue()
}
coVerifyOrder {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId)
}
@Test
fun `returns true when supplier emits multiple account lists, one with two accounts`() = runTest {
// Arrange
val accountList1 = createAccountList(activeAccounts = 1)
val accountList2 = createAccountList(activeAccounts = 2)
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountList1, accountList2))
verify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCount(wallet1.walletId) }
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isTrue()
}
}
@ -132,109 +89,71 @@ class IsAccountsModeEnabledUseCaseTest {
inner class InvokeSync {
@Test
fun `returns false when getUserWalletsSync returns empty list`() = runTest {
fun `returns false when getSyncOrNull returns null`() = runTest {
// Arrange
every { userWalletsListRepository.userWallets.value } returns emptyList()
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns null
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
verifyOrder {
userWalletsListRepository.userWallets.value
}
coVerify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCountSync(any()) }
}
@Test
fun `returns false when getUserWalletsSync returns one wallet with isMultiCurrency false`() = runTest {
fun `returns false when getSyncOrNull returns empty list`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = false)
every { userWalletsListRepository.userWallets.value } returns listOf(wallet)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns emptyList()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
verifyOrder {
userWalletsListRepository.userWallets.value
}
coVerify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCountSync(any()) }
}
@Test
fun `returns true when getUserWalletsSync returns one wallet with isMultiCurrency true`() = runTest {
fun `returns false when getSyncOrNull returns account list with one account`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
val accountList = createAccountList(activeAccounts = 1)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(accountList)
every { userWalletsListRepository.userWallets.value } returns listOf(wallet)
coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns 2.some()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns true when getSyncOrNull returns account list with two accounts`() = runTest {
// Arrange
val accountList = createAccountList(activeAccounts = 2)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(accountList)
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isTrue()
coVerifyOrder {
userWalletsListRepository.userWallets.value
accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId)
}
}
@Test
fun `returns false when getUserWalletsSync returns multi wallet with None counts`() = runTest {
fun `returns true when getSyncOrNull returns multiple account lists, one with two accounts`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { userWalletsListRepository.userWallets.value } returns listOf(wallet)
coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns none()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
coVerifyOrder {
userWalletsListRepository.userWallets.value
accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId)
}
}
@Test
fun `returns true when getUserWalletsSync returns multi and single wallets`() = runTest {
// Arrange
val wallet1 = createUserWallet(isMultiCurrency = false)
val wallet2 = createUserWallet(isMultiCurrency = true)
every { userWalletsListRepository.userWallets.value } returns listOf(wallet1, wallet2)
coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId) } returns 2.some()
val accountList1 = createAccountList(activeAccounts = 1)
val accountList2 = createAccountList(activeAccounts = 2)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(accountList1, accountList2)
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isTrue()
coVerifyOrder {
userWalletsListRepository.userWallets.value
accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId)
}
coVerify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet1.walletId) }
}
}
private fun createUserWallet(isMultiCurrency: Boolean): UserWallet = mockk {
every { this@mockk.walletId } returns UserWalletId(stringValue = "011")
every { this@mockk.isMultiCurrency } returns isMultiCurrency
private fun createAccountList(activeAccounts: Int): AccountList = mockk {
every { this@mockk.activeAccounts } returns activeAccounts
}
}

View file

@ -363,7 +363,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
private fun createLoadingAccountStatusList(accountList: AccountList): AccountStatusList {
return AccountStatusList(
userWalletId = accountList.userWalletId,
accountStatuses = accountList.accounts.map { account ->
accountStatuses = accountList.accounts.mapNotNull { account ->
when (account) {
is Account.CryptoPortfolio -> {
val currencyStatuses = account.cryptoCurrencies.map {
@ -380,7 +380,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
priceChangeLce = lceLoading(),
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
is Account.Payment -> null
}
},
totalAccounts = accountList.totalAccounts,

View file

@ -46,7 +46,7 @@ class RecoverCryptoPortfolioUseCase(
val accountList = getAccountList(userWalletId = accountId.userWalletId)
ensure(accountList.canAddMoreAccounts) {
ensure(accountList.canAddMoreCryptoAccounts) {
raise(Error.AccountListRequirementsNotMet(cause = AccountList.Error.ExceedsMaxAccountsCount))
}

View file

@ -3,7 +3,7 @@ package com.tangem.domain.nft.models
import com.tangem.domain.models.account.Account
data class WalletNFTCollections(
val collections: Map<Account, List<NFTCollections>>,
val collections: Map<Account.CryptoPortfolio, List<NFTCollections>>,
) {
val flattenCollections by lazy { collections.values.flatten() }
}

View file

@ -18,14 +18,18 @@ class GetNFTCollectionsUseCase(
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(userWalletId: UserWalletId): Flow<WalletNFTCollections> {
return singleAccountListSupplier(userWalletId)
.mapLatest { statusList -> statusList.accounts.mapNotNull(::flowOfNFTCollections) }
.mapLatest { statusList ->
statusList.accounts.filterIsInstance<Account.CryptoPortfolio>().mapNotNull(::flowOfNFTCollections)
}
.flatMapLatest { flows ->
combine(flows) { WalletNFTCollections(it.toMap()) }
}
}
private fun flowOfNFTCollections(account: Account): Flow<Pair<Account, List<NFTCollections>>>? {
val currencies = (account as? Account.CryptoPortfolio)?.cryptoCurrencies.orEmpty()
private fun flowOfNFTCollections(
account: Account.CryptoPortfolio,
): Flow<Pair<Account.CryptoPortfolio, List<NFTCollections>>>? {
val currencies = account.cryptoCurrencies
if (currencies.isEmpty()) return null

View file

@ -160,33 +160,30 @@ class WalletBalanceFetcher internal constructor(
paymentAccountRefactorEnabled: Boolean,
) {
coroutineScope {
val errorDeferreds = fetchingSources.map { source ->
async {
when (source) {
is WalletFetchingSource.Balance -> {
balanceFetchingOperations.fetchAll(
userWalletId = userWalletId,
currencies = currencies,
sources = source.sources,
).mapKeys { (fetchingSource, _) -> fetchingSource.name }
}
is WalletFetchingSource.TangemPay -> {
fetchPaymentAccount(userWalletId, paymentAccountRefactorEnabled)
.leftOrNull()
?.let { error -> mapOf(FetchErrorFormatter.TANGEM_PAY_SOURCE_NAME to error) }
.orEmpty()
}
// Fetch balance sources in parallel
val balanceErrors = fetchingSources.filterIsInstance<WalletFetchingSource.Balance>()
.map { source ->
async {
balanceFetchingOperations.fetchAll(
userWalletId = userWalletId,
currencies = currencies,
sources = source.sources,
).mapKeys { (fetchingSource, _) -> fetchingSource.name }
}
}
}
.awaitAll()
.fold(emptyMap<String, Throwable>()) { acc, map -> acc + map }
val errors = errorDeferreds.awaitAll().fold(emptyMap<String, Throwable>()) { acc, map -> acc + map }
check(errors.isEmpty()) {
val message = FetchErrorFormatter.formatWalletErrors(userWalletId, errors)
check(balanceErrors.isEmpty()) {
val message = FetchErrorFormatter.formatWalletErrors(userWalletId, balanceErrors)
TangemLogger.e(message)
message
}
// Fetch TangemPay separately — may run long-polling, so it must not block balance error checking
if (fetchingSources.any { it is WalletFetchingSource.TangemPay }) {
fetchPaymentAccount(userWalletId, paymentAccountRefactorEnabled)
}
}
}

View file

@ -14,7 +14,7 @@ sealed class WalletFetchingSource {
/**
* TangemPay account fetching source.
* Handled separately from standard balance sources via [PaymentAccountStatusFetcher].
* Handled separately from standard balance sources via [com.tangem.domain.pay.flow.PaymentAccountStatusFetcher].
*/
data object TangemPay : WalletFetchingSource()

View file

@ -0,0 +1,20 @@
package com.tangem.domain.transaction.usecase
import com.tangem.domain.models.network.Network
import com.tangem.domain.transaction.WalletAddressServiceRepository
class IsMemoRequiredUseCase(
private val walletAddressServiceRepository: WalletAddressServiceRepository,
) {
suspend operator fun invoke(network: Network, destinationAddress: String): Boolean {
return try {
walletAddressServiceRepository.isMemoRequired(
network = network,
destinationAddress = destinationAddress,
)
} catch (_: Throwable) {
false
}
}
}

View file

@ -33,7 +33,6 @@ import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents.Compa
import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents
import com.tangem.features.account.createedit.entity.AccountCreateEditUM
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.toggleProgress
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateButton
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateColorSelect
@ -173,7 +172,7 @@ internal class AccountCreateEditModel @Inject constructor(
val name = state.account.name.toDomain().getOrNull() ?: return
val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon)
val isNewName = name != params.account.accountName
val isNewIcon = icon != params.account.portfolioIcon
val isNewIcon = icon != params.account.icon
val derivationIndex = params.account.derivationIndex.value
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon, derivationIndex))
@ -258,7 +257,7 @@ internal class AccountCreateEditModel @Inject constructor(
is AccountCreateEditComponent.Params.Create -> isValidName
is AccountCreateEditComponent.Params.Edit -> {
val oldName = params.account.accountName.toUM()
val oldIcon = CryptoPortfolioIconConverter.convert(params.account.portfolioIcon)
val oldIcon = CryptoPortfolioIconConverter.convert(params.account.icon)
val isNewName = this.account.name.trim() != oldName
val isNewIcon = this.account.portfolioIcon != oldIcon

View file

@ -7,7 +7,6 @@ import com.tangem.core.res.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.features.account.AccountCreateEditComponent
import kotlinx.collections.immutable.toImmutableList
@ -37,10 +36,8 @@ internal class AccountCreateEditUMBuilder(
)
is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account(
name = params.account.accountName.toUM(),
portfolioIcon = CryptoPortfolioIconConverter.convert(params.account.portfolioIcon),
derivationInfo = createAccountDerivationInfo(
index = (params.account as Account.CryptoPortfolio).derivationIndex.value,
),
portfolioIcon = CryptoPortfolioIconConverter.convert(params.account.icon),
derivationInfo = createAccountDerivationInfo(index = params.account.derivationIndex.value),
inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account),
onNameChange = onNameChange,
)
@ -50,7 +47,7 @@ internal class AccountCreateEditUMBuilder(
fun initColorsUM(onColorSelect: (CryptoPortfolioIcon.Color) -> Unit): AccountCreateEditUM.Colors {
val selected: CryptoPortfolioIcon.Color = when (params) {
is AccountCreateEditComponent.Params.Create -> createIcon.color
is AccountCreateEditComponent.Params.Edit -> params.account.portfolioIcon.color
is AccountCreateEditComponent.Params.Edit -> params.account.icon.color
}
return AccountCreateEditUM.Colors(
selected = selected,
@ -62,7 +59,7 @@ internal class AccountCreateEditUMBuilder(
fun initIconsUM(onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit): AccountCreateEditUM.Icons {
val selected: CryptoPortfolioIcon.Icon = when (params) {
is AccountCreateEditComponent.Params.Create -> createIcon.value
is AccountCreateEditComponent.Params.Edit -> params.account.portfolioIcon.value
is AccountCreateEditComponent.Params.Edit -> params.account.icon.value
}
return AccountCreateEditUM.Icons(
selected = selected,
@ -85,13 +82,6 @@ internal class AccountCreateEditUMBuilder(
}
internal companion object {
val Account.portfolioIcon: CryptoPortfolioIcon
get() = when (this) {
is Account.CryptoPortfolio -> this.icon
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
fun AccountCreateEditUM.updateColorSelect(color: CryptoPortfolioIcon.Color): AccountCreateEditUM {
val newIcon = this.account.portfolioIcon.copy(
color = color,

View file

@ -21,7 +21,6 @@ import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
import com.tangem.features.account.details.entity.AccountDetailsUM
import com.tangem.features.account.details.entity.AccountDetailsUM.ArchiveMode
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -164,7 +163,7 @@ internal class AccountDetailsModel @Inject constructor(
?.isMultiCurrency == true
return AccountDetailsUM(
accountName = account.accountName.toUM().value,
accountIcon = CryptoPortfolioIconConverter.convert(account.portfolioIcon),
accountIcon = CryptoPortfolioIconConverter.convert(account.icon),
onCloseClick = { router.pop() },
onAccountEditClick = { onEditAccountClick(account) },
onManageTokensClick = { onManageTokensClick(account) },

View file

@ -28,7 +28,7 @@ data class AvailableToAddWallet(
@Serializable
data class AvailableToAddAccount(
val account: AccountStatus,
val account: AccountStatus.CryptoPortfolio,
val availableNetworks: Set<TokenMarketInfo.Network>,
val addedNetworks: Set<Network>,
) {

View file

@ -16,7 +16,6 @@ import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -325,10 +324,7 @@ internal class AddToPortfolioModel @Inject constructor(
network: TokenMarketInfo.Network,
account: AvailableToAddAccount,
): CryptoCurrency? {
val accountIndex = when (val accountStatus = account.account) {
is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex
is AccountStatus.Payment -> TODO("[REDACTED_JIRA]")
}
val accountIndex = account.account.account.derivationIndex
return getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = addToPortfolioManager.token,

View file

@ -11,7 +11,6 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenUiBuilder.Companion.toggleProgress
@ -105,11 +104,8 @@ internal class AddTokenModel @Inject constructor(
if (status == null) {
processError(error = null)
} else {
when (account) {
is Account.CryptoPortfolio -> if (!account.isMainAccount) {
analyticsEventHandler.send(analyticsEventBuilder.addToNotMainAccount())
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
if (!account.isMainAccount) {
analyticsEventHandler.send(analyticsEventBuilder.addToNotMainAccount())
}
analyticsEventHandler.send(

View file

@ -276,15 +276,12 @@ internal class MarketsPortfolioDelegate @AssistedInject constructor(
)
}
private fun Account.toAccountPortfolioHeader(): PortfolioHeader = PortfolioHeader(
private fun Account.CryptoPortfolio.toAccountPortfolioHeader(): PortfolioHeader = PortfolioHeader(
id = this.accountId.value,
state = AccountTitleUM.Account(
prefixText = TextReference.EMPTY,
name = this.accountName.toUM().value,
icon = when (this) {
is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(this.icon)
is Account.Payment -> TODO("[REDACTED_JIRA]")
},
icon = CryptoPortfolioIconConverter.convert(this.icon),
),
)
@ -335,7 +332,7 @@ private data class Portfolio(
private data class AccountWithAdded(
val addedCurrency: List<CryptoCurrencyStatus>,
val accountStatus: AccountStatus,
val accountStatus: AccountStatus.CryptoPortfolio,
)
private data class SettingsBox(

View file

@ -9,6 +9,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@ -54,6 +55,7 @@ internal class NewsDetailsModel @Inject constructor(
private val markArticleAsViewedUseCase: MarkArticleAsViewedUseCase,
private val toggleArticleLikedUseCase: ToggleArticleLikedUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val designFeatureToggles: DesignFeatureToggles,
paramsContainer: ParamsContainer,
) : Model() {
@ -71,6 +73,7 @@ internal class NewsDetailsModel @Inject constructor(
dispatchers = dispatchers,
observeNewsDetailsUseCase = observeNewsDetailsUseCase,
prefetchedIds = params.preselectedArticlesId.toSet(),
isRedesignEnabled = designFeatureToggles.isRedesignEnabled,
)
}

View file

@ -20,12 +20,14 @@ internal class NewsDetailsPaginationManager(
currentCategoryIds: Provider<List<Int>>,
modelScope: CoroutineScope,
prefetchedIds: Set<Int>,
isRedesignEnabled: Boolean,
) : NewsListBatchFlowManager(
getNewsListBatchFlowUseCase = getNewsListBatchFlowUseCase,
currentLanguage = currentLanguage,
currentCategoryIds = currentCategoryIds,
modelScope = modelScope,
dispatchers = dispatchers,
isRedesignEnabled = isRedesignEnabled,
) {
private val _cachedPrefetchedIds = MutableStateFlow(prefetchedIds)

View file

@ -4,6 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.components.chip.entity.ChipUM
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase
@ -34,6 +35,7 @@ internal class NewsListModel @Inject constructor(
private val getNewsCategoriesUseCase: GetNewsCategoriesUseCase,
private val getNewsListBatchFlowUseCase: GetNewsListBatchFlowUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val designFeatureToggles: DesignFeatureToggles,
paramsContainer: ParamsContainer,
) : Model() {
@ -58,6 +60,7 @@ internal class NewsListModel @Inject constructor(
},
modelScope = modelScope,
dispatchers = dispatchers,
isRedesignEnabled = designFeatureToggles.isRedesignEnabled,
)
}

View file

@ -22,6 +22,7 @@ import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal open class NewsListBatchFlowManager(
private val isRedesignEnabled: Boolean,
getNewsListBatchFlowUseCase: GetNewsListBatchFlowUseCase,
private val currentLanguage: Provider<String>,
private val currentCategoryIds: Provider<List<Int>>,
@ -30,7 +31,13 @@ internal open class NewsListBatchFlowManager(
) {
private val actionsFlow = MutableSharedFlow<BatchAction<Int, NewsListConfig, Nothing>>()
private val converter by lazy {
ShortArticleToArticleConfigUMConverter(null)
ShortArticleToArticleConfigUMConverter(
isTrending = if (isRedesignEnabled) {
null
} else {
false
},
)
}
private val batchFlow = getNewsListBatchFlowUseCase(

View file

@ -17,8 +17,8 @@ import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.managetokens.GetSupportedNetworksUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.AddCustomTokenMode
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
@ -214,23 +214,14 @@ internal class CustomTokenSelectorModel @Inject constructor(
this.account.derivationIndex.value.toLong() == accountNode
val accounts = singleAccountStatusListSupplier(mode.userWalletId)
.first().accountStatuses
.first().accountStatuses.filterCryptoPortfolio()
val accountStatus = accounts.find { account ->
when (account) {
is AccountStatus.CryptoPortfolio -> account.sameNodeAndNotMain()
is AccountStatus.Payment -> TODO("[REDACTED_JIRA]")
}
}
accountStatus?.account
accounts
.find { account -> account.sameNodeAndNotMain() }
?.account
}
val accountName = when (account) {
is Account.CryptoPortfolio -> account.accountName
is Account.Payment -> TODO("[REDACTED_JIRA]")
null -> null
}
val accountName = account?.accountName
if (accountName == null) {
onDerivationPathSelected(derivationPath, null)

View file

@ -95,15 +95,12 @@ internal class UpdateDataStateTransformer(
createNFTsUM(mainAccountCollection).toPersistentList()
}
private fun Account.toAccountPortfolioUM(): NFTCollectionPortfolioUM = NFTCollectionPortfolioUM(
private fun Account.CryptoPortfolio.toAccountPortfolioUM(): NFTCollectionPortfolioUM = NFTCollectionPortfolioUM(
id = this.accountId.value,
title = AccountTitleUM.Account(
prefixText = TextReference.EMPTY,
name = this.accountName.toUM().value,
icon = when (this) {
is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(this.icon)
is Account.Payment -> TODO("[REDACTED_JIRA]")
},
icon = CryptoPortfolioIconConverter.convert(this.icon),
),
)

View file

@ -8,6 +8,7 @@ import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -42,14 +43,10 @@ internal class HotCryptoPortfolioDataLoader @Inject constructor(
.invokeSync(userWalletId, hotCryptoCurrencies)
.getOrNull()
.orEmpty()
val accountsWithHotCrypto = walletAccounts.accountStatuses.map { accountStatus ->
val account: AccountStatus.CryptoPortfolio = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus
is AccountStatus.Payment -> TODO("[REDACTED_JIRA]")
}
val addedHotCrypto = mapOfAddedCurrencies[account.account].orEmpty()
val accountsWithHotCrypto = walletAccounts.accountStatuses.filterCryptoPortfolio().map { accountStatus ->
val addedHotCrypto = mapOfAddedCurrencies[accountStatus.account].orEmpty()
HotCryptoPortfolioData.Account(
account = account,
account = accountStatus,
addedHotCrypto = addedHotCrypto,
)
}

View file

@ -247,20 +247,16 @@ internal class AvailableSwapPairsModel @Inject constructor(
val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding
val filterByQueryAccountList: Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>> = accountList
.filterCryptoPortfolio()
.associate { accountStatus ->
when (accountStatus) {
is AccountStatus.CryptoPortfolio -> {
val statuses = accountStatus.tokenList.flattenCurrencies()
.filterNot { status ->
status.currency.network.rawId == selectedStatus?.currency?.network?.rawId &&
status.currency.id.contractAddress == selectedStatus.currency.id.contractAddress
}
.filterByQuery(query = query)
accountStatus.account to statuses
val statuses = accountStatus.tokenList.flattenCurrencies()
.filterNot { status ->
status.currency.network.rawId == selectedStatus?.currency?.network?.rawId &&
status.currency.id.contractAddress == selectedStatus.currency.id.contractAddress
}
is AccountStatus.Payment -> TODO("[REDACTED_JIRA]")
}
.filterByQuery(query = query)
accountStatus.account to statuses
}
.filterValues { it.isNotEmpty() }

View file

@ -21,12 +21,9 @@ internal class SetLoadingAccountTokenListTransformer(
private val accountListItemConverter = LoadingAccountTokenItemConverter(appCurrency)
override fun transform(prevState: TokenListUM): TokenListUM {
val totalTokensCount = accountList.sumOf { account ->
when (account) {
is AccountStatus.CryptoPortfolio -> account.tokenList.flattenCurrencies().size
is AccountStatus.Payment -> TODO("[REDACTED_JIRA]")
}
}
val totalTokensCount = accountList
.filterCryptoPortfolio()
.sumOf { account -> account.tokenList.flattenCurrencies().size }
return prevState.copy(
availableItems = persistentListOf(),
@ -40,13 +37,10 @@ internal class SetLoadingAccountTokenListTransformer(
)
} else {
TokenListUMData.TokenList(
tokensList = accountList.flatMap { account ->
when (account) {
is AccountStatus.CryptoPortfolio -> LoadingTokenListItemConverter.convertList(
account.tokenList.flattenCurrencies().map(CryptoCurrencyStatus::currency),
)
is AccountStatus.Payment -> TODO("[REDACTED_JIRA]")
}
tokensList = accountList.filterCryptoPortfolio().flatMap { account ->
LoadingTokenListItemConverter.convertList(
account.tokenList.flattenCurrencies().map(CryptoCurrencyStatus::currency),
)
}.toPersistentList(),
totalTokensCount = totalTokensCount,
)

View file

@ -18,7 +18,6 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
@ -240,13 +239,8 @@ internal class OnrampTokenListModel @Inject constructor(
): Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>> = accountStatuses.asSequence()
.filterCryptoPortfolio()
.associate { accountStatus ->
when (accountStatus) {
is AccountStatus.CryptoPortfolio -> {
val filteredList = accountStatus.tokenList.flattenCurrencies().filterByQuery(query = query)
accountStatus.account to filteredList
}
is AccountStatus.Payment -> TODO("[REDACTED_JIRA]")
}
val filteredList = accountStatus.tokenList.flattenCurrencies().filterByQuery(query = query)
accountStatus.account to filteredList
}.filter { (_, value) -> value.isNotEmpty() }
private fun List<CryptoCurrencyStatus>.filterByQuery(query: String): List<CryptoCurrencyStatus> {

View file

@ -4,9 +4,9 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.account.derivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -40,20 +40,13 @@ internal class ReferralInteractorImpl(
val account = singleAccountSupplier.getSyncOrNull(
params = SingleAccountProducer.Params(accountId = accountId),
)
?: error("Account not found: $accountId")
val accountIndex = when (account) {
is Account.CryptoPortfolio -> account.derivationIndex
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
) ?: error("Account not found: $accountId")
val cryptoCurrency = getCryptoCurrency(
userWalletId = accountId.userWalletId,
tokenData = tokenData,
accountIndex = accountIndex,
)
?: error("Failed to create crypto currency")
accountIndex = account.derivationIndex,
) ?: error("Failed to create crypto currency")
manageCryptoCurrenciesUseCase(
accountId = accountId,

View file

@ -33,7 +33,7 @@ interface SendNotificationsComponent {
val callback: ModelCallback,
) {
data class NotificationData(
val destinationAddress: String,
val destinationAddress: String?,
val memo: String?,
val amountValue: BigDecimal,
val reduceAmountBy: BigDecimal,

View file

@ -34,7 +34,6 @@ import com.tangem.domain.tokens.GetCurrencyCheckUseCase
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.features.send.v2.api.SendNotificationsComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
@ -69,7 +68,6 @@ internal class NotificationsModel @Inject constructor(
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
private val validateTransactionUseCase: ValidateTransactionUseCase,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
private val getTronFeeNotificationShowCountUseCase: GetTronFeeNotificationShowCountUseCase,
private val incrementNotificationsShowCountUseCase: IncrementNotificationsShowCountUseCase,
private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger,
@ -302,7 +300,7 @@ internal class NotificationsModel @Inject constructor(
}
private suspend fun MutableList<NotificationUM>.addWarningNotifications(
destinationAddress: String,
destinationAddress: String?,
memo: String?,
enteredAmount: BigDecimal,
sendingAmount: BigDecimal,
@ -311,14 +309,18 @@ internal class NotificationsModel @Inject constructor(
isFeeCoverage: Boolean,
currencyCheck: CryptoCurrencyCheck,
) {
val validationError = validateTransactionUseCase(
userWalletId = userWalletId,
amount = enteredAmount.convertToSdkAmount(cryptoCurrencyStatus),
fee = fee,
memo = memo,
destination = destinationAddress,
network = cryptoCurrencyStatus.currency.network,
).leftOrNull()
val validationError = if (destinationAddress != null) {
validateTransactionUseCase(
userWalletId = userWalletId,
amount = enteredAmount.convertToSdkAmount(cryptoCurrencyStatus),
fee = fee,
memo = memo,
destination = destinationAddress,
network = cryptoCurrencyStatus.currency.network,
).leftOrNull()
} else {
null
}
addRentExemptionNotification(
rentWarning = currencyCheck.rentWarning,
@ -352,10 +354,6 @@ internal class NotificationsModel @Inject constructor(
params.callback.onAmountReduceTo(reduceTo)
},
)
addDestinationTagRequiredNotification(
isMemoRequired = currencyCheck.isMemoRequired,
memo = memo,
)
addHighFeeWarningNotification(
enteredAmountValue = enteredAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus,
@ -376,21 +374,6 @@ internal class NotificationsModel @Inject constructor(
addTronNetworkFeesNotification()
}
private suspend fun MutableList<NotificationUM>.addDestinationTagRequiredNotification(
isMemoRequired: Boolean,
memo: String?,
) {
if (!isMemoRequired || contains(NotificationUM.Error.DestinationTagRequired)) return
val isMemoInvalid = memo.isNullOrEmpty() || validateWalletMemoUseCase(
userWalletId = userWalletId,
cryptoCurrency = currency,
memo = memo,
).isLeft()
if (isMemoInvalid) {
add(NotificationUM.Error.DestinationTagRequired)
}
}
private suspend fun MutableList<NotificationUM>.addTronNetworkFeesNotification() {
val cryptoCurrency = cryptoCurrencyStatus.currency
val isTronToken = cryptoCurrency is CryptoCurrency.Token &&

View file

@ -215,13 +215,13 @@ internal class SwapAmountModel @Inject constructor(
}
}
uiState.update { amountUM ->
if (amountUM !is SwapAmountUM.Content) return@update amountUM
amountUM.copy(
uiState.transformerUpdate(
SwapAmountChangeAmountTypeTransformer(
selectedAmountType = selectedAmountType,
swapRateType = newSwapRateType,
)
}
isBalanceHidden = params.isBalanceHidingFlow.value,
),
)
startLoadingQuotesTask(isSilentReload = false)
}

View file

@ -0,0 +1,51 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.swap.models.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountUpdateSubtitleConverter
import com.tangem.utils.transformer.Transformer
internal class SwapAmountChangeAmountTypeTransformer(
private val selectedAmountType: SwapAmountType,
private val swapRateType: ExpressRateType,
private val isBalanceHidden: Boolean,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
val subtitleConverter = SwapAmountUpdateSubtitleConverter(
selectedAmountType = selectedAmountType,
isBalanceHidden = isBalanceHidden,
)
val newPrimaryAmount = (prevState.primaryAmount as? SwapAmountFieldUM.Content)?.let { field ->
subtitleConverter.updateSubtitles(
field = field,
cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus,
isAmountEmpty = true,
)
} ?: prevState.primaryAmount
val newSecondaryAmount = if (prevState.secondaryCryptoCurrencyStatus != null) {
(prevState.secondaryAmount as? SwapAmountFieldUM.Content)?.let { field ->
subtitleConverter.updateSubtitles(
field = field,
cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus,
isAmountEmpty = true,
)
} ?: prevState.secondaryAmount
} else {
prevState.secondaryAmount
}
return prevState.copy(
selectedAmountType = selectedAmountType,
swapRateType = swapRateType,
primaryAmount = newPrimaryAmount,
secondaryAmount = newSecondaryAmount,
)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
@ -51,6 +52,7 @@ internal class SwapNotificationsComponent(
val enteredFromAmount: BigDecimal? = null,
val fromCryptoCurrencyStatus: CryptoCurrencyStatus? = null,
val priceImpact: PriceImpact? = null,
val provider: ExpressProvider? = null,
)
}
}

View file

@ -1,15 +1,14 @@
package com.tangem.features.swap.v2.impl.notifications.model
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.transaction.usecase.IsMemoRequiredUseCase
import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger
import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact
import com.tangem.features.swap.v2.impl.notifications.DefaultSwapNotificationsUpdateTrigger
@ -17,6 +16,7 @@ import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent.Params.SwapNotificationData
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener
import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -26,16 +26,17 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class SwapNotificationsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val swapNotificationsUpdateListener: SwapNotificationsUpdateListener,
private val swapNotificationsUpdateTrigger: DefaultSwapNotificationsUpdateTrigger,
private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger,
private val validateTransactionUseCase: ValidateTransactionUseCase,
private val isMemoRequiredUseCase: IsMemoRequiredUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
) : Model() {
@ -80,24 +81,50 @@ internal class SwapNotificationsModel @Inject constructor(
.isNotEmpty()
swapNotificationsUpdateTrigger.callbackHasError(hasErrorNotification)
uiState.value = notifications.toImmutableList()
val fromCurrency = notificationData.fromCryptoCurrency
val toCurrency = notificationData.toCryptoCurrencyStatus?.currency
val provider = notificationData.provider
if (fromCurrency != null && toCurrency != null && provider != null) {
if (notifications.any { it is SwapNotificationUM.Warning.HighPriceImpact }) {
analyticsEventHandler.send(
SendWithSwapAnalyticEvents.HighPriceImpact(
sendToken = fromCurrency.symbol,
receiveToken = toCurrency.symbol,
sendBlockchain = fromCurrency.network.name,
receiveBlockchain = toCurrency.network.name,
providerName = provider.name,
),
)
}
if (notifications.any { it is SwapNotificationUM.Warning.TradeTooHigh }) {
analyticsEventHandler.send(
SendWithSwapAnalyticEvents.TradeTooLarge(
sendToken = fromCurrency.symbol,
receiveToken = toCurrency.symbol,
sendBlockchain = fromCurrency.network.name,
receiveBlockchain = toCurrency.network.name,
providerName = provider.name,
),
)
}
}
}
private suspend fun MutableList<NotificationUM>.addDestinationTagRequiredNotification() {
val toCryptoCurrencyStatus = notificationData.toCryptoCurrencyStatus ?: return
val userWalletId = notificationData.userWalletId ?: return
val destinationAddress = notificationData.destinationAddress
if (destinationAddress.isEmpty()) return
val validationError = validateTransactionUseCase(
amount = BigDecimal.ZERO.convertToSdkAmount(toCryptoCurrencyStatus),
fee = null,
memo = notificationData.memo,
destination = destinationAddress,
userWalletId = userWalletId,
network = toCryptoCurrencyStatus.currency.network,
).leftOrNull()
if (validationError is BlockchainSdkError.DestinationTagRequired) {
val isMemoRequired = if (notificationData.memo.isNullOrEmpty()) {
isMemoRequiredUseCase(
network = toCryptoCurrencyStatus.currency.network,
destinationAddress = destinationAddress,
)
} else {
false
}
if (isMemoRequired) {
add(NotificationUM.Error.DestinationTagRequired)
}
}

View file

@ -133,6 +133,41 @@ internal sealed class SendWithSwapAnalyticEvents(
),
)
class HighPriceImpact(
val sendToken: String,
val receiveToken: String,
val sendBlockchain: String,
val receiveBlockchain: String,
val providerName: String,
) : SendWithSwapAnalyticEvents(
event = "Notice - High price impact",
params = mapOf(
SEND_TOKEN to sendToken,
RECEIVE_TOKEN to receiveToken,
"Send Blockchain" to sendBlockchain,
"Receive Blockchain" to receiveBlockchain,
PROVIDER to providerName,
),
)
class TradeTooLarge(
val sendToken: String,
val receiveToken: String,
val sendBlockchain: String,
val receiveBlockchain: String,
val providerName: String,
) : SendWithSwapAnalyticEvents(
event = "Notice - Trade too large",
params = mapOf(
SEND_TOKEN to sendToken,
RECEIVE_TOKEN to receiveToken,
"Send Blockchain" to sendBlockchain,
"Receive Blockchain" to receiveBlockchain,
PROVIDER to providerName,
),
)
enum class ErrorScreen {
Amount,
Confirm,

View file

@ -11,7 +11,6 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.swap.models.SwapDirection
@ -113,10 +112,11 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
appCurrency = params.appCurrency,
callback = model,
notificationData = SendNotificationsComponent.Params.NotificationData(
destinationAddress = when (val currency = model.primaryCurrencyStatus.currency) {
is CryptoCurrency.Token -> currency.contractAddress
is CryptoCurrency.Coin -> "0"
},
/**
* Null when destination is unknown at this point (e.g. CEX swap address is only known
* after receiving exchange-data). For DEX / DEX_BRIDGE, the address is known upfront.
*/
destinationAddress = null,
memo = null,
amountValue = model.confirmData.enteredFromAmount.orZero(),
reduceAmountBy = model.confirmData.reduceAmountBy.orZero(),

View file

@ -436,10 +436,11 @@ internal class SendWithSwapConfirmModel @Inject constructor(
feeUMV2?.feeExtraInfo?.feeCryptoCurrencyStatus ?: params.primaryFeePaidCurrencyStatusFlow.value
sendNotificationsUpdateTrigger.triggerUpdate(
data = NotificationData(
destinationAddress = when (val currency = primaryCurrencyStatus.currency) {
is CryptoCurrency.Token -> currency.contractAddress
is CryptoCurrency.Coin -> "0"
},
/**
* Null when destination is unknown at this point (e.g. CEX swap address is only known
* after receiving exchange-data). For DEX / DEX_BRIDGE, the address is known upfront.
*/
destinationAddress = null,
memo = null,
amountValue = confirmData.enteredFromAmount.orZero(),
reduceAmountBy = confirmData.reduceAmountBy,
@ -460,6 +461,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
enteredFromAmount = confirmData.enteredFromAmount,
fromCryptoCurrencyStatus = confirmData.fromCryptoCurrencyStatus,
priceImpact = confirmData.priceImpact,
provider = confirmData.quote?.provider,
),
)
uiState.transformerUpdate(

View file

@ -91,7 +91,7 @@ internal class SwapTransactionSender @AssistedInject constructor(
val feeValue = confirmData.fee?.amount?.value ?: return
val destination = confirmData.enteredDestination ?: return
val (amount, currencyStatus) = when (confirmData.amountType) {
val (swapDataRequestAmount, swapDataRequestCurrency) = when (confirmData.amountType) {
SwapAmountType.From -> {
val amountValue = confirmData.enteredFromAmount ?: return
val subtracted = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
@ -109,10 +109,15 @@ internal class SwapTransactionSender @AssistedInject constructor(
}
}
val fromTransactionAmount = when (confirmData.amountType) {
SwapAmountType.From -> swapDataRequestAmount
SwapAmountType.To -> confirmData.enteredFromAmount ?: return
}
val swapData = getSwapDataUseCase(
userWallet = userWallet,
fromCryptoCurrencyStatus = fromStatus,
amount = amount.toStringWithRightOffset(currencyStatus.currency.decimals),
amount = swapDataRequestAmount.toStringWithRightOffset(swapDataRequestCurrency.currency.decimals),
amountType = confirmData.amountType,
toCryptoCurrency = toStatus.currency,
toAddress = destination,
@ -124,7 +129,7 @@ internal class SwapTransactionSender @AssistedInject constructor(
).getOrElse { error -> onExpressError(error); return }
createAndSendCexTransaction(
fromAmount = amount,
fromAmount = fromTransactionAmount,
fromStatus = fromStatus,
fromAccount = fromAccount,
toStatus = toStatus,

View file

@ -12,7 +12,6 @@ import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.notifications.notifications
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.extensions.*
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent

View file

@ -1,9 +1,11 @@
package com.tangem.feature.swap.domain
import java.util.Collections.synchronizedSet
class AllowPermissionsHandlerImpl : AllowPermissionsHandler {
// todo maybe need to save in store
private val allowPermissionsInProgress = mutableSetOf<String>()
private val allowPermissionsInProgress = synchronizedSet(mutableSetOf<String>())
override fun addAddressToInProgress(tokenAddress: String) {
allowPermissionsInProgress.add(tokenAddress)

View file

@ -72,11 +72,7 @@ import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.*
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
@ -1301,7 +1297,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
)
?.filterIsInstance<CryptoCurrency.Coin>()
?.firstOrNull { it.network.id == network.id && it.network.derivationPath == network.derivationPath }
?: error("Unable to create network coin with ID: ${network.id}")
?: currenciesRepository.createCoinCurrency(network)
}
private suspend fun createEmptyAmountState(): SwapState {

View file

@ -12,6 +12,7 @@ import javax.inject.Singleton
internal class SwapDomainModule {
@Provides
@Singleton
fun provideAllowPermissionsHandler(): AllowPermissionsHandler {
return AllowPermissionsHandlerImpl()
}

View file

@ -75,6 +75,7 @@ import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter
import com.tangem.feature.swap.domain.AllowPermissionsHandler
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.TransactionFeeResult
import com.tangem.feature.swap.domain.TxFeeSealedState
@ -89,6 +90,7 @@ import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.router.SwapRouter
import com.tangem.feature.swap.ui.StateBuilder
import com.tangem.feature.swap.utils.formatToUIRepresentation
import com.tangem.feature.swap.utils.getContractAddress
import com.tangem.features.approval.api.GiveApprovalComponent
import com.tangem.features.approval.api.GiveApprovalFeatureToggles
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
@ -146,6 +148,7 @@ internal class SwapModel @Inject constructor(
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
private val messageSender: UiMessageSender,
private val paymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase,
private val allowPermissionsHandler: AllowPermissionsHandler,
chooseTokenBridgeFactory: ChooseTokenBridge.Factory,
giveApprovalFeatureToggles: GiveApprovalFeatureToggles,
) : Model() {
@ -260,6 +263,10 @@ internal class SwapModel @Inject constructor(
}
override fun onApproveDone() {
val fromContractAddress = dataState.fromCryptoCurrency?.currency?.getContractAddress()
if (fromContractAddress != null) {
allowPermissionsHandler.addAddressToInProgress(fromContractAddress)
}
approvalSlotNavigation.dismiss()
updateWalletBalance()
uiState = stateBuilder.loadingPermissionState(uiState)
@ -812,8 +819,8 @@ internal class SwapModel @Inject constructor(
analyticsEventHandler.send(
SwapEvents.HighPriceImpact(
sendToken = fromToken.currency.symbol,
receiveToken = toToken.currency.network.name,
sendBlockchain = fromToken.currency.symbol,
receiveToken = toToken.currency.symbol,
sendBlockchain = fromToken.currency.network.name,
receiveBlockchain = toToken.currency.network.name,
providerName = provider.name,
),
@ -823,8 +830,8 @@ internal class SwapModel @Inject constructor(
analyticsEventHandler.send(
SwapEvents.TradeTooLarge(
sendToken = fromToken.currency.symbol,
receiveToken = toToken.currency.network.name,
sendBlockchain = fromToken.currency.symbol,
receiveToken = toToken.currency.symbol,
sendBlockchain = fromToken.currency.network.name,
receiveBlockchain = toToken.currency.network.name,
providerName = provider.name,
),

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.simple
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.presentation.R
@ -52,4 +53,11 @@ internal fun getExpressErrorTitle(expressDataError: ExpressDataError): TextRefer
internal fun SwapAmount.formatToUIRepresentation(): String {
return value.format { simple(decimals = decimals) }
}
internal fun CryptoCurrency.getContractAddress(): String {
return when (this) {
is CryptoCurrency.Token -> this.contractAddress
is CryptoCurrency.Coin -> "0"
}
}

View file

@ -204,7 +204,7 @@ internal class TesterAccountsViewModel @Inject constructor(
var nextIndex = accountList.totalAccounts
@Suppress("LoopWithTooManyJumpStatements") // never mind for Tester Menu
while (accountList.canAddMoreAccounts) {
while (accountList.canAddMoreCryptoAccounts) {
val derivationIndex = DerivationIndex(nextIndex).getOrNull() ?: break
val newAccount = Account.CryptoPortfolio.invoke(
@ -246,7 +246,7 @@ internal class TesterAccountsViewModel @Inject constructor(
withContext(dispatchers.default) {
val updatedAccountList = AccountList.invoke(
userWalletId = accountList.userWalletId,
accounts = if (possibleToArchive > AccountList.MAX_ACCOUNTS_COUNT - 1) {
accounts = if (possibleToArchive > AccountList.MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT - 1) {
listOf(accountList.mainAccount)
} else {
accountList.accounts.subList(fromIndex = 0, toIndex = accountList.accounts.size - possibleToArchive)

View file

@ -97,7 +97,7 @@ internal class AccountItemsDelegate @Inject constructor(
add(header)
addAll(accounts.map(::mapAccount).applySortingOrder(order = accountsOrder))
val isAddAccountEnabled = accounts.size < AccountList.MAX_ACCOUNTS_COUNT
val isAddAccountEnabled = accounts.size < AccountList.MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT
val shouldShowDescription = accounts.size > 1
val isArchivedAccountsEnabled = accountStatusList.accountStatuses.size != accountStatusList.totalAccounts
@ -165,7 +165,7 @@ internal class AccountItemsDelegate @Inject constructor(
title = resourceReference(R.string.account_add_limit_dialog_title),
message = resourceReference(
id = R.string.account_add_limit_dialog_description,
formatArgs = wrappedList(AccountList.MAX_ACCOUNTS_COUNT.toString()),
formatArgs = wrappedList(AccountList.MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT.toString()),
),
firstActionBuilder = { firstAction },
),

View file

@ -4,6 +4,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import com.tangem.features.tangempay.entity.TangemPayMainUM
internal class TangemPayHideOnboardingStateTransformer(
userWalletId: UserWalletId,
@ -11,7 +12,7 @@ internal class TangemPayHideOnboardingStateTransformer(
override fun transform(prevState: WalletState): WalletState {
return if (prevState is WalletState.MultiCurrency.Content) {
prevState.copy(tangemPayState = TangemPayState.Empty)
prevState.copy(tangemPayState = TangemPayState.Empty, tangemPayMainUM = TangemPayMainUM.Empty)
} else {
prevState
}

View file

@ -5,7 +5,7 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1487"
tangemBlockchainSdk = "develop-1491"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-602"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^

View file

@ -24,6 +24,7 @@ internal class WalletManagerFactoryCreator @Inject constructor(
private val blockchainDataStorage: BlockchainDataStorage,
private val blockchainSDKLogger: BlockchainSDKLogger,
private val isSolanaTxHistoryEnabled: Boolean,
private val isSolanaScaledUiAmountEnabled: Boolean,
) {
fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory {
@ -37,6 +38,7 @@ internal class WalletManagerFactoryCreator @Inject constructor(
isYieldSupplyEnabled = true,
isPendingTransactionsEnabled = true,
isSolanaTxHistoryEnabled = isSolanaTxHistoryEnabled,
isSolanaScaledUiAmountEnabled = isSolanaScaledUiAmountEnabled,
),
blockchainDataStorage = blockchainDataStorage,
loggers = listOf(blockchainSDKLogger),

View file

@ -100,6 +100,9 @@ internal object BlockchainSDKFactoryModule {
isSolanaTxHistoryEnabled = featureTogglesManager.isFeatureEnabled(
FeatureToggles.SOLANA_TX_HISTORY_ENABLED,
),
isSolanaScaledUiAmountEnabled = featureTogglesManager.isFeatureEnabled(
FeatureToggles.SOLANA_SCALED_UI_AMOUNT_ENABLED,
),
)
}
}