Updated on 2026-08-14
This commit is contained in:
commit
a3216e8e23
57 changed files with 532 additions and 468 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import javax.inject.Singleton
|
|||
internal class SwapDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAllowPermissionsHandler(): AllowPermissionsHandler {
|
||||
return AllowPermissionsHandlerImpl()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ internal object BlockchainSDKFactoryModule {
|
|||
isSolanaTxHistoryEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.SOLANA_TX_HISTORY_ENABLED,
|
||||
),
|
||||
isSolanaScaledUiAmountEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.SOLANA_SCALED_UI_AMOUNT_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue