Updated on 2026-08-14
This commit is contained in:
parent
f5696659f2
commit
d5554a7f7d
16 changed files with 1248 additions and 57 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1212,6 +1212,8 @@
|
|||
<string name="qr_scanner_warning_unknown_parameters_title">Unknown Parameters</string>
|
||||
<string name="quick_action_buy_description">Credit card or bank account</string>
|
||||
<string name="quick_action_receive_description">Share your address or QR-code</string>
|
||||
<string name="quick_action_sell_description">Sell crypto securely</string>
|
||||
<string name="quick_action_send_description">Send to another wallet</string>
|
||||
<string name="quick_action_swap_description">Between your portfolios</string>
|
||||
<string name="quick_top_up_chip_other">Other</string>
|
||||
<string name="quick_top_up_title">Quick top up</string>
|
||||
|
|
@ -1352,8 +1354,6 @@
|
|||
<string name="send_notification_invalid_reserve_amount_text">Target account is not created. Please change the amount to send.</string>
|
||||
<string name="send_notification_invalid_reserve_amount_title">The amount to send must be at least %s</string>
|
||||
<string name="send_notification_leave_button">Leave %s</string>
|
||||
<string name="send_notification_no_trustline_text">A trustline for %s is required first.</string>
|
||||
<string name="send_notification_no_trustline_title">Can\'t receive token</string>
|
||||
<string name="send_notification_reduce_by">Reduce by %s</string>
|
||||
<string name="send_notification_reduce_to">Reduce to %s</string>
|
||||
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
|
||||
|
|
@ -1626,6 +1626,8 @@
|
|||
<string name="swapping_token_not_available">not available</string>
|
||||
<string name="swapping_trade_too_large_text">Not enough liquidity for this trade.\nReduce the amount or choose another provider.</string>
|
||||
<string name="swapping_trade_too_large_title">Trade too large</string>
|
||||
<string name="swapping_transfer_action">Transfer</string>
|
||||
<string name="swapping_transfer_action_in_progress">Transfer...</string>
|
||||
<string name="tangem_pay_beta_notification_subtitle">We would be happy to receive your feedback</string>
|
||||
<string name="tangem_pay_beta_notification_title">Tangem Pay is now in beta</string>
|
||||
<string name="tangem_pay_card_details_unable_to_rename_card_title">Unable to rename card</string>
|
||||
|
|
@ -1766,6 +1768,8 @@
|
|||
<string name="tangempay_onboarding_purchases_title">Pay exactly what you see</string>
|
||||
<string name="tangempay_onboarding_security_description">A separate payment account will be created without disclosing your addresses and assets</string>
|
||||
<string name="tangempay_onboarding_security_title">Unrivaled privacy</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_description">And link a payment card to it</string>
|
||||
<string name="tangempay_onboarding_setup_wallet_title">We\'ll set up a wallet</string>
|
||||
<string name="tangempay_onboarding_title">Get your free Tangem Pay Card in minutes</string>
|
||||
<string name="tangempay_pay_support">Pay Support</string>
|
||||
<string name="tangempay_payment_account">Payment account</string>
|
||||
|
|
@ -1815,7 +1819,7 @@
|
|||
<string name="token_button_unavailability_reason_yield_supply_approval">Approval has been revoked. Your funds remain in Yield mode. To perform actions, please go to Yield mode and grant permission again.</string>
|
||||
<string name="token_details_balance_available">Available balance</string>
|
||||
<string name="token_details_balance_total">Total balance</string>
|
||||
<string name="token_details_earn_staking_subtitle">Earn up to %s a year</string>
|
||||
<string name="token_details_earn_staking_subtitle">Up to %s APR</string>
|
||||
<string name="token_details_generate_xpub">Generate XPUB</string>
|
||||
<string name="token_details_hide_alert_hide">Hide</string>
|
||||
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
|
||||
|
|
@ -2133,7 +2137,7 @@
|
|||
<string name="warning_matic_migration_title">MATIC to POL Migration</string>
|
||||
<plurals name="warning_missing_derivation_message">
|
||||
<item quantity="one">Use your card or ring to get an address for %d network</item>
|
||||
<item quantity="other">Use your card or ring to get an addresses for %d networks</item>
|
||||
<item quantity="other">Use your card or ring to get addresses for %d networks</item>
|
||||
</plurals>
|
||||
<string name="warning_missing_derivation_title">Some addresses are missing</string>
|
||||
<string name="warning_network_unreachable_message">The network is currently unreachable. Please try again later.</string>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SwapPairLeast>,
|
||||
providerList: List<SwapProvider>,
|
||||
) {
|
||||
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<GetFeeError, TransactionFeeExtended> {
|
||||
// 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<GetFeeError, TransactionFee> {
|
||||
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 =
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue