diff --git a/common/ui/src/test/kotlin/com/tangem/common/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverterTest.kt b/common/ui/src/test/kotlin/com/tangem/common/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverterTest.kt
new file mode 100644
index 0000000000..f3a3e6a414
--- /dev/null
+++ b/common/ui/src/test/kotlin/com/tangem/common/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverterTest.kt
@@ -0,0 +1,282 @@
+package com.tangem.common.ui.components.currency.icon.converter
+
+import com.google.common.truth.Truth.assertThat
+import com.tangem.common.ui.extensions.networkIconResId
+import com.tangem.core.ui.components.currency.icon.CurrencyIconState
+import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.domain.models.currency.CryptoCurrencyStatus
+import com.tangem.domain.models.network.Network
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+internal class CryptoCurrencyToIconStateConverterTest {
+
+ private val sut = CryptoCurrencyToIconStateConverter(isAvailable = true)
+ private val sutUnavailable = CryptoCurrencyToIconStateConverter(isAvailable = false)
+
+ @BeforeEach
+ fun setUp() {
+ mockkStatic("com.tangem.common.ui.extensions.NetworkIconExtKt")
+ }
+
+ @AfterEach
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ // region public API — convert(value: CryptoCurrencyStatus)
+
+ @Test
+ fun `GIVEN coin status WHEN convert THEN return CoinIcon with currency and network fields`() {
+ val coin = buildCoin(
+ isTestnet = false,
+ isCustom = false,
+ iconUrl = "https://example.com/eth.png",
+ )
+ val status = buildStatus(currency = coin, isError = false)
+
+ val result = sut.convert(status)
+
+ assertThat(result).isEqualTo(
+ CurrencyIconState.CoinIcon(
+ url = "https://example.com/eth.png",
+ fallbackResId = NETWORK_ICON_RES_ID,
+ isGrayscale = false,
+ shouldShowCustomBadge = false,
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN token status WHEN convert THEN return TokenIcon with currency and network fields`() {
+ val token = buildToken(
+ isTestnet = false,
+ isCustom = false,
+ iconUrl = "https://example.com/usdt.png",
+ contractAddress = USDT_CONTRACT,
+ )
+ val status = buildStatus(currency = token, isError = false)
+
+ val result = sut.convert(status) as CurrencyIconState.TokenIcon
+
+ assertThat(result.url).isEqualTo("https://example.com/usdt.png")
+ assertThat(result.topBadgeIconResId).isEqualTo(NETWORK_ICON_RES_ID)
+ assertThat(result.isGrayscale).isFalse()
+ assertThat(result.shouldShowCustomBadge).isFalse()
+ }
+
+ // endregion
+
+ // region public API — convert(currency: CryptoCurrency)
+
+ @Test
+ fun `GIVEN coin currency WHEN convert without status THEN return CoinIcon with isUnreachable=false`() {
+ val coin = buildCoin(isTestnet = false, isCustom = false, iconUrl = "url")
+
+ val result = sut.convert(currency = coin) as CurrencyIconState.CoinIcon
+
+ assertThat(result.isGrayscale).isFalse()
+ assertThat(result.url).isEqualTo("url")
+ }
+
+ @Test
+ fun `GIVEN token currency WHEN convert without status THEN return TokenIcon with isErrorStatus=false`() {
+ val token = buildToken(
+ isTestnet = false,
+ isCustom = false,
+ iconUrl = "url",
+ contractAddress = USDT_CONTRACT,
+ )
+
+ val result = sut.convert(currency = token) as CurrencyIconState.TokenIcon
+
+ assertThat(result.isGrayscale).isFalse()
+ assertThat(result.url).isEqualTo("url")
+ }
+
+ // endregion
+
+ // region public API — convertCustom
+
+ @Test
+ fun `GIVEN coin status with custom flag WHEN convertCustom with forceGrayscale and badge off THEN both flags propagate`() {
+ val coin = buildCoin(isTestnet = false, isCustom = true, iconUrl = "url")
+ val status = buildStatus(currency = coin, isError = false)
+
+ val result = sut.convertCustom(
+ value = status,
+ forceGrayscale = true,
+ showCustomTokenBadge = false,
+ ) as CurrencyIconState.CoinIcon
+
+ assertThat(result.isGrayscale).isTrue()
+ assertThat(result.shouldShowCustomBadge).isFalse()
+ }
+
+ @Test
+ fun `GIVEN token status WHEN convertCustom with forceGrayscale THEN TokenIcon is grayscale`() {
+ val token = buildToken(
+ isTestnet = false,
+ isCustom = false,
+ iconUrl = "url",
+ contractAddress = USDT_CONTRACT,
+ )
+ val status = buildStatus(currency = token, isError = false)
+
+ val result = sut.convertCustom(
+ value = status,
+ forceGrayscale = true,
+ showCustomTokenBadge = true,
+ ) as CurrencyIconState.TokenIcon
+
+ assertThat(result.isGrayscale).isTrue()
+ }
+
+ // endregion
+
+ // region getIconStateForCoin — isGrayscale matrix
+
+ @Test
+ fun `GIVEN no override and live data WHEN convert coin THEN isGrayscale is false`() {
+ val coin = buildCoin(isTestnet = false, isCustom = false, iconUrl = "url")
+ val status = buildStatus(currency = coin, isError = false)
+
+ val result = sut.convert(status) as CurrencyIconState.CoinIcon
+
+ assertThat(result.isGrayscale).isFalse()
+ }
+
+ @Test
+ fun `GIVEN testnet network WHEN convert coin THEN isGrayscale is true`() {
+ val coin = buildCoin(isTestnet = true, isCustom = false, iconUrl = "url")
+ val status = buildStatus(currency = coin, isError = false)
+
+ val result = sut.convert(status) as CurrencyIconState.CoinIcon
+
+ assertThat(result.isGrayscale).isTrue()
+ }
+
+ @Test
+ fun `GIVEN error status WHEN convert coin THEN isGrayscale is true`() {
+ val coin = buildCoin(isTestnet = false, isCustom = false, iconUrl = "url")
+ val status = buildStatus(currency = coin, isError = true)
+
+ val result = sut.convert(status) as CurrencyIconState.CoinIcon
+
+ assertThat(result.isGrayscale).isTrue()
+ }
+
+ @Test
+ fun `GIVEN converter not available WHEN convert coin THEN isGrayscale is true`() {
+ val coin = buildCoin(isTestnet = false, isCustom = false, iconUrl = "url")
+ val status = buildStatus(currency = coin, isError = false)
+
+ val result = sutUnavailable.convert(status) as CurrencyIconState.CoinIcon
+
+ assertThat(result.isGrayscale).isTrue()
+ }
+
+ @Test
+ fun `GIVEN forceGrayscale flag WHEN convertCustom coin THEN isGrayscale is true`() {
+ val coin = buildCoin(isTestnet = false, isCustom = false, iconUrl = "url")
+ val status = buildStatus(currency = coin, isError = false)
+
+ val result = sut.convertCustom(
+ value = status,
+ forceGrayscale = true,
+ showCustomTokenBadge = true,
+ ) as CurrencyIconState.CoinIcon
+
+ assertThat(result.isGrayscale).isTrue()
+ }
+
+ // endregion
+
+ // region getIconStateForToken — branches
+
+ @Test
+ fun `GIVEN custom token without iconUrl WHEN convert THEN return CustomTokenIcon`() {
+ val token = buildToken(
+ isTestnet = false,
+ isCustom = true,
+ iconUrl = null,
+ contractAddress = USDT_CONTRACT,
+ )
+ val status = buildStatus(currency = token, isError = false)
+
+ val result = sut.convert(status) as CurrencyIconState.CustomTokenIcon
+
+ assertThat(result.topBadgeIconResId).isEqualTo(NETWORK_ICON_RES_ID)
+ assertThat(result.isGrayscale).isFalse()
+ assertThat(result.shouldShowCustomBadge).isTrue()
+ }
+
+ @Test
+ fun `GIVEN custom token with iconUrl WHEN convert THEN return TokenIcon with custom badge`() {
+ val token = buildToken(
+ isTestnet = false,
+ isCustom = true,
+ iconUrl = "https://example.com/usdt.png",
+ contractAddress = USDT_CONTRACT,
+ )
+ val status = buildStatus(currency = token, isError = false)
+
+ val result = sut.convert(status) as CurrencyIconState.TokenIcon
+
+ assertThat(result.url).isEqualTo("https://example.com/usdt.png")
+ assertThat(result.shouldShowCustomBadge).isTrue()
+ }
+
+ // endregion
+
+ // region helpers
+
+ private fun buildCoin(
+ isTestnet: Boolean,
+ isCustom: Boolean,
+ iconUrl: String?,
+ ): CryptoCurrency.Coin {
+ val network: Network = mockk { every { this@mockk.isTestnet } returns isTestnet }
+ val coin: CryptoCurrency.Coin = mockk()
+ every { coin.network } returns network
+ every { coin.iconUrl } returns iconUrl
+ every { coin.isCustom } returns isCustom
+ every { coin.networkIconResId } returns NETWORK_ICON_RES_ID
+ return coin
+ }
+
+ private fun buildToken(
+ isTestnet: Boolean,
+ isCustom: Boolean,
+ iconUrl: String?,
+ contractAddress: String,
+ ): CryptoCurrency.Token {
+ val network: Network = mockk { every { this@mockk.isTestnet } returns isTestnet }
+ val token: CryptoCurrency.Token = mockk()
+ every { token.network } returns network
+ every { token.iconUrl } returns iconUrl
+ every { token.isCustom } returns isCustom
+ every { token.contractAddress } returns contractAddress
+ every { token.networkIconResId } returns NETWORK_ICON_RES_ID
+ return token
+ }
+
+ private fun buildStatus(currency: CryptoCurrency, isError: Boolean): CryptoCurrencyStatus {
+ val value: CryptoCurrencyStatus.Value = mockk { every { this@mockk.isError } returns isError }
+ return CryptoCurrencyStatus(currency = currency, value = value)
+ }
+
+ // endregion
+
+ private companion object {
+ const val NETWORK_ICON_RES_ID = 1234
+ const val USDT_CONTRACT = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
+ }
+}
\ No newline at end of file
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index 0b2a2f4972..3ed11e5ea1 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -1212,6 +1212,8 @@
Unknown Parameters
Credit card or bank account
Share your address or QR-code
+ Sell crypto securely
+ Send to another wallet
Between your portfolios
Other
Quick top up
@@ -1352,8 +1354,6 @@
Target account is not created. Please change the amount to send.
The amount to send must be at least %s
Leave %s
- A trustline for %s is required first.
- Can\'t receive token
Reduce by %s
Reduce to %s
Kindly be aware that your transaction may experience delays under specific fee settings
@@ -1626,6 +1626,8 @@
not available
Not enough liquidity for this trade.\nReduce the amount or choose another provider.
Trade too large
+ Transfer
+ Transfer...
We would be happy to receive your feedback
Tangem Pay is now in beta
Unable to rename card
@@ -1766,6 +1768,8 @@
Pay exactly what you see
A separate payment account will be created without disclosing your addresses and assets
Unrivaled privacy
+ And link a payment card to it
+ We\'ll set up a wallet
Get your free Tangem Pay Card in minutes
Pay Support
Payment account
@@ -1815,7 +1819,7 @@
Approval has been revoked. Your funds remain in Yield mode. To perform actions, please go to Yield mode and grant permission again.
Available balance
Total balance
- Earn up to %s a year
+ Up to %s APR
Generate XPUB
Hide
You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.
@@ -2133,7 +2137,7 @@
MATIC to POL Migration
- Use your card or ring to get an address for %d network
- - Use your card or ring to get an addresses for %d networks
+ - Use your card or ring to get addresses for %d networks
Some addresses are missing
The network is currently unreachable. Please try again later.
diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts
index 6631516744..769a037ad0 100644
--- a/features/swap/domain/build.gradle.kts
+++ b/features/swap/domain/build.gradle.kts
@@ -50,11 +50,7 @@ dependencies {
implementation(projects.domain.account.status)
implementation(projects.domain.visa)
implementation(projects.domain.visa.models)
-
- implementation(projects.features.swap.api)
- implementation(projects.features.swap.domain.api)
- implementation(projects.features.swap.domain.models)
- implementation(projects.libs.blockchainSdk)
+ implementation(projects.domain.balanceHiding)
/** Core modules */
implementation(projects.core.utils)
@@ -63,6 +59,10 @@ dependencies {
/** Feature Apis */
implementation(projects.features.wallet.api)
+ implementation(projects.features.swap.api)
+ implementation(projects.features.swap.domain.api)
+ implementation(projects.features.swap.domain.models)
+ implementation(projects.libs.blockchainSdk)
/** Other Libraries **/
implementation(deps.kotlin.coroutines)
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt
index 0e96260f26..963d0ab92d 100644
--- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt
@@ -7,6 +7,8 @@ import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.SwapInteractorImpl
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.features.swap.SwapFeatureToggles
+import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor
+import com.tangem.feature.swap.domain.transfer.SwapTransferInteractorImpl
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -42,4 +44,8 @@ internal interface SwapDomainBindModule {
@Binds
@Singleton
fun provideSwapInteractor(swapInteractor: SwapInteractorImpl): SwapInteractor
+
+ @Binds
+ @Singleton
+ fun provideSwapTransferInteractor(swapTransferInteractor: SwapTransferInteractorImpl): SwapTransferInteractor
}
\ No newline at end of file
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 562869ee5a..bfafdfeadf 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
@@ -3,7 +3,9 @@ package com.tangem.feature.swap.domain.models.ui
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.core.ui.extensions.TextReference
+import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
+import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.swap.models.SwapCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.feature.swap.domain.TransactionFeeResult
@@ -37,7 +39,20 @@ sealed interface SwapState {
val swapProvider: SwapProvider,
) : SwapState
- data class EmptyAmountState(val zeroAmountEquivalent: TextReference) : SwapState
+ data class Transfer(
+ val userWallet: UserWallet,
+ val fromTokenInfo: TokenSwapInfo,
+ val toTokenInfo: TokenSwapInfo,
+ val txFee: TxFeeState,
+ val appCurrency: AppCurrency,
+ val isBalanceHidden: Boolean,
+ val isAccountsMode: Boolean,
+ ) : SwapState
+
+ data class EmptyAmountState(
+ val zeroAmountEquivalent: TextReference,
+ val isTransferMode: Boolean = false,
+ ) : SwapState
data class SwapError(
val fromTokenInfo: TokenSwapInfo,
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractor.kt
new file mode 100644
index 0000000000..788080bc93
--- /dev/null
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractor.kt
@@ -0,0 +1,16 @@
+package com.tangem.feature.swap.domain.transfer
+
+import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.domain.swap.models.SwapCurrencyStatus
+import com.tangem.feature.swap.domain.models.ui.SwapState
+
+interface SwapTransferInteractor {
+
+ suspend fun updateTransfer(
+ fromSwapCurrencyStatus: SwapCurrencyStatus,
+ toSwapCurrencyStatus: SwapCurrencyStatus,
+ fromTokenAmount: String,
+ ): SwapState
+
+ fun shouldTransferInsteadOfSwap(fromSwapCurrency: CryptoCurrency, toSwapCurrency: CryptoCurrency): Boolean
+}
\ No newline at end of file
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt
new file mode 100644
index 0000000000..e66862cd6f
--- /dev/null
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt
@@ -0,0 +1,97 @@
+package com.tangem.feature.swap.domain.transfer
+
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.format.bigdecimal.fiat
+import com.tangem.core.ui.format.bigdecimal.format
+import com.tangem.core.ui.utils.parseBigDecimalOrNull
+import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
+import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
+import com.tangem.domain.appcurrency.extenstions.unwrap
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
+import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.domain.swap.models.SwapCurrencyStatus
+import com.tangem.feature.swap.domain.models.SwapAmount
+import com.tangem.feature.swap.domain.models.ui.SwapState
+import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
+import com.tangem.feature.swap.domain.models.ui.TxFeeState
+import com.tangem.features.swap.SwapFeatureToggles
+import com.tangem.utils.extensions.orZero
+import kotlinx.coroutines.flow.first
+import java.math.BigDecimal
+import javax.inject.Inject
+
+class SwapTransferInteractorImpl @Inject constructor(
+ private val swapFeatureToggles: SwapFeatureToggles,
+ private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
+ private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
+ private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
+) : SwapTransferInteractor {
+
+ override suspend fun updateTransfer(
+ fromSwapCurrencyStatus: SwapCurrencyStatus,
+ toSwapCurrencyStatus: SwapCurrencyStatus,
+ fromTokenAmount: String,
+ ): SwapState {
+ val fromToken = fromSwapCurrencyStatus.currency
+ val toToken = toSwapCurrencyStatus.currency
+ val appCurrency = getSelectedAppCurrencyUseCase.unwrap()
+ val isBalanceHidden = getBalanceHidingSettingsUseCase.isBalanceHidden().first()
+ val isAccountsMode = isAccountsModeEnabledUseCase.invokeSync()
+ val fromTokenAmountValue = fromTokenAmount.parseBigDecimalOrNull() ?: return createEmptyAmountState(appCurrency)
+ val fromTokenAmountFiat = fromSwapCurrencyStatus.status.value.fiatRate.orZero() * fromTokenAmountValue
+
+ val fromTokenInfo = TokenSwapInfo(
+ tokenAmount = SwapAmount(fromTokenAmountValue, fromToken.decimals),
+ swapCurrencyStatus = fromSwapCurrencyStatus,
+ amountFiat = fromTokenAmountFiat,
+ )
+ // it is the same with fromToken
+ val toTokenInfo = TokenSwapInfo(
+ tokenAmount = SwapAmount(fromTokenAmountValue, toToken.decimals),
+ swapCurrencyStatus = toSwapCurrencyStatus,
+ amountFiat = fromTokenAmountFiat,
+ )
+ return SwapState.Transfer(
+ userWallet = toSwapCurrencyStatus.userWallet,
+ fromTokenInfo = fromTokenInfo,
+ toTokenInfo = toTokenInfo,
+ txFee = TxFeeState.Empty, // todo Will be implemented in [REDACTED_TASK_KEY]
+ appCurrency = appCurrency,
+ isBalanceHidden = isBalanceHidden,
+ isAccountsMode = isAccountsMode,
+ )
+ }
+
+ private fun createEmptyAmountState(appCurrency: AppCurrency): SwapState.EmptyAmountState {
+ return SwapState.EmptyAmountState(
+ zeroAmountEquivalent = stringReference(
+ BigDecimal.ZERO.format {
+ fiat(
+ fiatCurrencyCode = appCurrency.code,
+ fiatCurrencySymbol = appCurrency.symbol,
+ )
+ },
+ ),
+ isTransferMode = true,
+ )
+ }
+
+ override fun shouldTransferInsteadOfSwap(
+ fromSwapCurrency: CryptoCurrency,
+ toSwapCurrency: CryptoCurrency,
+ ): Boolean {
+ if (swapFeatureToggles.isSwapSwitchToTransferEnabled.not()) return false
+ val isSameCurrency = when {
+ fromSwapCurrency is CryptoCurrency.Coin && toSwapCurrency is CryptoCurrency.Coin -> {
+ fromSwapCurrency.network.rawId == toSwapCurrency.network.rawId
+ }
+ fromSwapCurrency is CryptoCurrency.Token && toSwapCurrency is CryptoCurrency.Token -> {
+ val isContractAddressSame = fromSwapCurrency.contractAddress == toSwapCurrency.contractAddress
+ fromSwapCurrency.network.rawId == toSwapCurrency.network.rawId && isContractAddressSame
+ }
+ else -> false
+ }
+ return isSameCurrency
+ }
+}
\ No newline at end of file
diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt
new file mode 100644
index 0000000000..f67cfeb9b2
--- /dev/null
+++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt
@@ -0,0 +1,287 @@
+package com.tangem.feature.swap.domain.transfer
+
+import arrow.core.right
+import com.google.common.truth.Truth.assertThat
+import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
+import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
+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.UserWallet
+import com.tangem.domain.swap.models.SwapCurrencyStatus
+import com.tangem.feature.swap.domain.models.SwapAmount
+import com.tangem.feature.swap.domain.models.ui.SwapState
+import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
+import com.tangem.feature.swap.domain.models.ui.TxFeeState
+import com.tangem.features.swap.SwapFeatureToggles
+import io.mockk.*
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import java.math.BigDecimal
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+internal class SwapTransferInteractorImplTest {
+
+ private val swapFeatureToggles: SwapFeatureToggles = mockk()
+ private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk()
+ private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase = mockk()
+ private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk()
+
+ private val sut = SwapTransferInteractorImpl(
+ swapFeatureToggles = swapFeatureToggles,
+ getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
+ getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase,
+ isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase,
+ )
+
+ @AfterEach
+ fun tearDown() {
+ clearAllMocks()
+ }
+
+ // region updateTransfer
+
+ @Test
+ fun `GIVEN unparsable amount WHEN updateTransfer THEN return EmptyAmountState in transfer mode`() = runTest {
+ val appCurrency = AppCurrency(code = "EUR", name = "Euro", symbol = "€")
+ val fromCurrencyStatus = buildCurrencyStatus(
+ rawCurrencyId = FROM_RAW_CURRENCY_ID,
+ decimals = FROM_DECIMALS,
+ )
+ val toCurrencyStatus = buildCurrencyStatus(
+ rawCurrencyId = TO_RAW_CURRENCY_ID,
+ decimals = TO_DECIMALS,
+ )
+ every { getSelectedAppCurrencyUseCase() } returns flowOf(appCurrency.right())
+ every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(false)
+ coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
+
+ val result = sut.updateTransfer(
+ fromSwapCurrencyStatus = fromCurrencyStatus,
+ toSwapCurrencyStatus = toCurrencyStatus,
+ fromTokenAmount = "abc",
+ )
+
+ assertThat(result).isInstanceOf(SwapState.EmptyAmountState::class.java)
+ assertThat((result as SwapState.EmptyAmountState).isTransferMode).isTrue()
+ verify { getSelectedAppCurrencyUseCase() }
+ verify { getBalanceHidingSettingsUseCase.isBalanceHidden() }
+ coVerify { isAccountsModeEnabledUseCase.invokeSync() }
+ }
+
+ @Test
+ fun `GIVEN valid amount WHEN updateTransfer THEN return Transfer state with mirrored from-and-to swap info`() =
+ runTest {
+ val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$")
+ val userWallet: UserWallet = mockk()
+ val fromCurrencyStatus = buildCurrencyStatus(
+ rawCurrencyId = FROM_RAW_CURRENCY_ID,
+ decimals = FROM_DECIMALS,
+ fiatRate = BigDecimal.TEN,
+ )
+ val toCurrencyStatus = buildCurrencyStatus(
+ rawCurrencyId = TO_RAW_CURRENCY_ID,
+ decimals = TO_DECIMALS,
+ userWallet = userWallet,
+ )
+ every { getSelectedAppCurrencyUseCase() } returns flowOf(appCurrency.right())
+ every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(true)
+ coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns true
+
+ val result = sut.updateTransfer(
+ fromSwapCurrencyStatus = fromCurrencyStatus,
+ toSwapCurrencyStatus = toCurrencyStatus,
+ fromTokenAmount = "1,5",
+ )
+
+ val expectedAmount = BigDecimal("1.5")
+ val expectedFiat = BigDecimal("15.0")
+ val expected = SwapState.Transfer(
+ userWallet = userWallet,
+ fromTokenInfo = TokenSwapInfo(
+ tokenAmount = SwapAmount(expectedAmount, FROM_DECIMALS),
+ swapCurrencyStatus = fromCurrencyStatus,
+ amountFiat = expectedFiat,
+ ),
+ toTokenInfo = TokenSwapInfo(
+ tokenAmount = SwapAmount(expectedAmount, TO_DECIMALS),
+ swapCurrencyStatus = toCurrencyStatus,
+ amountFiat = expectedFiat,
+ ),
+ txFee = TxFeeState.Empty,
+ appCurrency = appCurrency,
+ isBalanceHidden = true,
+ isAccountsMode = true,
+ )
+ assertThat(result).isEqualTo(expected)
+ coVerify { isAccountsModeEnabledUseCase.invokeSync() }
+ verify { getBalanceHidingSettingsUseCase.isBalanceHidden() }
+ }
+
+ // endregion
+
+ // region shouldTransferInsteadOfSwap
+
+ @Test
+ fun `GIVEN feature toggle disabled WHEN shouldTransferInsteadOfSwap THEN return false`() {
+ every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns false
+
+ val result = sut.shouldTransferInsteadOfSwap(
+ fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
+ toSwapCurrency = buildCoin(networkRawId = ETHEREUM),
+ )
+
+ assertThat(result).isFalse()
+ verify { swapFeatureToggles.isSwapSwitchToTransferEnabled }
+ }
+
+ @Test
+ fun `GIVEN both coins on the same network WHEN shouldTransferInsteadOfSwap THEN return true`() {
+ every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
+
+ val result = sut.shouldTransferInsteadOfSwap(
+ fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
+ toSwapCurrency = buildCoin(networkRawId = ETHEREUM),
+ )
+
+ assertThat(result).isTrue()
+ }
+
+ @Test
+ fun `GIVEN coins on different networks WHEN shouldTransferInsteadOfSwap THEN return false`() {
+ every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
+
+ val result = sut.shouldTransferInsteadOfSwap(
+ fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
+ toSwapCurrency = buildCoin(networkRawId = POLYGON),
+ )
+
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun `GIVEN tokens with same network and same contract WHEN shouldTransferInsteadOfSwap THEN return true`() {
+ every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
+
+ val result = sut.shouldTransferInsteadOfSwap(
+ fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
+ toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
+ )
+
+ assertThat(result).isTrue()
+ }
+
+ @Test
+ fun `GIVEN tokens with same network but different contract WHEN shouldTransferInsteadOfSwap THEN return false`() {
+ every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
+
+ val result = sut.shouldTransferInsteadOfSwap(
+ fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
+ toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDC_CONTRACT),
+ )
+
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun `GIVEN tokens with same contract but different network WHEN shouldTransferInsteadOfSwap THEN return false`() {
+ every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
+
+ val result = sut.shouldTransferInsteadOfSwap(
+ fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
+ toSwapCurrency = buildToken(networkRawId = POLYGON, contractAddress = USDT_CONTRACT),
+ )
+
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun `GIVEN coin from and token to WHEN shouldTransferInsteadOfSwap THEN return false`() {
+ every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
+
+ val result = sut.shouldTransferInsteadOfSwap(
+ fromSwapCurrency = buildCoin(networkRawId = ETHEREUM),
+ toSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
+ )
+
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun `GIVEN token from and coin to WHEN shouldTransferInsteadOfSwap THEN return false`() {
+ every { swapFeatureToggles.isSwapSwitchToTransferEnabled } returns true
+
+ val result = sut.shouldTransferInsteadOfSwap(
+ fromSwapCurrency = buildToken(networkRawId = ETHEREUM, contractAddress = USDT_CONTRACT),
+ toSwapCurrency = buildCoin(networkRawId = ETHEREUM),
+ )
+
+ assertThat(result).isFalse()
+ }
+
+ // endregion
+
+ // region helpers
+
+ private fun buildCoin(networkRawId: String): CryptoCurrency.Coin {
+ val network: Network = mockk { every { rawId } returns networkRawId }
+ return mockk {
+ every { this@mockk.network } returns network
+ }
+ }
+
+ private fun buildToken(networkRawId: String, contractAddress: String): CryptoCurrency.Token {
+ val network: Network = mockk { every { rawId } returns networkRawId }
+ return mockk {
+ every { this@mockk.network } returns network
+ every { this@mockk.contractAddress } returns contractAddress
+ }
+ }
+
+ private fun buildCurrencyStatus(
+ rawCurrencyId: CryptoCurrency.RawID?,
+ decimals: Int,
+ fiatRate: BigDecimal = BigDecimal.ZERO,
+ userWallet: UserWallet = mockk(),
+ ): SwapCurrencyStatus {
+ val currencyId: CryptoCurrency.ID = mockk {
+ every { this@mockk.rawCurrencyId } returns rawCurrencyId
+ }
+ val currency: CryptoCurrency.Coin = mockk {
+ every { this@mockk.id } returns currencyId
+ every { this@mockk.decimals } returns decimals
+ }
+ val currencyValue: CryptoCurrencyStatus.Value = mockk {
+ every { this@mockk.fiatRate } returns fiatRate
+ }
+ val status: CryptoCurrencyStatus = mockk {
+ every { this@mockk.value } returns currencyValue
+ }
+ return mockk {
+ every { this@mockk.currency } returns currency
+ every { this@mockk.userWallet } returns userWallet
+ every { this@mockk.status } returns status
+ }
+ }
+
+ // endregion
+
+ private companion object {
+ const val ETHEREUM = "ethereum"
+ const val POLYGON = "polygon"
+ const val USDT_CONTRACT = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
+ const val USDC_CONTRACT = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
+ const val FROM_DECIMALS = 18
+ const val TO_DECIMALS = 6
+ val USD_QUOTE: BigDecimal = BigDecimal("2000")
+ val FROM_RAW_CURRENCY_ID = CryptoCurrency.RawID(value = "eth")
+ val TO_RAW_CURRENCY_ID = CryptoCurrency.RawID(value = "matic")
+ }
+}
\ No newline at end of file
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 36da88d7be..8f249d3085 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
@@ -81,15 +81,15 @@ import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
+import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.ui.*
-import com.tangem.feature.swap.models.SwapAlertUM
-import com.tangem.feature.swap.models.SwapStateHolder
-import com.tangem.feature.swap.models.TokenSelectionDirection
-import com.tangem.feature.swap.models.UiActions
+import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor
+import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.SwapNotificationUM
import com.tangem.feature.swap.router.SwapRoute
import com.tangem.feature.swap.ui.StateBuilder
+import com.tangem.feature.swap.ui.transfer.SwapTransferStateBuilder
import com.tangem.feature.swap.utils.formatToUIRepresentation
import com.tangem.feature.swap.utils.getContractAddress
import com.tangem.features.approval.api.GiveApprovalComponent
@@ -142,6 +142,8 @@ internal class SwapModel @Inject constructor(
private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val swapInteractor: SwapInteractor,
+ private val swapTransferInteractor: SwapTransferInteractor,
+ private val swapTransferStateBuilder: SwapTransferStateBuilder,
private val urlOpener: UrlOpener,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val getPaymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase,
@@ -183,8 +185,9 @@ internal class SwapModel @Inject constructor(
),
)
+ private val actions = createUiActions()
private val stateBuilder = StateBuilder(
- actions = createUiActions(),
+ actions = actions,
isBalanceHiddenProvider = Provider { isBalanceHidden },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
isAccountsModeProvider = Provider { isAccountsMode },
@@ -565,6 +568,12 @@ internal class SwapModel @Inject constructor(
toSwapCurrencyStatus = newToSwapCurrencyStatus,
pairs = dataState.pairs,
)
+ val isUpdatedToTransferMode = isUpdatedToTransferMode(
+ fromSwapCurrencyStatus = newFromSwapCurrencyStatus,
+ toSwapCurrencyStatus = newToSwapCurrencyStatus,
+ fromTokenAmount = lastAmount.value,
+ )
+ if (isUpdatedToTransferMode) return@launch
if (toProvidersList.isEmpty()) {
handleSwapNotSupported(
fromSwapCurrencyStatus = newFromSwapCurrencyStatus,
@@ -584,6 +593,12 @@ internal class SwapModel @Inject constructor(
}
private fun initSwapPairs(fromSwapCurrencyStatus: SwapCurrencyStatus, toSwapCurrencyStatus: SwapCurrencyStatus) {
+ val isUpdatedToTransferMode = isUpdatedToTransferMode(
+ fromSwapCurrencyStatus = fromSwapCurrencyStatus,
+ toSwapCurrencyStatus = toSwapCurrencyStatus,
+ fromTokenAmount = lastAmount.value,
+ )
+ if (isUpdatedToTransferMode) return
modelScope.launch {
uiState = stateBuilder.createInitialLoadingState(
uiStateHolder = uiState,
@@ -620,32 +635,11 @@ internal class SwapModel @Inject constructor(
toSwapCurrencyStatus = toSwapCurrencyStatus,
)
} else {
- uiState = stateBuilder.updateCurrenciesState(
- uiStateHolder = uiState,
- emptyAmountState = SwapState.EmptyAmountState(
- zeroAmountEquivalent = stringReference(
- BigDecimal.ZERO.format {
- fiat(
- fiatCurrencyCode = selectedAppCurrencyFlow.value.code,
- fiatCurrencySymbol = selectedAppCurrencyFlow.value.symbol,
- )
- },
- ),
- ),
+ updateCurrenciesStateAndStartLoadingQuotes(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
- shouldResetAmount = false,
- )
- dataState = dataState.copy(
pairs = pairs,
- selectedPairProviders = providerList,
- )
- startLoadingQuotes(
- amount = lastAmount.value,
- reduceBalanceBy = lastReducedBalanceBy.value,
- fromSwapCurrencyStatus = fromSwapCurrencyStatus,
- toSwapCurrencyStatus = toSwapCurrencyStatus,
- toProvidersList = providerList,
+ providerList = providerList,
)
}
},
@@ -653,6 +647,81 @@ internal class SwapModel @Inject constructor(
}.saveIn(swapPairsJobHolder)
}
+ private fun updateCurrenciesStateAndStartLoadingQuotes(
+ fromSwapCurrencyStatus: SwapCurrencyStatus,
+ toSwapCurrencyStatus: SwapCurrencyStatus,
+ pairs: List,
+ providerList: List,
+ ) {
+ uiState = stateBuilder.updateCurrenciesState(
+ uiStateHolder = uiState,
+ emptyAmountState = SwapState.EmptyAmountState(
+ zeroAmountEquivalent = stringReference(
+ BigDecimal.ZERO.format {
+ fiat(
+ fiatCurrencyCode = selectedAppCurrencyFlow.value.code,
+ fiatCurrencySymbol = selectedAppCurrencyFlow.value.symbol,
+ )
+ },
+ ),
+ ),
+ fromSwapCurrencyStatus = fromSwapCurrencyStatus,
+ toSwapCurrencyStatus = toSwapCurrencyStatus,
+ shouldResetAmount = false,
+ )
+ dataState = dataState.copy(
+ pairs = pairs,
+ selectedPairProviders = providerList,
+ )
+ startLoadingQuotes(
+ amount = lastAmount.value,
+ reduceBalanceBy = lastReducedBalanceBy.value,
+ fromSwapCurrencyStatus = fromSwapCurrencyStatus,
+ toSwapCurrencyStatus = toSwapCurrencyStatus,
+ toProvidersList = providerList,
+ )
+ }
+
+ private fun isUpdatedToTransferMode(
+ fromSwapCurrencyStatus: SwapCurrencyStatus,
+ toSwapCurrencyStatus: SwapCurrencyStatus,
+ fromTokenAmount: String,
+ ): Boolean {
+ val shouldTransferInsteadOfSwap = swapTransferInteractor.shouldTransferInsteadOfSwap(
+ fromSwapCurrencyStatus.currency,
+ toSwapCurrencyStatus.currency,
+ )
+ if (shouldTransferInsteadOfSwap) {
+ modelScope.launch {
+ updateTransferUIState(fromSwapCurrencyStatus, toSwapCurrencyStatus, fromTokenAmount)
+ }
+ }
+ return shouldTransferInsteadOfSwap
+ }
+
+ private suspend fun updateTransferUIState(
+ fromSwapCurrencyStatus: SwapCurrencyStatus,
+ toSwapCurrencyStatus: SwapCurrencyStatus,
+ fromTokenAmount: String,
+ ) {
+ val swapState = swapTransferInteractor.updateTransfer(
+ fromSwapCurrencyStatus,
+ toSwapCurrencyStatus,
+ fromTokenAmount,
+ )
+ when (swapState) {
+ is SwapState.EmptyAmountState -> setupEmptyAmountUiState(swapState, fromSwapCurrencyStatus)
+ is SwapState.Transfer -> {
+ uiState = swapTransferStateBuilder.createTransferState(
+ actions = actions,
+ transferState = swapState,
+ uiStateHolder = uiState,
+ )
+ }
+ is SwapState.QuotesLoadedState, is SwapState.SwapError -> Unit
+ }
+ }
+
private fun retrySwapPairs(fromSwapCurrencyStatus: SwapCurrencyStatus, toSwapCurrencyStatus: SwapCurrencyStatus) {
if (swapPairsJobHolder.isActive) return
initSwapPairs(fromSwapCurrencyStatus, toSwapCurrencyStatus)
@@ -715,6 +784,12 @@ internal class SwapModel @Inject constructor(
val toSwapCurrencyStatus = dataState.toSwapCurrencyStatus
val amount = dataState.amount
if (fromSwapCurrencyStatus != null && toSwapCurrencyStatus != null && amount != null) {
+ val isUpdatedToTransferMode = isUpdatedToTransferMode(
+ fromSwapCurrencyStatus = fromSwapCurrencyStatus,
+ toSwapCurrencyStatus = toSwapCurrencyStatus,
+ fromTokenAmount = lastAmount.value,
+ )
+ if (isUpdatedToTransferMode) return
startLoadingQuotes(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
@@ -834,6 +909,7 @@ internal class SwapModel @Inject constructor(
sendAnalyticsForNotifications(provider, fromSwapCurrencyStatus.status, toSwapCurrencyStatus.status)
updatePermissionNotificationState(state)
}
+ is SwapState.Transfer -> Unit
is SwapState.EmptyAmountState -> {
setupEmptyAmountUiState(state, fromSwapCurrencyStatus)
lastPermissionNotificationTokens = null
@@ -1277,6 +1353,12 @@ internal class SwapModel @Inject constructor(
)
if (toSwapCurrencyStatus != null) {
+ val isUpdatedToTransferMode = isUpdatedToTransferMode(
+ fromSwapCurrencyStatus = fromSwapCurrencyStatus,
+ toSwapCurrencyStatus = toSwapCurrencyStatus,
+ fromTokenAmount = lastAmount.value,
+ )
+ if (isUpdatedToTransferMode) return@launch
if (toSwapCurrencyStatus.status.value.amount != null) {
isAmountChangedByUser = true
}
@@ -1430,6 +1512,9 @@ internal class SwapModel @Inject constructor(
)
}
},
+ onTransferClick = {
+ // TODO: Will be implemented in [REDACTED_TASK_KEY]
+ },
onChangeCardsClicked = {
onChangeCardsClicked()
analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked())
@@ -1556,18 +1641,21 @@ internal class SwapModel @Inject constructor(
}
private fun filterTokensFromSelector() {
- if (swapFeatureToggles.isSwapSwitchToTransferEnabled) return
val tokenFilter = { accountStatus: AccountStatus, currencyStatus: CryptoCurrencyStatus ->
if (currencyStatus.currency.isCustom) {
false
} else {
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus
val toSwapCurrencyStatus = dataState.toSwapCurrencyStatus
+ val shouldShowSameCoinsWithDifferentAddress = swapFeatureToggles.isSwapSwitchToTransferEnabled &&
+ fromSwapCurrencyStatus?.account?.accountId != accountStatus.accountId &&
+ fromSwapCurrencyStatus?.currency?.network?.rawId == toSwapCurrencyStatus?.currency?.network?.rawId
(fromSwapCurrencyStatus?.account?.accountId != accountStatus.accountId ||
fromSwapCurrencyStatus.currency.id != currencyStatus.currency.id) &&
(toSwapCurrencyStatus?.account?.accountId != accountStatus.accountId ||
- toSwapCurrencyStatus.currency.id != currencyStatus.currency.id)
+ toSwapCurrencyStatus.currency.id != currencyStatus.currency.id) ||
+ shouldShowSameCoinsWithDifferentAddress
}
}
@@ -1867,6 +1955,7 @@ internal class SwapModel @Inject constructor(
override suspend fun loadFeeExtended(
selectedToken: CryptoCurrencyStatus?,
): Either {
+ // TODO use getFeeGaselessUsecase in transfer. Will be implemented in [REDACTED_TASK_KEY]
val fromSwapCurrencyStatus =
dataState.fromSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!!
@@ -1920,7 +2009,7 @@ internal class SwapModel @Inject constructor(
uiState = uiState.copy(
swapButton = uiState.swapButton.copy(
isEnabled = false,
- isInProgress = false,
+ mode = SwapButton.Mode.SWAP_PROGRESSING,
),
)
modelScope.launch {
@@ -1939,7 +2028,7 @@ internal class SwapModel @Inject constructor(
override suspend fun loadFee(): Either {
TangemLogger.e("loadFee: Start loading fee")
-
+ // TODO use getFeeUsecase in transfer. Will be implemented in [REDACTED_TASK_KEY]
val fromSwapCurrencyStatus =
dataState.fromSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
val toSwapCurrencyStatus =
diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
index 3afeda4ddc..d96f1afc23 100644
--- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
+++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
@@ -72,10 +72,20 @@ sealed class SwapCardState {
data class SwapButton(
@DrawableRes val walletInteractionIcon: Int?,
val isEnabled: Boolean,
- val isInProgress: Boolean = false,
+ val mode: Mode = Mode.SWAP,
val isHoldToConfirm: Boolean = false,
val onClick: () -> Unit,
-)
+) {
+ enum class Mode {
+ SWAP_PROGRESSING,
+ SWAP,
+ TRANSFER,
+ TRANSFER_PROGRESSING,
+ }
+
+ val isInProgress
+ get() = mode == Mode.SWAP_PROGRESSING || mode == Mode.TRANSFER_PROGRESSING
+}
@Immutable
sealed interface TransactionCardType {
diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt
index ac922cd6f9..f9f2353886 100644
--- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt
+++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt
@@ -8,6 +8,7 @@ internal data class UiActions(
val onAmountChanged: (String) -> Unit,
val onAmountSelected: (Boolean) -> Unit,
val onSwapClick: () -> Unit,
+ val onTransferClick: () -> Unit,
val onChangeCardsClicked: () -> Unit,
val onBackClicked: () -> Unit,
val onMaxAmountSelected: () -> Unit,
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 1aeab71402..fe6ade5424 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
@@ -35,6 +35,7 @@ import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.model.SwapNotificationsFactory
import com.tangem.feature.swap.model.SwapProcessDataState
import com.tangem.feature.swap.models.*
+import com.tangem.feature.swap.models.SwapButton.Mode
import com.tangem.feature.swap.models.states.*
import com.tangem.feature.swap.presentation.R
import com.tangem.feature.swap.utils.formatToUIRepresentation
@@ -79,7 +80,7 @@ internal class StateBuilder(
swapButton = SwapButton(
walletInteractionIcon = null,
isEnabled = false,
- isInProgress = true,
+ mode = Mode.SWAP_PROGRESSING,
isHoldToConfirm = false,
onClick = {},
),
@@ -736,6 +737,7 @@ internal class StateBuilder(
swapButton = SwapButton(
walletInteractionIcon = fromSwapCurrencyStatus?.userWallet?.let(::walletInterationIcon),
isEnabled = false,
+ mode = if (emptyAmountState.isTransferMode) Mode.TRANSFER else Mode.SWAP,
isHoldToConfirm = fromSwapCurrencyStatus?.userWallet?.isHotWallet == true,
onClick = { },
),
@@ -749,7 +751,7 @@ internal class StateBuilder(
return uiState.copy(
swapButton = uiState.swapButton.copy(
isEnabled = false,
- isInProgress = true,
+ mode = Mode.SWAP_PROGRESSING,
),
)
}
@@ -880,7 +882,7 @@ internal class StateBuilder(
return uiState.copy(
swapButton = uiState.swapButton.copy(
isEnabled = false,
- isInProgress = false,
+ mode = Mode.SWAP,
),
notifications = notificationsFactory.getApprovalInProgressStateNotification(uiState.notifications),
)
@@ -1130,7 +1132,7 @@ internal class StateBuilder(
): ProviderState? {
val provider = this.key
return when (val state = this.value) {
- is SwapState.EmptyAmountState -> null
+ is SwapState.EmptyAmountState, is SwapState.Transfer -> null
is SwapState.QuotesLoadedState -> {
SwapProviderStateBuilder.buildContentSelectable(
provider = provider,
diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
index 4395a9786f..b8006f40bb 100644
--- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
+++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
@@ -14,6 +14,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -345,7 +346,7 @@ private fun MainButton(state: SwapStateHolder) {
state.swapButton.isHoldToConfirm -> {
HoldToConfirmButton(
modifier = Modifier.fillMaxWidth(),
- text = stringResourceSafe(R.string.swapping_swap_action),
+ text = getButtonTitle(state.swapButton.mode),
enabled = state.swapButton.isEnabled,
onConfirm = state.swapButton.onClick,
isLoading = state.swapButton.isInProgress,
@@ -355,11 +356,7 @@ private fun MainButton(state: SwapStateHolder) {
else -> {
PrimaryButtonIconEnd(
modifier = Modifier.fillMaxWidth(),
- text = if (state.swapButton.isInProgress) {
- stringResourceSafe(id = R.string.swapping_swap_action_in_progress)
- } else {
- stringResourceSafe(id = R.string.swapping_swap_action)
- },
+ text = getButtonTitle(state.swapButton.mode),
iconResId = state.swapButton.walletInteractionIcon,
enabled = state.swapButton.isEnabled,
onClick = state.swapButton.onClick,
@@ -368,6 +365,19 @@ private fun MainButton(state: SwapStateHolder) {
}
}
+@Composable
+@ReadOnlyComposable
+private fun getButtonTitle(mode: SwapButton.Mode): String {
+ return when (mode) {
+ SwapButton.Mode.SWAP_PROGRESSING -> stringResourceSafe(id = R.string.swapping_swap_action_in_progress)
+ SwapButton.Mode.SWAP -> stringResourceSafe(id = R.string.swapping_swap_action)
+ SwapButton.Mode.TRANSFER -> stringResourceSafe(id = R.string.swapping_transfer_action)
+ SwapButton.Mode.TRANSFER_PROGRESSING -> stringResourceSafe(
+ id = R.string.swapping_transfer_action_in_progress,
+ )
+ }
+}
+
// region preview
private val state = SwapStateHolder(
diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt
new file mode 100644
index 0000000000..fa52e874f8
--- /dev/null
+++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt
@@ -0,0 +1,172 @@
+package com.tangem.feature.swap.ui.transfer
+
+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
+import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
+import com.tangem.common.ui.userwallet.ext.walletInterationIcon
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.format.bigdecimal.crypto
+import com.tangem.core.ui.format.bigdecimal.fiat
+import com.tangem.core.ui.format.bigdecimal.format
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.models.account.Account
+import com.tangem.domain.models.currency.CryptoCurrencyStatus
+import com.tangem.domain.swap.models.SwapCurrencyStatus
+import com.tangem.feature.swap.domain.models.ui.SwapState
+import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
+import com.tangem.feature.swap.models.*
+import com.tangem.feature.swap.presentation.R
+import com.tangem.feature.swap.utils.formatToUIRepresentation
+import com.tangem.utils.StringsSigns.DASH_SIGN
+import java.math.BigDecimal
+import javax.inject.Inject
+
+internal class SwapTransferStateBuilder @Inject constructor() {
+
+ private val iconConverter by lazy(::CryptoCurrencyToIconStateConverter)
+
+ fun createTransferState(
+ actions: UiActions,
+ transferState: SwapState.Transfer,
+ uiStateHolder: SwapStateHolder,
+ ): SwapStateHolder {
+ val fromTokenSwapInfo = transferState.fromTokenInfo
+ val toTokenSwapInfo = transferState.toTokenInfo
+ return uiStateHolder.copy(
+ sendCardData = createSendSwapCardState(
+ actions = actions,
+ tokenSwapInfo = fromTokenSwapInfo,
+ appCurrency = transferState.appCurrency,
+ isAccountsMode = transferState.isAccountsMode,
+ isFromCard = true,
+ isBalanceHidden = transferState.isBalanceHidden,
+ ),
+ receiveCardData = createSendSwapCardState(
+ actions = actions,
+ tokenSwapInfo = toTokenSwapInfo,
+ appCurrency = transferState.appCurrency,
+ isAccountsMode = transferState.isAccountsMode,
+ isFromCard = false,
+ isBalanceHidden = transferState.isBalanceHidden,
+ ),
+ swapButton = SwapButton(
+ walletInteractionIcon = walletInterationIcon(transferState.userWallet),
+ isEnabled = false,
+ mode = SwapButton.Mode.TRANSFER,
+ onClick = actions.onTransferClick,
+ ),
+ )
+ }
+
+ @Suppress("LongParameterList")
+ private fun createSendSwapCardState(
+ actions: UiActions,
+ tokenSwapInfo: TokenSwapInfo,
+ appCurrency: AppCurrency,
+ isAccountsMode: Boolean,
+ isFromCard: Boolean,
+ isBalanceHidden: Boolean,
+ ): SwapCardState {
+ val swapCurrencyStatus = tokenSwapInfo.swapCurrencyStatus
+ val formattedSwapAmount = tokenSwapInfo.tokenAmount.formatToUIRepresentation()
+
+ return SwapCardState.SwapCardData(
+ type = createSendTransactionCardType(
+ actions = actions,
+ swapCurrencyStatus = tokenSwapInfo.swapCurrencyStatus,
+ isAccountsMode = isAccountsMode,
+ isFromCard = isFromCard,
+ ),
+ currencyIconState = iconConverter.convert(
+ value = swapCurrencyStatus.status,
+ ),
+ tokenSymbol = stringReference(swapCurrencyStatus.currency.symbol),
+ amountEquivalent = getFormattedFiatAmount(
+ appCurrency = appCurrency,
+ amount = tokenSwapInfo.amountFiat,
+ ),
+ amountTextFieldValue = TextFieldValue(
+ text = formattedSwapAmount,
+ selection = TextRange(index = formattedSwapAmount.length),
+ ),
+ balance = swapCurrencyStatus.status.getFormattedAmount(),
+ isBalanceHidden = isBalanceHidden,
+ )
+ }
+
+ private fun createSendTransactionCardType(
+ actions: UiActions,
+ swapCurrencyStatus: SwapCurrencyStatus,
+ isAccountsMode: Boolean,
+ isFromCard: Boolean,
+ ): TransactionCardType {
+ val type = if (isFromCard) {
+ TransactionCardType.Inputtable(
+ onAmountChanged = actions.onAmountChanged,
+ onFocusChanged = actions.onAmountSelected,
+ inputError = TransactionCardType.InputError.Empty,
+ accountTitleUM = getCardAccountTitle(
+ account = swapCurrencyStatus.account,
+ isAccountsMode = isAccountsMode,
+ isFromCard = true,
+ ),
+ isEnabled = true,
+ )
+ } else {
+ TransactionCardType.ReadOnly(
+ accountTitleUM = getCardAccountTitle(
+ account = swapCurrencyStatus.account,
+ isAccountsMode = isAccountsMode,
+ isFromCard = false,
+ ),
+ )
+ }
+ return type
+ }
+
+ private fun getCardAccountTitle(account: Account?, isAccountsMode: Boolean, isFromCard: Boolean): AccountTitleUM {
+ val (prefix, placeholder) = if (isFromCard) {
+ R.string.swapping_from_account_title to R.string.swapping_from_title_v2
+ } else {
+ R.string.swapping_to_account_title to R.string.swapping_to_title
+ }
+ return if (account != null && isAccountsMode) {
+ AccountTitleUM.Account(
+ prefixText = resourceReference(prefix),
+ name = account.accountName.toUM().value,
+ icon = account.toIconUM(),
+ )
+ } else {
+ AccountTitleUM.Text(resourceReference(placeholder))
+ }
+ }
+
+ private fun getFormattedFiatAmount(appCurrency: AppCurrency, amount: BigDecimal?): TextReference {
+ return stringReference(
+ amount.format {
+ fiat(
+ fiatCurrencyCode = appCurrency.code,
+ fiatCurrencySymbol = appCurrency.symbol,
+ )
+ },
+ )
+ }
+
+ private fun CryptoCurrencyStatus.getFormattedAmount(): String {
+ val amount = this.value.amount ?: return DASH_SIGN
+ return amount.format { crypto(symbol = "", decimals = currency.decimals) }
+ }
+
+ private fun Account.toIconUM(): AccountIconUM {
+ return when (this) {
+ is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(icon)
+ is Account.Payment -> AccountIconUM.Payment
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt
index dde42d0e89..2d15fe9295 100644
--- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt
+++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt
@@ -20,6 +20,8 @@ import kotlinx.collections.immutable.toImmutableList
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
import java.math.BigDecimal
internal class StateBuilderSwapDataTest {
@@ -330,10 +332,17 @@ internal class StateBuilderSwapDataTest {
assertThat(result.swapButton.isEnabled).isFalse()
}
- @Test
- fun `WHEN called THEN swapButton isInProgress is false`() {
+ @ParameterizedTest
+ @EnumSource(
+ value = SwapButton.Mode::class,
+ mode = EnumSource.Mode.INCLUDE,
+ names = ["SWAP_PROGRESSING", "TRANSFER_PROGRESSING"],
+ )
+ fun `WHEN called THEN swapButton isInProgress is false`(mode: SwapButton.Mode) {
val baseState = buildReadyState(coldWallet).copy(
- swapButton = buildReadyState(coldWallet).swapButton.copy(isInProgress = true),
+ swapButton = buildReadyState(coldWallet).swapButton.copy(
+ mode = mode,
+ ),
)
val result = sut.loadingPermissionState(baseState)
diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt
new file mode 100644
index 0000000000..5e3196c4a2
--- /dev/null
+++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt
@@ -0,0 +1,191 @@
+package com.tangem.feature.swap.ui.transfer
+
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
+import com.google.common.truth.Truth.assertThat
+import com.tangem.common.ui.account.AccountTitleUM
+import com.tangem.common.ui.account.CryptoPortfolioIconConverter
+import com.tangem.common.ui.account.toUM
+import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
+import com.tangem.common.ui.userwallet.ext.walletInterationIcon
+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.wallet.UserWallet
+import com.tangem.domain.models.wallet.UserWalletId
+import com.tangem.domain.swap.models.SwapCurrencyStatus
+import com.tangem.feature.swap.buildSwapCurrencyStatus
+import com.tangem.feature.swap.domain.models.SwapAmount
+import com.tangem.feature.swap.domain.models.ui.PriceImpact
+import com.tangem.feature.swap.domain.models.ui.SwapState
+import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
+import com.tangem.feature.swap.domain.models.ui.TxFeeState
+import com.tangem.feature.swap.models.*
+import com.tangem.feature.swap.models.states.ProviderState
+import com.tangem.feature.swap.presentation.R
+import com.tangem.feature.swap.utils.formatToUIRepresentation
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import java.math.BigDecimal
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+internal class SwapTransferStateBuilderTest {
+
+ private val actions: UiActions = mockk(relaxed = true)
+ private val sut = SwapTransferStateBuilder()
+
+ private val userWalletId = UserWalletId(stringValue = "deadbeef")
+ private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
+ every { walletId } returns userWalletId
+ }
+ private val fromCurrencyStatus: SwapCurrencyStatus = buildSwapCurrencyStatus(coldWallet)
+ private val toCurrencyStatus: SwapCurrencyStatus = buildSwapCurrencyStatus(coldWallet)
+ private val iconConverter = CryptoCurrencyToIconStateConverter()
+ private val fromIcon = iconConverter.convert(fromCurrencyStatus.status)
+ private val toIcon = iconConverter.convert(toCurrencyStatus.status)
+
+ @Test
+ fun `GIVEN accounts mode enabled WHEN createTransferState THEN cards expose Account titles for from and to`() {
+ val transferState = buildTransferState(
+ fromAmount = BigDecimal("1.5"),
+ toAmount = BigDecimal("1.5"),
+ isAccountsMode = true,
+ )
+
+ val result = sut.createTransferState(actions, transferState, baseStateHolder())
+
+ val portfolioAccount = fromCurrencyStatus.account as Account.CryptoPortfolio
+ val expectedAccountIcon = CryptoPortfolioIconConverter.convert(portfolioAccount.icon)
+ val expectedAccountName = portfolioAccount.accountName.toUM().value
+ val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
+ val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
+ assertThat(sendType.accountTitleUM).isEqualTo(
+ AccountTitleUM.Account(
+ prefixText = resourceReference(R.string.swapping_from_account_title),
+ name = expectedAccountName,
+ icon = expectedAccountIcon,
+ ),
+ )
+ assertThat(receiveType.accountTitleUM).isEqualTo(
+ AccountTitleUM.Account(
+ prefixText = resourceReference(R.string.swapping_to_account_title),
+ name = expectedAccountName,
+ icon = expectedAccountIcon,
+ ),
+ )
+ assertSharedCardShape(
+ result = result,
+ transferState = transferState,
+ )
+ }
+
+ @Test
+ fun `GIVEN accounts mode disabled WHEN createTransferState THEN cards fall back to Text titles for from and to`() {
+ val transferState = buildTransferState(
+ fromAmount = BigDecimal("2"),
+ toAmount = BigDecimal("2"),
+ isAccountsMode = false,
+ )
+
+ val result = sut.createTransferState(actions, transferState, baseStateHolder())
+
+ val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
+ val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
+ assertThat(sendType.accountTitleUM).isEqualTo(
+ AccountTitleUM.Text(resourceReference(R.string.swapping_from_title_v2)),
+ )
+ assertThat(receiveType.accountTitleUM).isEqualTo(
+ AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)),
+ )
+ assertSharedCardShape(
+ result = result,
+ transferState = transferState,
+ )
+ }
+
+ private fun assertSharedCardShape(
+ result: SwapStateHolder,
+ transferState: SwapState.Transfer,
+ ) {
+ val sendCard = result.sendCardData as SwapCardState.SwapCardData
+ val receiveCard = result.receiveCardData as SwapCardState.SwapCardData
+ val expectedFromText = transferState.fromTokenInfo.tokenAmount.formatToUIRepresentation()
+ val expectedToText = transferState.toTokenInfo.tokenAmount.formatToUIRepresentation()
+ assertThat(sendCard.amountTextFieldValue).isEqualTo(
+ TextFieldValue(text = expectedFromText, selection = TextRange(index = expectedFromText.length)),
+ )
+ assertThat(receiveCard.amountTextFieldValue).isEqualTo(
+ TextFieldValue(text = expectedToText, selection = TextRange(index = expectedToText.length)),
+ )
+ assertThat(sendCard.currencyIconState).isEqualTo(fromIcon)
+ assertThat(receiveCard.currencyIconState).isEqualTo(toIcon)
+ assertThat(sendCard.isBalanceHidden).isEqualTo(transferState.isBalanceHidden)
+ assertThat(receiveCard.isBalanceHidden).isEqualTo(transferState.isBalanceHidden)
+ assertThat((sendCard.type is TransactionCardType.Inputtable)).isTrue()
+ assertThat(receiveCard.type).isInstanceOf(TransactionCardType.ReadOnly::class.java)
+ assertThat(result.swapButton).isEqualTo(
+ SwapButton(
+ walletInteractionIcon = walletInterationIcon(transferState.userWallet),
+ isEnabled = false,
+ mode = SwapButton.Mode.TRANSFER,
+ onClick = actions.onTransferClick,
+ ),
+ )
+ }
+
+ private fun buildTransferState(
+ fromAmount: BigDecimal,
+ toAmount: BigDecimal,
+ isAccountsMode: Boolean,
+ ): SwapState.Transfer {
+ val fromInfo = TokenSwapInfo(
+ tokenAmount = SwapAmount(value = fromAmount, decimals = fromCurrencyStatus.currency.decimals),
+ amountFiat = fromAmount * QUOTE,
+ swapCurrencyStatus = fromCurrencyStatus,
+ )
+ val toInfo = TokenSwapInfo(
+ tokenAmount = SwapAmount(value = toAmount, decimals = toCurrencyStatus.currency.decimals),
+ amountFiat = toAmount * QUOTE,
+ swapCurrencyStatus = toCurrencyStatus,
+ )
+ return SwapState.Transfer(
+ userWallet = coldWallet,
+ fromTokenInfo = fromInfo,
+ toTokenInfo = toInfo,
+ txFee = TxFeeState.Empty,
+ appCurrency = AppCurrency.Default,
+ isBalanceHidden = false,
+ isAccountsMode = isAccountsMode,
+ )
+ }
+
+ private fun baseStateHolder(): SwapStateHolder = SwapStateHolder(
+ sendCardData = SwapCardState.Loading(
+ type = TransactionCardType.ReadOnly(
+ accountTitleUM = AccountTitleUM.Text(resourceReference(R.string.swapping_from_title_v2)),
+ ),
+ ),
+ receiveCardData = SwapCardState.Loading(
+ type = TransactionCardType.ReadOnly(
+ accountTitleUM = AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)),
+ ),
+ ),
+ isInsufficientFunds = false,
+ changeCardsButtonState = ChangeCardsButtonState.ENABLED,
+ providerState = ProviderState.Empty(),
+ priceImpact = PriceImpact.Empty,
+ swapButton = SwapButton(walletInteractionIcon = null, isEnabled = false, onClick = {}),
+ shouldShowMaxAmount = false,
+ onRefresh = {},
+ onBackClicked = {},
+ onChangeCardsClicked = {},
+ onSelectTokenClick = {},
+ onSuccess = {},
+ )
+
+ private companion object {
+ val QUOTE: BigDecimal = BigDecimal("2000")
+ }
+}
\ No newline at end of file