diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt index 27dceeec39..5927f3e422 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.res.painterResource import com.tangem.core.ui.R import com.tangem.core.ui.components.account.AccountCharIcon import com.tangem.core.ui.components.account.AccountResIcon +import com.tangem.core.ui.components.account.PaymentAccountIcon import com.tangem.core.ui.components.currency.DefaultCurrencyIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -61,6 +62,7 @@ internal fun ContentIcon( background = icon.background, alpha = alpha, ) + is CurrencyIconState.PaymentAccount -> PaymentAccountIcon(modifier = modifier, size = icon.size) is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon( modifier = modifier, resId = icon.resId, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index 9ec3a73b8d..f5cb062474 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -62,6 +62,7 @@ fun CurrencyIcon( is CurrencyIconState.FiatIcon, is CurrencyIconState.CustomTokenIcon, is CurrencyIconState.TokenIcon, + is CurrencyIconState.PaymentAccount, is CurrencyIconState.CryptoPortfolio.Icon, is CurrencyIconState.CryptoPortfolio.Letter, -> { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt index 5bdd5f41f4..505e0d5b67 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt @@ -88,6 +88,12 @@ sealed class CurrencyIconState { override val topBadgeIconResId: Int? = null } + data class PaymentAccount(val size: AccountIconSize = AccountIconSize.Default) : CurrencyIconState() { + override val isGrayscale: Boolean = false + override val shouldShowCustomBadge: Boolean = false + override val topBadgeIconResId: Int? = null + } + @Immutable sealed class CryptoPortfolio : CurrencyIconState() { override val shouldShowCustomBadge: Boolean = false @@ -155,6 +161,7 @@ sealed class CurrencyIconState { is CryptoPortfolio.Letter -> copy( isGrayscale = isGrayscale, ) + is PaymentAccount, is Loading, is Locked, is Empty, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index 0c6291b831..7ae2238fb7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -2,7 +2,8 @@ package com.tangem.core.ui.components.tokenlist import androidx.compose.animation.* import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds -import androidx.compose.animation.core.* +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -24,6 +25,7 @@ import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.account.AccountCharIcon import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.account.AccountResIcon +import com.tangem.core.ui.components.account.PaymentAccountIcon import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.SearchBar @@ -98,6 +100,9 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea icon.copy( size = if (isExpanded) AccountIconSize.ExtraSmall else AccountIconSize.Default, ) + is CurrencyIconState.PaymentAccount -> icon.copy( + size = if (isExpanded) AccountIconSize.ExtraSmall else AccountIconSize.Default, + ) else -> icon } @@ -183,7 +188,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B } } -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun ExpandedPortfolioHeader( state: TokenItemState, @@ -209,6 +214,7 @@ fun ExpandedPortfolioHeader( composables.icon.invoke(Modifier) } else { when (val icon = state.iconState) { + is CurrencyIconState.PaymentAccount -> PaymentAccountIcon(size = icon.size) is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon( resId = icon.resId, color = icon.color, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt index ac427c9961..e3750fd53e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt @@ -169,6 +169,7 @@ private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurR blurRadius = blurRadius, ) } + is CurrencyIconState.PaymentAccount -> Unit CurrencyIconState.Loading -> Unit CurrencyIconState.Locked -> Unit } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt index 48607769ea..ca7b011917 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt @@ -7,8 +7,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.core.error.UniversalError import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.pay.entity.TangemPayCurrencyFactory import com.tangem.data.pay.util.TangemPayErrorConverter -import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory @@ -16,14 +16,8 @@ import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory" -/** - * Custom token parameters. Will be used only for F&F. - */ -private const val TOKEN_ID = "usd-coin" -private const val TOKEN_NAME = "USDC" -private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" -private const val TOKEN_DECIMALS = 6 +@Deprecated("Use TangemPayCurrencyFactory instead") internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor( excludedBlockchains: ExcludedBlockchains, private val errorConverter: TangemPayErrorConverter, @@ -47,32 +41,11 @@ internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor( ) cryptoCurrencyFactory.createToken( network = requireNotNull(network), - rawId = CryptoCurrency.RawID(TOKEN_ID), - name = TOKEN_NAME, - symbol = TOKEN_NAME, - contractAddress = TOKEN_CONTRACT_ADDRESS, - decimals = TOKEN_DECIMALS, - ) - }.mapLeft { exception -> - TangemLogger.withTag(TAG).e("Error", exception) - errorConverter.convert(exception) - } - } - - override fun create(userWallet: UserWallet): Either { - return catch { - val network = networkFactory.create( - blockchain = VisaUtilities.visaBlockchain, - extraDerivationPath = null, - userWallet = userWallet, - ) - cryptoCurrencyFactory.createToken( - network = requireNotNull(network), - rawId = CryptoCurrency.RawID(TOKEN_ID), - name = TOKEN_NAME, - symbol = TOKEN_NAME, - contractAddress = TOKEN_CONTRACT_ADDRESS, - decimals = TOKEN_DECIMALS, + rawId = CryptoCurrency.RawID(TangemPayCurrencyFactory.TOKEN_ID), + name = TangemPayCurrencyFactory.TOKEN_NAME, + symbol = TangemPayCurrencyFactory.TOKEN_NAME, + contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS, + decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS, ) }.mapLeft { exception -> TangemLogger.withTag(TAG).e("Error", exception) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt index a42852436a..57eb9b4402 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -1,11 +1,12 @@ package com.tangem.data.pay.converter -import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convert -import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convertBack +import com.tangem.data.pay.entity.TangemPayCurrencyFactory import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.domain.models.StatusSource import com.tangem.domain.models.account.PaymentAccountStatusValue -import com.tangem.utils.converter.TwoWayConverter +import com.tangem.domain.models.wallet.UserWalletId +import javax.inject.Inject +import javax.inject.Singleton /** * Two-way converter between [PaymentAccountStatusValue] and [PaymentAccountStatusValueDM]. @@ -15,10 +16,12 @@ import com.tangem.utils.converter.TwoWayConverter * * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. */ -internal object PaymentAccountStatusValueDMConverter : - TwoWayConverter { +@Singleton +internal class PaymentAccountStatusValueDMConverter @Inject constructor( + private val tangemPayCurrencyFactory: TangemPayCurrencyFactory, +) { - override fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? { + fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? { return when (value) { is PaymentAccountStatusValue.NotCreated -> PaymentAccountStatusValueDM.NotCreated() is PaymentAccountStatusValue.UnderReview -> PaymentAccountStatusValueDM.UnderReview( @@ -60,7 +63,7 @@ internal object PaymentAccountStatusValueDMConverter : } } - override fun convertBack(value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue { + fun convertBack(userWalletId: UserWalletId, value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue { return when (value) { is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated is PaymentAccountStatusValueDM.CardIssueFailed -> PaymentAccountStatusValue.Error.CardIssueFailed( @@ -80,6 +83,7 @@ internal object PaymentAccountStatusValueDMConverter : isPinSet = value.isPinSet, fiatBalance = value.fiatBalance.toDomain(), cryptoBalance = value.cryptoBalance.toDomain(), + cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), ) } else { PaymentAccountStatusValue.Loaded( @@ -92,6 +96,7 @@ internal object PaymentAccountStatusValueDMConverter : isPinSet = value.isPinSet, fiatBalance = value.fiatBalance.toDomain(), cryptoBalance = value.cryptoBalance.toDomain(), + cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), ) } is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 3887f44e6f..d697221c1b 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -6,6 +6,7 @@ import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory import com.tangem.data.pay.DefaultTangemPayEligibilityManager +import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer import com.tangem.data.pay.repository.* @@ -24,6 +25,7 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.repository.* +import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase @@ -112,6 +114,7 @@ internal interface TangemPayDataModule { @ApplicationContext context: Context, dispatchers: CoroutineDispatcherProvider, scope: AppCoroutineScope, + converter: PaymentAccountStatusValueDMConverter, ): PaymentAccountStatusesStore { return PaymentAccountStatusesStore( runtimeStore = RuntimeSharedStore(), @@ -124,6 +127,7 @@ internal interface TangemPayDataModule { produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") }, scope = scope, ), + converter = converter, scope = scope, ) } @@ -139,6 +143,14 @@ internal interface TangemPayDataModule { ) {} } + @Provides + @Singleton + fun provideGetTangemPayCryptoCurrencyStatusUseCase( + paymentAccountStatusSupplier: PaymentAccountStatusSupplier, + ): GetPaymentAccountCryptoCurrencyStatusUseCase { + return GetPaymentAccountCryptoCurrencyStatusUseCase(paymentAccountStatusSupplier) + } + @Provides @Singleton fun provideTangemPayMainScreenCustomerInfoUseCase( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/entity/TangemPayCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/TangemPayCurrencyFactory.kt new file mode 100644 index 0000000000..ede6bba797 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/TangemPayCurrencyFactory.kt @@ -0,0 +1,49 @@ +package com.tangem.data.pay.entity + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.network.NetworkFactory +import com.tangem.domain.card.common.visa.VisaUtilities +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.requireUserWalletsSync +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class TangemPayCurrencyFactory @Inject constructor( + excludedBlockchains: ExcludedBlockchains, + private val userWalletsListRepository: UserWalletsListRepository, + private val networkFactory: NetworkFactory, +) { + private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) { + CryptoCurrencyFactory(excludedBlockchains) + } + + fun create(userWalletId: UserWalletId): CryptoCurrency.Token { + val userWallet = userWalletsListRepository.requireUserWalletsSync() + .firstOrNull { it.walletId == userWalletId } + ?: error("User wallet with id $userWalletId not found") + val network = networkFactory.create( + blockchain = VisaUtilities.visaBlockchain, + userWallet = userWallet, + extraDerivationPath = null, + ) + return cryptoCurrencyFactory.createToken( + network = requireNotNull(network), + rawId = CryptoCurrency.RawID(TOKEN_ID), + name = TOKEN_NAME, + symbol = TOKEN_NAME, + contractAddress = TOKEN_CONTRACT_ADDRESS, + decimals = TOKEN_DECIMALS, + ) + } + + companion object { + internal const val TOKEN_ID = "usd-coin" + internal const val TOKEN_NAME = "USDC" + internal const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" + internal const val TOKEN_DECIMALS = 6 + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index cdd78706de..9e0ae38e91 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -1,6 +1,7 @@ package com.tangem.data.pay.flow import arrow.core.Either +import com.tangem.data.pay.entity.TangemPayCurrencyFactory import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.StatusSource @@ -8,6 +9,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.PaymentAccountStatusValue 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.OrderStatus @@ -29,6 +31,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private val customerOrderRepository: CustomerOrderRepository, private val deviceSecurity: DeviceSecurityInfoProvider, private val dispatchers: CoroutineDispatcherProvider, + private val tangemPayCurrencyFactory: TangemPayCurrencyFactory, ) : PaymentAccountStatusFetcher { private val logger = TangemLogger.withTag(TAG) @@ -132,7 +135,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( }, ifRight = { customerInfo -> logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}") - val status = customerInfo.mapToPaymentAccountStatus() + val status = customerInfo.mapToPaymentAccountStatus(account.userWalletId) if (status is PaymentAccountStatusValue.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) { // If order id wasn't saved -> start order creation and get customer info onboardingRepository.createOrder(account.userWalletId) @@ -167,7 +170,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( }, ifRight = { customerInfo -> if (customerInfo.kycStatus == KycStatus.REJECTED) { - customerInfo.mapToPaymentAccountStatus() + customerInfo.mapToPaymentAccountStatus(account.userWalletId) } else { PaymentAccountStatusValue.Error.CardIssueFailed( customerId = orderData.customerId, @@ -182,7 +185,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId) .fold( ifLeft = { it.mapToPaymentAccountStatus() }, - ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, + ifRight = { it.mapToPaymentAccountStatus(account.userWalletId) }, ) } OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable @@ -191,7 +194,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ) } - private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue { + private fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { val cardInfo = this.cardInfo val productInstance = this.productInstance return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) { @@ -202,6 +205,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ) } else if (cardInfo != null && productInstance != null && !customerId.isNullOrEmpty()) { convertToContentState( + userWalletId = userWalletId, productInstance = productInstance, cardInfo = cardInfo, customerId = requireNotNull(customerId) { "CustomerId must not be null" }, @@ -212,10 +216,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( } private fun convertToContentState( + userWalletId: UserWalletId, productInstance: CustomerInfo.ProductInstance, cardInfo: CustomerInfo.CardInfo, customerId: String, ): PaymentAccountStatusValue { + val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId) return when (productInstance.frozenState) { TangemPayCardFrozenState.Frozen -> PaymentAccountStatusValue.Locked( source = StatusSource.ACTUAL, @@ -227,6 +233,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( isPinSet = cardInfo.isPinSet, fiatBalance = cardInfo.fiatBalance, cryptoBalance = cardInfo.cryptoBalance, + cryptoCurrency = cryptoCurrency, ) else -> PaymentAccountStatusValue.Loaded( source = StatusSource.ACTUAL, @@ -238,6 +245,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( isPinSet = cardInfo.isPinSet, fiatBalance = cardInfo.fiatBalance, cryptoBalance = cardInfo.cryptoBalance, + cryptoCurrency = cryptoCurrency, ) } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt index ec8a48d880..62accbccf7 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt @@ -29,6 +29,7 @@ internal typealias WalletIdWithPaymentStatusDM = Map, private val persistenceDataStore: DataStore, + private val converter: PaymentAccountStatusValueDMConverter, scope: AppCoroutineScope, ) { @@ -39,7 +40,7 @@ internal class PaymentAccountStatusesStore( runtimeStore.store( value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) -> val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId)) - val statusValue = PaymentAccountStatusValueDMConverter.convertBack(value = statusDM) + val statusValue = converter.convertBack(userWalletId = account.userWalletId, value = statusDM) AccountStatus.Payment(account = account, value = statusValue) }, ) @@ -87,7 +88,7 @@ internal class PaymentAccountStatusesStore( } private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatusValue) { - val statusDM = PaymentAccountStatusValueDMConverter.convert(value = status) ?: return + val statusDM = converter.convert(value = status) ?: return persistenceDataStore.updateData { storedStatuses -> storedStatuses.toMutableMap().apply { put(key = userWalletId.stringValue, value = statusDM) diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt index fc44ccc105..53beaf23be 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -2,9 +2,13 @@ package com.tangem.domain.models.account import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.serialization.SerializedBigDecimal import kotlinx.serialization.Serializable +import java.math.BigDecimal /** * Represents the various states a payment account can have, encapsulating different information based on the state. @@ -104,7 +108,29 @@ sealed class PaymentAccountStatusValue { val isPinSet: Boolean, val fiatBalance: FiatBalance, val cryptoBalance: CryptoBalance, - ) : PaymentAccountStatusValue() + val cryptoCurrency: CryptoCurrency.Token, + ) : PaymentAccountStatusValue() { + val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( + currency = cryptoCurrency, + value = CryptoCurrencyStatus.Loaded( + amount = cryptoBalance.balance, + fiatAmount = fiatBalance.availableBalance, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + type = NetworkAddress.Address.Type.Primary, + value = cryptoBalance.depositAddress, + ), + ), + sources = CryptoCurrencyStatus.Sources(), + pendingTransactions = emptySet(), + stakingBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + ), + ) + } /** * Represents a state where the payment account is successfully loaded with complete information. @@ -130,7 +156,29 @@ sealed class PaymentAccountStatusValue { val isPinSet: Boolean, val fiatBalance: FiatBalance, val cryptoBalance: CryptoBalance, - ) : PaymentAccountStatusValue() + val cryptoCurrency: CryptoCurrency.Token, + ) : PaymentAccountStatusValue() { + val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( + currency = cryptoCurrency, + value = CryptoCurrencyStatus.Loaded( + amount = cryptoBalance.balance, + fiatAmount = fiatBalance.availableBalance, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + type = NetworkAddress.Address.Type.Primary, + value = cryptoBalance.depositAddress, + ), + ), + sources = CryptoCurrencyStatus.Sources(), + pendingTransactions = emptySet(), + stakingBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + ), + ) + } /** Represents an error state for the payment account status. */ @Serializable diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt index 1004e00447..31a2537914 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt @@ -5,8 +5,8 @@ import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +@Deprecated("TangemPayCurrencyFactory") interface TangemPayCryptoCurrencyFactory { fun create(userWallet: UserWallet, chainId: Int): Either - fun create(userWallet: UserWallet): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt new file mode 100644 index 0000000000..fe66dc36f7 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt @@ -0,0 +1,34 @@ +package com.tangem.domain.pay.usecase + +import arrow.core.Option +import arrow.core.none +import arrow.core.some +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier +import kotlinx.coroutines.flow.firstOrNull + +class GetPaymentAccountCryptoCurrencyStatusUseCase( + private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Option> { + val accountStatus = paymentAccountStatusSupplier.invoke(userWalletId).firstOrNull() ?: return none() + val cryptoCurrencyStatus = when (val statusValue = accountStatus.value) { + is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus + is PaymentAccountStatusValue.Locked -> statusValue.cryptoCurrencyStatus + else -> return none() + } + return if (cryptoCurrencyStatus.currency == cryptoCurrency) { + (accountStatus.account to cryptoCurrencyStatus).some() + } else { + none() + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 18ff42ff9e..df6dc83590 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -19,7 +19,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.CryptoCurrencyAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked -import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase @@ -69,7 +68,6 @@ internal class SendDestinationModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val parseQrCodeUseCase: ParseQrCodeUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory, private val analyticsEventHandler: AnalyticsEventHandler, private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, ) : Model(), SendDestinationClickIntents { @@ -260,16 +258,16 @@ internal class SendDestinationModel @Inject constructor( private fun AccountStatus.Payment.getDestinationWalletUM(wallet: UserWallet): DestinationWalletUM? { val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: return null - val address = when (val status = this.value) { - is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress - is PaymentAccountStatusValue.Locked -> status.cryptoBalance.depositAddress + val (paymentAccountAddress, currency) = when (val status = this.value) { + is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress to status.cryptoCurrency + is PaymentAccountStatusValue.Locked -> status.cryptoBalance.depositAddress to status.cryptoCurrency else -> return null } - val currency = tangemPayCryptoCurrencyFactory.create(wallet).getOrNull() ?: return null + return if (contractAddress.equals(currency.contractAddress, true)) { DestinationWalletUM( name = wallet.name, - address = address, + address = paymentAccountAddress, cryptoCurrency = currency, userWalletId = wallet.walletId, account = account, diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt index ed9144cac3..2d0f54f7b2 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt @@ -2,9 +2,7 @@ package com.tangem.features.swap import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -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.UserWalletId import java.math.BigDecimal @@ -17,8 +15,6 @@ interface SwapComponent : ComposableContentComponent { val isInitialReverseOrder: Boolean = false, val screenSource: String, val tangemPayInput: TangemPayInput? = null, - val preselectedToToken: CryptoCurrencyStatus? = null, - val preselectedAccount: Account? = null, ) { data class TangemPayInput( val cryptoAmount: BigDecimal, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index 2b50f90d14..dba1d47550 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -162,8 +162,12 @@ internal class SavedSwapTransactionListConverter( } private fun findAccountByDerivationIndex(accountList: AccountList?, derivationIndex: DerivationIndex?): Account? { - return accountList?.accounts?.asSequence()?.filterIsInstance() - ?.firstOrNull { it.derivationIndex == derivationIndex } + val accounts = accountList?.accounts ?: return null + + return accounts.asSequence() + .filterIsInstance() + .firstOrNull { it.derivationIndex == derivationIndex } + ?: accounts.firstOrNull { it is Account.Payment }.takeIf { derivationIndex == null } } private fun UserTokensResponse.Token.getDerivationIndex(): DerivationIndex? { diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index d0baa14afb..b76d4c6e4c 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.domain.express.models) implementation(projects.domain.account) implementation(projects.domain.account.status) + implementation(projects.domain.visa) implementation(projects.domain.visa.models) implementation(projects.features.swap.domain.api) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 98165323aa..ae3c82c978 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -49,9 +49,9 @@ interface SwapInteractor { @Throws(IllegalStateException::class) suspend fun findBestQuote( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, providers: List, amountToSwap: String, reduceBalanceBy: BigDecimal, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index e44392c352..18dc50d0b6 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -31,7 +31,7 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.express.models.ExpressOperationType 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.account.PaymentAccountStatusValue import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -134,13 +134,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun getAccountCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { val walletAccountCurrencyStatuses = singleAccountStatusListSupplier.getSyncOrNull( SingleAccountStatusListProducer.Params(userWalletId), - )?.accountStatuses.orEmpty().filterCryptoPortfolio() + )?.accountStatuses.orEmpty() val walletAccountCurrencyStatusesExceptInitial: Map> = walletAccountCurrencyStatuses.mapNotNull { accountStatus -> val filteredCurrencies = when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.flattenCurrencies().filterCurrencies(currency) - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") + is AccountStatus.Payment -> getPaymentAccountCurrencies(accountStatus) } if (filteredCurrencies.isNotEmpty()) { @@ -184,6 +184,16 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } + private fun getPaymentAccountCurrencies(accountStatus: AccountStatus.Payment): List { + val currencyStatus = when (val statusValue = accountStatus.value) { + is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus + is PaymentAccountStatusValue.Locked -> statusValue.cryptoCurrencyStatus + else -> return emptyList() + } + + return listOf(currencyStatus) + } + private fun List.filterCurrencies(currency: CryptoCurrency) = this.filter { status -> val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || status.currency.getContractAddress() != currency.getContractAddress() @@ -207,11 +217,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( tokenInfoForFilter(pair).network == currency.network.backendId } - val accountCurrencyList = cryptoCurrenciesList.mapNotNull { (accountEntry, currencyStatusList) -> - val cryptoPortfolio = accountEntry as? Account.CryptoPortfolio ?: return@mapNotNull null - + val accountCurrencyList = cryptoCurrenciesList.map { (accountEntry, currencyStatusList) -> AccountSwapAvailability( - account = cryptoPortfolio, + account = accountEntry, currencyList = currencyStatusList.map { currencyStatus -> val providers = findProvidersForPair( cryptoCurrencyStatuses = currencyStatus, @@ -314,11 +322,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } + @Suppress("LongMethod") override suspend fun findBestQuote( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, providers: List, amountToSwap: String, reduceBalanceBy: BigDecimal, @@ -343,7 +352,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( return providers.associateWith { createEmptyAmountState() } } val amount = SwapAmount(amountDecimal, fromToken.currency.decimals) - val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) + val isBalanceWithoutFeeEnough = when (fromAccount) { + is Account.Payment -> true + else -> isBalanceEnough(fromToken, amount, null) + } val networkId = fromToken.currency.network.backendId when (provider.type) { ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { @@ -397,9 +409,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun manageDex( networkId: String, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, provider: SwapProvider, txFeeSealedState: TxFeeSealedState, amount: SwapAmount, @@ -480,9 +492,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun manageDexSolana( networkId: String, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, provider: SwapProvider, txFeeSealedState: TxFeeSealedState, amount: SwapAmount, @@ -535,9 +547,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun manageCex( networkId: String, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, provider: SwapProvider, amount: SwapAmount, reduceBalanceBy: BigDecimal, @@ -1292,9 +1304,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount: SwapAmount, reduceBalanceBy: BigDecimal, fromTokenStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toTokenStatus: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, provider: SwapProvider, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, @@ -1386,9 +1398,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( quoteDataModel: Either, amount: SwapAmount, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, networkId: String, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, @@ -1500,7 +1512,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun createSwapErrorWith( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, amount: SwapAmount, includeFeeInAmount: IncludeFeeInAmount, expressDataError: ExpressDataError, @@ -1690,9 +1702,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( provider: SwapProvider, networkId: String, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: SwapAmount, txFeeSealedState: TxFeeSealedState, expressOperationType: ExpressOperationType, @@ -1834,7 +1846,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun produceDexSwapDataError( error: ExpressDataError, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, amount: SwapAmount, ): SwapState.SwapError { val rates = getQuotes(fromToken.currency.id) @@ -1931,9 +1943,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun updateBalances( provider: SwapProvider, fromTokenStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toTokenStatus: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, swapData: SwapDataModel?, @@ -1999,7 +2011,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun updatePermissionState( networkId: String, fromTokenStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, spenderAddress: String?, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 89f79b2d72..d538156362 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -101,7 +101,7 @@ data class TokenSwapInfo( val tokenAmount: SwapAmount, val amountFiat: BigDecimal, val cryptoCurrencyStatus: CryptoCurrencyStatus, - val account: Account.CryptoPortfolio?, + val account: Account?, ) data class RequestApproveStateData( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index f0587337d6..d2da881490 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -45,13 +45,13 @@ data class CurrenciesGroup( ) data class AccountSwapAvailability( - val account: Account.CryptoPortfolio, + val account: Account, val currencyList: List, ) data class AccountSwapCurrency( val isAvailable: Boolean, - val account: Account.CryptoPortfolio, + val account: Account, val cryptoCurrencyStatus: CryptoCurrencyStatus, val providers: List, ) \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt index e87799d55e..c421ec5874 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt @@ -4,6 +4,7 @@ import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -23,14 +24,14 @@ internal interface ChooseTokenBridge { val onClose: Channel // todo swap legacy api, remove - val onTokenSelected: Channel> + val onTokenSelected: Channel val onNewTokenAdded: Channel> val searchQueryState: StateFlow val currenciesGroup: Flow - fun onTokenSelected(tokenId: Pair) { - onTokenSelected.trySend(tokenId) + fun onTokenSelected(result: ChooseTokenResultOld) { + onTokenSelected.trySend(result) onSearchQuery("") } @@ -57,6 +58,12 @@ internal interface ChooseTokenBridge { } } +data class ChooseTokenResultOld( + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val account: Account, + val isSearched: Boolean, +) + data class ChooseTokenResult( val currency: CryptoCurrencyStatus, val account: AccountStatus, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt index 870a5a7abe..a8b7cb6142 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult +import com.tangem.feature.swap.choosetoken.api.ChooseTokenResultOld import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel.Companion.DEBOUNCE_SEARCH_DELAY import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import dagger.assisted.Assisted @@ -19,7 +20,7 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor( override val onCurrencyChosen: Channel = Channel() - override val onTokenSelected: Channel> = Channel() + override val onTokenSelected: Channel = Channel() override val onNewTokenAdded: Channel> = Channel() override val onClose: Channel = Channel() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt index ca32af6c5e..0766482b98 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt @@ -116,10 +116,13 @@ internal class ChooseTokenModel @Inject constructor( TokensDataConverter( onSearchEntered = { query -> bridge.onSearchQuery(query) }, - onTokenClick = { tokenId -> - val selected = tokenId to ChooseTokenAnalyticsPayload - .IsSearched(isSearchingState) - bridge.onTokenSelected(selected) + onTokenClick = { account, cryptoCurrencyStatus -> + val result = ChooseTokenResultOld( + account = account, + cryptoCurrencyStatus = cryptoCurrencyStatus, + isSearched = searchQueryState.value.isNotEmpty(), + ) + bridge.onTokenSelected(result) }, onAccountClick = { account -> expandedAccountsFlow.update { expandedList -> diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt index 0bb771929f..333bb5087c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt @@ -2,15 +2,21 @@ package com.tangem.feature.swap.converters import com.tangem.common.getTotalCryptoAmount import com.tangem.common.getTotalFiatAmount +import com.tangem.common.ui.R import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter import com.tangem.common.ui.account.TokensListPortfolioItemConverter +import com.tangem.common.ui.account.toUM import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -29,34 +35,68 @@ internal class AccountTokenItemConverter( private val appCurrency: AppCurrency, private val unavailableErrorText: TextReference, private val expandedAccounts: Map, - private val onTokenItemClick: (String) -> Unit, - private val onAccountItemClick: (Account.CryptoPortfolio) -> Unit, + private val onTokenItemClick: (Account, CryptoCurrencyStatus) -> Unit, + private val onAccountItemClick: (Account) -> Unit, ) : Converter { override fun convert(value: AccountSwapAvailability): TokensListItemUM.Portfolio { - return TokensListPortfolioItemConverter( - tokenItemUM = AccountCryptoPortfolioItemStateConverter( + val headerTokenItemState = when (val account = value.account) { + is Account.CryptoPortfolio -> AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, - account = value.account.copy( - cryptoCurrencies = value.currencyList.map { it.cryptoCurrencyStatus.currency }, - ), + account = account.copy(cryptoCurrencies = value.currencyList.map { it.cryptoCurrencyStatus.currency }), onItemClick = onAccountItemClick, ).convert( TotalFiatBalance.Loaded( amount = value.currencyList.sumOf { it.cryptoCurrencyStatus.value.fiatAmount.orZero() }, source = StatusSource.ONLY_CACHE, ), - ), + ) + is Account.Payment -> createPaymentAccountHeaderState(value) + } + return TokensListPortfolioItemConverter( + tokenItemUM = headerTokenItemState, isExpanded = expandedAccounts[value.account.accountId] != false, isCollapsable = true, tokens = value.currencyList.map { accountSwapCurrency -> - createAvailableItemConverter() + createAvailableItemConverter(value.account) .convert(accountSwapCurrency.cryptoCurrencyStatus) }.map(TokensListItemUM::Token).toPersistentList(), ).convert(Unit) } - fun createAvailableItemConverter(): TokenItemStateConverter { + private fun createPaymentAccountHeaderState(accountSwapAvailability: AccountSwapAvailability): TokenItemState { + val account = accountSwapAvailability.account + val tokensCount = accountSwapAvailability.currencyList.size + val fiatBalance = + accountSwapAvailability.currencyList.sumOf { it.cryptoCurrencyStatus.value.fiatAmount.orZero() } + return TokenItemState.Content( + id = account.accountId.value, + iconState = CurrencyIconState.PaymentAccount(), + titleState = TokenItemState.TitleState.Content(text = account.accountName.toUM().value), + subtitleState = TokenItemState.SubtitleState.TextContent( + value = pluralReference( + R.plurals.common_tokens_count, + count = tokensCount, + formatArgs = wrappedList(tokensCount), + ), + isAvailable = false, + ), + onItemClick = { onAccountItemClick(account) }, + fiatAmountState = FiatAmountState.Content( + text = fiatBalance.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + isFlickering = false, + ), + subtitle2State = null, + onItemLongClick = null, + ) + } + + fun createAvailableItemConverter(account: Account): TokenItemStateConverter { return TokenItemStateConverter( appCurrency = appCurrency, subtitleStateProvider = { status -> @@ -70,7 +110,7 @@ internal class AccountTokenItemConverter( fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true) }, - onItemClick = { account, currencyStatus -> onTokenItemClick(currencyStatus.currency.id.value) }, + onItemClick = { _, currencyStatus -> onTokenItemClick(account, currencyStatus) }, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 64ba7cf5b0..d06953f1a3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenListUMData @@ -16,10 +17,10 @@ import kotlinx.collections.immutable.toPersistentList @Suppress("LongParameterList") internal class TokensDataConverter( - private val onSearchEntered: (String) -> Unit, - onTokenClick: (String) -> Unit, - onAccountClick: (Account.CryptoPortfolio) -> Unit, + onTokenClick: (Account, CryptoCurrencyStatus) -> Unit, + onAccountClick: (Account) -> Unit, private val expandedAccounts: Map, + private val onSearchEntered: (String) -> Unit, private val tokensDataState: CurrenciesGroup, private val isBalanceHidden: Boolean, private val isAccountsMode: Boolean, @@ -52,7 +53,7 @@ internal class TokensDataConverter( } else { val tokensList = accountList.flatMap { (_, currencyList) -> currencyList.asSequence().map { accountSwapCurrency -> - accountListItemConverter.createAvailableItemConverter() + accountListItemConverter.createAvailableItemConverter(accountSwapCurrency.account) .convert(accountSwapCurrency.cryptoCurrencyStatus) }.map(TokensListItemUM::Token).toPersistentList() }.toPersistentList() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index dcdc0a63a5..41203dc1f7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -33,7 +33,6 @@ import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase @@ -49,12 +48,14 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.promo.models.StoryContentIds import com.tangem.domain.settings.usercountry.GetUserCountryUseCase @@ -144,6 +145,7 @@ internal class SwapModel @Inject constructor( private val appsFlyerStore: AppsFlyerStore, private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, private val messageSender: UiMessageSender, + private val paymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase, chooseTokenBridgeFactory: ChooseTokenBridge.Factory, giveApprovalFeatureToggles: GiveApprovalFeatureToggles, ) : Model() { @@ -219,8 +221,10 @@ internal class SwapModel @Inject constructor( private val swapRouter: SwapRouter = SwapRouter(router = router) private var userCountry: UserCountry? = null - private var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null - private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null + private var fromAccount: Account? = null + private var toAccount: Account? = null + private var fromAccountStatus: CryptoCurrencyStatus? = null + private var toAccountStatus: CryptoCurrencyStatus? = null /** * If user came from Tangem Pay -> fromAccountCurrencyStatus == null @@ -286,8 +290,12 @@ internal class SwapModel @Inject constructor( .launchIn(modelScope) chooseTokenBridge.onTokenSelected.receiveAsFlow() - .onEach { (addedToken, isSearched) -> - onTokenSelect(addedToken, isSearched.value) + .onEach { result -> + onTokenSelect( + account = result.account, + cryptoCurrencyStatus = result.cryptoCurrencyStatus, + isSearched = result.isSearched, + ) } .launchIn(modelScope) @@ -322,22 +330,31 @@ internal class SwapModel @Inject constructor( userWalletId = userWalletId, currency = initialCurrencyFrom, ).getOrNull() + val fromPaymentAccountStatus = + paymentAccountCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom).getOrNull() val toAccountStatus = initialCurrencyTo?.let { currencyTo -> getAccountCurrencyStatusUseCase.invokeSync( userWalletId = userWalletId, currency = currencyTo, ).getOrNull() } + val toPaymentAccountStatus = initialCurrencyTo?.let { currencyTo -> + paymentAccountCryptoCurrencyStatusUseCase(userWalletId, currencyTo).getOrNull() + } + val fromAccount = fromAccountStatus?.account ?: fromPaymentAccountStatus?.first + val fromStatus = fromAccountStatus?.status ?: fromPaymentAccountStatus?.second - if (fromAccountStatus == null) { + if (fromAccount != null && fromStatus != null) { + this@SwapModel.fromAccount = fromAccount + this@SwapModel.fromAccountStatus = fromStatus + this@SwapModel.toAccount = toAccountStatus?.account ?: toPaymentAccountStatus?.first + this@SwapModel.toAccountStatus = toAccountStatus?.status ?: toPaymentAccountStatus?.second + this@SwapModel.initialFromStatus = fromStatus + this@SwapModel.initialToStatus = toAccountStatus?.status ?: toPaymentAccountStatus?.second + initTokens(isInitiallyReversed) + } else { showAlert() swapRouter.back() - } else { - fromAccountCurrencyStatus = fromAccountStatus - toAccountCurrencyStatus = toAccountStatus - initialFromStatus = fromAccountStatus.status - initialToStatus = toAccountStatus?.status - initTokens(isInitiallyReversed) } } else { val fromStatus = getFromStatus() @@ -405,7 +422,7 @@ internal class SwapModel @Inject constructor( updateTokensState(state) val (selectedCurrency, selectedAccount) = run { - var selectedAccountCurrency = toAccountCurrencyStatus + var selectedAccountCurrency = toAccountStatus if (selectedAccountCurrency == null) { val amountSwapCurrency = swapInteractor.getInitialCurrencyToSwap( @@ -415,14 +432,11 @@ internal class SwapModel @Inject constructor( ) if (amountSwapCurrency != null) { - selectedAccountCurrency = AccountCryptoCurrencyStatus( - account = amountSwapCurrency.account, - status = amountSwapCurrency.cryptoCurrencyStatus, - ) + selectedAccountCurrency = amountSwapCurrency.cryptoCurrencyStatus } } - selectedAccountCurrency?.status to selectedAccountCurrency?.account + selectedAccountCurrency to toAccount } val isApplied = applyInitialTokenChoice( @@ -534,7 +548,7 @@ internal class SwapModel @Inject constructor( private fun applyInitialTokenChoice( state: TokensDataStateExpress, selectedCurrency: CryptoCurrencyStatus?, - selectedAccount: Account.CryptoPortfolio?, + selectedAccount: Account?, isReverseFromTo: Boolean, ): Boolean { // exceptional case @@ -555,9 +569,9 @@ internal class SwapModel @Inject constructor( } val (fromAccount, toAccount) = if (canUseFromAccountCurrencyStatus) { if (isOrderReversed.value) { - selectedAccount to requireNotNull(fromAccountCurrencyStatus).account + selectedAccount to fromAccount } else { - requireNotNull(fromAccountCurrencyStatus).account to selectedAccount + fromAccount to selectedAccount } } else { null to null @@ -600,9 +614,9 @@ internal class SwapModel @Inject constructor( private fun startLoadingQuotes( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: String, reduceBalanceBy: BigDecimal, toProvidersList: List, @@ -673,9 +687,9 @@ internal class SwapModel @Inject constructor( private fun loadQuotesTask( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: String, reduceBalanceBy: BigDecimal, toProvidersList: List, @@ -777,7 +791,7 @@ internal class SwapModel @Inject constructor( selectedFeeType = (getSelectedFee() as? TxFee.Legacy)?.feeType ?: FeeType.NORMAL, isReverseSwapPossible = isReverseSwapPossible(), needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), - hideFee = tangemPayInput?.isWithdrawal == true, + hideFee = isTangemPayWithdrawal(), ) } @@ -971,8 +985,9 @@ internal class SwapModel @Inject constructor( } val fromCurrency = requireNotNull(dataState.fromCryptoCurrency) val fee = getSelectedFee() + val isTangemPayWithdrawal = isTangemPayWithdrawal() - if (fee == null && tangemPayInput?.isWithdrawal != true) { + if (fee == null && !isTangemPayWithdrawal) { TangemLogger.e("onSwapClick: fee is null and isWithdrawal is ${tangemPayInput?.isWithdrawal}") showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) modelScope.launch { @@ -994,7 +1009,7 @@ internal class SwapModel @Inject constructor( includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount, fee = fee, expressOperationType = ExpressOperationType.SWAP, - isTangemPayWithdrawal = tangemPayInput?.isWithdrawal == true, + isTangemPayWithdrawal = isTangemPayWithdrawal, ) }.onSuccess { swapTransactionState -> when (swapTransactionState) { @@ -1252,11 +1267,12 @@ internal class SwapModel @Inject constructor( } @Suppress("LongMethod") - private fun onTokenSelect(id: String, isSearched: Boolean) { + private fun onTokenSelect(account: Account, cryptoCurrencyStatus: CryptoCurrencyStatus, isSearched: Boolean) { val tokens = dataState.tokensDataState ?: return - val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) + val foundToken = cryptoCurrencyStatus + val foundAccount = account - foundToken?.currency?.symbol?.let { symbol -> + foundToken.currency.symbol.let { symbol -> analyticsEventHandler.send( SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol), ) @@ -1270,108 +1286,90 @@ internal class SwapModel @Inject constructor( ) } - if (foundToken != null) { - val fromToken: CryptoCurrencyStatus - val fromAccount: Account.CryptoPortfolio? - val toToken: CryptoCurrencyStatus - val toAccount: Account.CryptoPortfolio? - if (isOrderReversed.value) { - fromToken = foundToken - fromAccount = foundAccount - toToken = initialFromStatus - toAccount = fromAccountCurrencyStatus?.account + val fromToken: CryptoCurrencyStatus + val fromAccount: Account? + val toToken: CryptoCurrencyStatus + val toAccount: Account? + if (isOrderReversed.value) { + fromToken = foundToken + fromAccount = foundAccount + toToken = initialFromStatus + toAccount = this.fromAccount - val newToken = fromToken.currency as? CryptoCurrency.Coin - if (newToken != null) { - subscribeToCoinBalanceUpdates( - userWalletId = userWalletId, - coin = newToken, - isFromCurrency = true, - ) - } else { - fromTokenBalanceJobHolder.cancel() - } + val newToken = fromToken.currency as? CryptoCurrency.Coin + if (newToken != null) { + subscribeToCoinBalanceUpdates( + userWalletId = userWalletId, + coin = newToken, + isFromCurrency = true, + ) } else { - fromToken = initialFromStatus - fromAccount = fromAccountCurrencyStatus?.account - toToken = foundToken - toAccount = foundAccount - - val newToken = toToken.currency as? CryptoCurrency.Coin - if (newToken != null) { - subscribeToCoinBalanceUpdates( - userWalletId = userWalletId, - coin = newToken, - isFromCurrency = false, - ) - } else { - toTokenBalanceJobHolder.cancel() - } + fromTokenBalanceJobHolder.cancel() } - - if (dataState.fromCryptoCurrency != null && dataState.tokensDataState != null) { - isAmountChangedByUser = true - } - - dataState = dataState.copy( - fromCryptoCurrency = fromToken, - fromAccount = fromAccount, - toCryptoCurrency = toToken, - toAccount = toAccount, - selectedProvider = null, - ) - swapRouter.openScreen(SwapNavScreen.Main) - if (handleSwapNotSupported( - state = tokens, - fromToken = fromToken, - toToken = toToken, - fromAccount = fromAccount, - toAccount = toAccount, - ) - ) { - return - } - modelScope.launch { - TangemLogger.i( - "updateFeePaidCryptoCurrencyFor: id = ${fromToken.currency.id}, " + - "isOrderReversed: ${isOrderReversed.value}", - ) - if ((uiState.sendCardData as? SwapCardState.SwapCardData)?.type is TransactionCardType.ReadOnly) { - uiState = stateBuilder.createInitialLoadingState( - initialCurrencyFrom = fromToken.currency, - initialCurrencyTo = toToken.currency, - fromNetworkInfo = fromToken.currency.getNetworkInfo(), - ) - } - updateFeePaidCryptoCurrencyFor(fromToken) - startLoadingQuotes( - fromToken = fromToken, - fromAccount = fromAccount, - toToken = toToken, - toAccount = toAccount, - amount = lastAmount.value, - reduceBalanceBy = lastReducedBalanceBy.value, - toProvidersList = findSwapProviders(fromToken, toToken), - ) - } - updateTokensState(tokens) - } - } - - private fun getSelectedTokenAndAccount( - tokens: TokensDataStateExpress, - id: String, - ): Pair { - val accountCryptoCurrencyStatus = if (isOrderReversed.value) { - tokens.fromGroup } else { - tokens.toGroup - }.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> - accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> - accountSwapCurrency.cryptoCurrencyStatus.currency.id.value == id + fromToken = initialFromStatus + fromAccount = this.fromAccount + toToken = foundToken + toAccount = foundAccount + + val newToken = toToken.currency as? CryptoCurrency.Coin + if (newToken != null) { + subscribeToCoinBalanceUpdates( + userWalletId = userWalletId, + coin = newToken, + isFromCurrency = false, + ) + } else { + toTokenBalanceJobHolder.cancel() } } - return accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account + + if (dataState.fromCryptoCurrency != null && dataState.tokensDataState != null) { + isAmountChangedByUser = true + } + + dataState = dataState.copy( + fromCryptoCurrency = fromToken, + fromAccount = fromAccount, + toCryptoCurrency = toToken, + toAccount = toAccount, + selectedProvider = null, + ) + swapRouter.openScreen(SwapNavScreen.Main) + if (handleSwapNotSupported( + state = tokens, + fromToken = fromToken, + toToken = toToken, + fromAccount = fromAccount, + toAccount = toAccount, + ) + ) { + return + } + modelScope.launch { + TangemLogger.i( + "updateFeePaidCryptoCurrencyFor: id = ${fromToken.currency.id}, " + + "isOrderReversed: ${isOrderReversed.value}", + ) + if ((uiState.sendCardData as? SwapCardState.SwapCardData)?.type is TransactionCardType.ReadOnly) { + uiState = stateBuilder.createInitialLoadingState( + initialCurrencyFrom = fromToken.currency, + initialCurrencyTo = toToken.currency, + fromNetworkInfo = fromToken.currency.getNetworkInfo(), + ) + } + updateFeePaidCryptoCurrencyFor(fromToken) + startLoadingQuotes( + fromToken = fromToken, + fromAccount = fromAccount, + toToken = toToken, + toAccount = toAccount, + amount = lastAmount.value, + reduceBalanceBy = lastReducedBalanceBy.value, + toProvidersList = findSwapProviders(fromToken, toToken), + ) + } + updateTokensState(tokens) } @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -1923,8 +1921,8 @@ internal class SwapModel @Inject constructor( state: TokensDataStateExpress, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, ): Boolean { val selectedCurrency = if (isOrderReversed.value) fromToken else toToken if (isTokenAvailableForSwap(state, selectedCurrency, isOrderReversed.value)) return false @@ -1972,8 +1970,12 @@ internal class SwapModel @Inject constructor( } } + private fun isTangemPayWithdrawal(): Boolean { + return tangemPayInput?.isWithdrawal == true || dataState.fromAccount is Account.Payment + } + private fun List.filterForTangemPayWithdrawal(): List { - return if (tangemPayInput?.isWithdrawal == true) { + return if (isTangemPayWithdrawal()) { filter { it.type == ExchangeProviderType.CEX } } else { this diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt index f352427c56..1c031215dd 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt @@ -15,8 +15,8 @@ data class SwapProcessDataState( val fromCryptoCurrency: CryptoCurrencyStatus? = null, val toCryptoCurrency: CryptoCurrencyStatus? = null, val feePaidCryptoCurrency: CryptoCurrencyStatus? = null, - val fromAccount: Account.CryptoPortfolio? = null, - val toAccount: Account.CryptoPortfolio? = null, + val fromAccount: Account? = null, + val toAccount: Account? = null, // Amount from input val amount: String? = null, val reduceBalanceBy: BigDecimal = BigDecimal.ZERO, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6f704854d9..0ebbb39c1e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.ui import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.ui.account.AccountIconUM import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toUM @@ -177,8 +178,8 @@ internal class StateBuilder( uiStateHolder: SwapStateHolder, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, mainTokenId: String, ): SwapStateHolder { val canSelectSendToken = mainTokenId != fromToken.currency.id.value @@ -241,8 +242,8 @@ internal class StateBuilder( fromToken: CryptoCurrency, toToken: CryptoCurrency, mainTokenId: String, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, ): SwapStateHolder { val canSelectSendToken = mainTokenId != fromToken.id.value val canSelectReceiveToken = mainTokenId != toToken.id.value @@ -501,7 +502,7 @@ internal class StateBuilder( swapProvider: SwapProvider, fromToken: TokenSwapInfo, toToken: CryptoCurrencyStatus?, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, includeFeeInAmount: IncludeFeeInAmount, expressDataError: ExpressDataError, isReverseSwapPossible: Boolean, @@ -618,7 +619,7 @@ internal class StateBuilder( emptyAmountState: SwapState.EmptyAmountState, fromTokenStatus: CryptoCurrencyStatus, toTokenStatus: CryptoCurrencyStatus?, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, isReverseSwapPossible: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -693,7 +694,7 @@ internal class StateBuilder( amountFormatted: String, amountRaw: String, fromToken: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, minTxAmount: BigDecimal?, ): SwapStateHolder { if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState @@ -1327,30 +1328,37 @@ internal class StateBuilder( return FCA_RESTRICTED_PROVIDER_IDS.contains(providerId) } - private fun getFromCardAccountTitle(fromAccount: Account.CryptoPortfolio?): AccountTitleUM { + private fun getFromCardAccountTitle(fromAccount: Account?): AccountTitleUM { return if (fromAccount != null && isAccountsModeProvider()) { AccountTitleUM.Account( prefixText = resourceReference(R.string.common_from), name = fromAccount.accountName.toUM().value, - icon = CryptoPortfolioIconConverter.convert(fromAccount.icon), + icon = fromAccount.toIconUM(), ) } else { AccountTitleUM.Text(resourceReference(R.string.swapping_from_title)) } } - private fun getToCardAccountTitle(toAccount: Account.CryptoPortfolio?): AccountTitleUM { + private fun getToCardAccountTitle(toAccount: Account?): AccountTitleUM { return if (toAccount != null && isAccountsModeProvider()) { AccountTitleUM.Account( prefixText = resourceReference(R.string.common_to), name = toAccount.accountName.toUM().value, - icon = CryptoPortfolioIconConverter.convert(toAccount.icon), + icon = toAccount.toIconUM(), ) } else { AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)) } } + private fun Account.toIconUM(): AccountIconUM { + return when (this) { + is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(icon) + is Account.Payment -> AccountIconUM.Payment + } + } + private fun getChangeCardsButtonState(isReverseSwapPossible: Boolean) = if (isReverseSwapPossible) { ChangeCardsButtonState.ENABLED } else {