Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-12 10:50:31 +01:00
parent f5696659f2
commit d5554a7f7d
16 changed files with 1248 additions and 57 deletions

View file

@ -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 =

View file

@ -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 {

View file

@ -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,

View file

@ -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,

View file

@ -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(

View file

@ -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
}
}
}

View file

@ -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)

View file

@ -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")
}
}