Updated on 2026-08-14
This commit is contained in:
parent
23eeb6de60
commit
b13925b77a
9 changed files with 252 additions and 5 deletions
|
|
@ -60,7 +60,7 @@
|
|||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "SWAP_SWITCH_TO_TRANSFER_ENABLED",
|
||||
"name": "AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ internal class FeatureTogglesNamingConventionTest {
|
|||
"STAKING_ETH_ENABLED",
|
||||
"SWAP_AB_ENABLED",
|
||||
"SWAP_INTEGRATED_APPROVE",
|
||||
"SWAP_SWITCH_TO_TRANSFER_ENABLED",
|
||||
"USEDESK_ENABLED",
|
||||
"VIRTUAL_ACCOUNTS_ENABLED",
|
||||
"VISA_ONBOARDING_ENABLED",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ internal class DefaultSwapFeatureToggles @Inject constructor(
|
|||
) : SwapFeatureToggles {
|
||||
|
||||
override val isSwapSwitchToTransferEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.SWAP_SWITCH_TO_TRANSFER_ENABLED,
|
||||
toggle = FeatureToggles.AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED,
|
||||
)
|
||||
|
||||
override val isSwapIntegratedApproveEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
|
|
|
|||
|
|
@ -763,6 +763,7 @@ internal class SwapModel @Inject constructor(
|
|||
) as? SwapState.Transfer ?: currentTransferState
|
||||
dataState = dataState.copy(currentTransferState = refreshed)
|
||||
uiState = swapTransferStateBuilder.updateTransferButtonEnableState(
|
||||
dataState = dataState,
|
||||
transferState = refreshed,
|
||||
actions = actions,
|
||||
uiStateHolder = uiState,
|
||||
|
|
@ -1319,6 +1320,11 @@ internal class SwapModel @Inject constructor(
|
|||
txUrl = txUrl,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
fee = null,
|
||||
onExplorerClick = {
|
||||
if (txUrl.isNotEmpty()) {
|
||||
urlOpener.openUrl(txUrl)
|
||||
}
|
||||
},
|
||||
)
|
||||
router.replaceAll(SwapRoute.Success)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ internal data class SwapStateHolder(
|
|||
val swapUIMode: SwapUIMode = SwapUIMode.Detailed,
|
||||
val shouldShowAbMenu: Boolean = false,
|
||||
|
||||
val transferFooter: TextReference? = null,
|
||||
|
||||
val onRefresh: () -> Unit,
|
||||
val onBackClicked: () -> Unit,
|
||||
val onChangeCardsClicked: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ internal class StateBuilder(
|
|||
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
|
||||
priceImpact = PriceImpact.Empty,
|
||||
shouldShowMaxAmount = shouldShowMaxAmount(fromCurrency, toCurrency),
|
||||
transferFooter = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -778,6 +779,7 @@ internal class StateBuilder(
|
|||
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
providerState = ProviderState.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
transferFooter = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,9 +30,11 @@ import androidx.compose.ui.text.withStyle
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.tangem.common.ui.footers.SendingText
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
|
|
@ -97,6 +99,13 @@ internal fun SwapScreenContent(
|
|||
.padding(top = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
if (state.transferFooter != null) {
|
||||
TransferFooter(
|
||||
textReference = state.transferFooter,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
|
||||
MainButton(state = state)
|
||||
}
|
||||
|
|
@ -187,6 +196,14 @@ private fun ProviderTos(tosState: TosState, modifier: Modifier = Modifier) {
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransferFooter(textReference: TextReference, modifier: Modifier = Modifier) {
|
||||
SendingText(
|
||||
modifier = modifier,
|
||||
footerText = textReference,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAnnotatedStringForLegalsWithClick(
|
||||
tos: LegalState?,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
|
|
@ -20,6 +21,7 @@ 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.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
|
||||
import com.tangem.feature.swap.model.SwapProcessDataState
|
||||
|
|
@ -27,13 +29,17 @@ import com.tangem.feature.swap.models.*
|
|||
import com.tangem.feature.swap.models.SwapButton.Mode
|
||||
import com.tangem.feature.swap.models.states.SwapNotificationUM
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.features.send.v2.api.utils.formatFooterFiatFee
|
||||
import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass")
|
||||
internal class SwapTransferStateBuilder @Inject constructor(
|
||||
private val notificationsFactory: SwapTransferNotificationsFactory,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
) {
|
||||
|
||||
private val iconConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
|
@ -204,7 +210,9 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun updateTransferButtonEnableState(
|
||||
dataState: SwapProcessDataState,
|
||||
transferState: SwapState.Transfer,
|
||||
actions: UiActions,
|
||||
uiStateHolder: SwapStateHolder,
|
||||
|
|
@ -223,6 +231,12 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
swapButton = uiStateHolder.swapButton.copy(
|
||||
isEnabled = getTransferButtonEnabled(notifications, fee),
|
||||
),
|
||||
transferFooter = getSendingFooterText(
|
||||
dataState = dataState,
|
||||
fee = fee,
|
||||
tokenSwapInfo = transferState.fromTokenInfo,
|
||||
appCurrency = transferState.appCurrency,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -238,6 +252,58 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getSendingFooterText(
|
||||
dataState: SwapProcessDataState,
|
||||
fee: Fee?,
|
||||
tokenSwapInfo: TokenSwapInfo,
|
||||
appCurrency: AppCurrency,
|
||||
): TextReference? {
|
||||
if (fee == null) return null
|
||||
|
||||
val fiatAmountValue = tokenSwapInfo.amountFiat
|
||||
val status = dataState.fromSwapCurrencyStatus?.status ?: return null
|
||||
val fiatFeeValue = fee.amount.value
|
||||
val isFeeConvertibleToFiat = status.currency.network.hasFiatFeeRate
|
||||
|
||||
val fiatSendingValue = if (isFeeConvertibleToFiat) {
|
||||
fiatFeeValue?.let { fiatAmountValue.plus(it) }
|
||||
} else {
|
||||
fiatAmountValue
|
||||
}
|
||||
|
||||
val fiatSending = fiatSendingValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
val networkId = status.currency.network.id
|
||||
val fiatFee = formatFooterFiatFee(
|
||||
amount = fee.amount.copy(value = fiatFeeValue),
|
||||
isFeeConvertibleToFiat = isFeeConvertibleToFiat,
|
||||
isFeeApproximate = isFeeApproximateUseCase(networkId = networkId, amountType = fee.amount.type),
|
||||
appCurrency = appCurrency,
|
||||
)
|
||||
|
||||
return if (fee is Fee.Tron) {
|
||||
getTronTokenFeeSendingText(
|
||||
fee = fee,
|
||||
fiatFee = fiatFee,
|
||||
fiatSending = stringReference(fiatSending),
|
||||
)
|
||||
} else {
|
||||
resourceReference(
|
||||
id = if (isFeeConvertibleToFiat) {
|
||||
com.tangem.features.send.v2.impl.R.string.send_summary_transaction_description
|
||||
} else {
|
||||
com.tangem.features.send.v2.impl.R.string.send_summary_transaction_description_no_fiat_fee
|
||||
},
|
||||
formatArgs = wrappedList(fiatSending, fiatFee),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createTransferInProgressState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
swapButton = uiState.swapButton.copy(
|
||||
|
|
@ -256,6 +322,7 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
txUrl: String,
|
||||
timestamp: Long,
|
||||
fee: TextReference?,
|
||||
onExplorerClick: () -> Unit,
|
||||
): SwapStateHolder {
|
||||
val fromSwapCurrencyStatus = requireNotNull(dataState.fromSwapCurrencyStatus)
|
||||
val toSwapCurrencyStatus = requireNotNull(dataState.toSwapCurrencyStatus)
|
||||
|
|
@ -301,7 +368,7 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
toTokenFiatAmount = toFiatAmount,
|
||||
fromTokenIconState = iconConverter.convert(fromSwapCurrencyStatus.status),
|
||||
toTokenIconState = iconConverter.convert(toSwapCurrencyStatus.status),
|
||||
onExploreButtonClick = {},
|
||||
onExploreButtonClick = onExplorerClick,
|
||||
onStatusButtonClick = {},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
|
||||
|
|
@ -12,11 +13,18 @@ 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.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
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.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.feature.swap.buildSwapCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
|
|
@ -51,7 +59,13 @@ internal class SwapTransferStateBuilderTest {
|
|||
)
|
||||
} returns persistentListOf()
|
||||
}
|
||||
private val sut = SwapTransferStateBuilder(notificationsFactory = notificationsFactory)
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase = mockk(relaxed = true) {
|
||||
every { invoke(networkId = any(), amountType = any()) } returns false
|
||||
}
|
||||
private val sut = SwapTransferStateBuilder(
|
||||
notificationsFactory = notificationsFactory,
|
||||
isFeeApproximateUseCase = isFeeApproximateUseCase,
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId(stringValue = "deadbeef")
|
||||
private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
|
||||
|
|
@ -277,6 +291,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
isAccountsMode = false,
|
||||
)
|
||||
val fee: Fee = mockk(relaxed = true)
|
||||
val dataState = SwapProcessDataState()
|
||||
val uiState = baseStateHolder().copy(
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = null,
|
||||
|
|
@ -296,6 +311,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
} returns persistentListOf()
|
||||
|
||||
val result = sut.updateTransferButtonEnableState(
|
||||
dataState = dataState,
|
||||
transferState = transferState,
|
||||
actions = actions,
|
||||
uiStateHolder = uiState,
|
||||
|
|
@ -317,6 +333,135 @@ internal class SwapTransferStateBuilderTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Tron fee WHEN updateTransferButtonEnableState THEN transferFooter uses Tron token fee sending text`() =
|
||||
runTest {
|
||||
val fromAmount = BigDecimal("1")
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = fromAmount,
|
||||
toAmount = fromAmount,
|
||||
isAccountsMode = false,
|
||||
)
|
||||
val statusWithNetwork = buildStatusWithNetwork(hasFiatFeeRate = false)
|
||||
val dataState = SwapProcessDataState(fromSwapCurrencyStatus = statusWithNetwork)
|
||||
val fee = Fee.Tron(
|
||||
amount = Amount(currencySymbol = "TRX", value = BigDecimal("0.5"), decimals = 6),
|
||||
remainingEnergy = 1000L,
|
||||
feeEnergy = 100L,
|
||||
)
|
||||
val uiState = baseStateHolder()
|
||||
|
||||
val result = sut.updateTransferButtonEnableState(
|
||||
dataState = dataState,
|
||||
transferState = transferState,
|
||||
actions = actions,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
)
|
||||
|
||||
assertThat(result.transferFooter).isInstanceOf(TextReference.Combined::class.java)
|
||||
val refs = (result.transferFooter as TextReference.Combined).refs.data
|
||||
assertThat(refs).hasSize(3)
|
||||
assertThat(refs[0]).isInstanceOf(TextReference.Res::class.java)
|
||||
assertThat((refs[0] as TextReference.Res).id)
|
||||
.isEqualTo(com.tangem.features.send.v2.api.R.string.send_summary_transaction_description_prefix)
|
||||
assertThat(refs[2]).isInstanceOf(TextReference.Res::class.java)
|
||||
assertThat((refs[2] as TextReference.Res).id)
|
||||
.isEqualTo(com.tangem.features.send.v2.api.R.string.send_summary_transaction_description_suffix_fee_covered)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-Tron fee and fiat-convertible network WHEN updateTransferButtonEnableState THEN transferFooter uses fiat fee description`() =
|
||||
runTest {
|
||||
val fromAmount = BigDecimal("1")
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = fromAmount,
|
||||
toAmount = fromAmount,
|
||||
isAccountsMode = false,
|
||||
)
|
||||
val statusWithNetwork = buildStatusWithNetwork(hasFiatFeeRate = true)
|
||||
val dataState = SwapProcessDataState(fromSwapCurrencyStatus = statusWithNetwork)
|
||||
val feeValue = BigDecimal("0.001")
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(currencySymbol = "ETH", value = feeValue, decimals = 18),
|
||||
)
|
||||
val uiState = baseStateHolder()
|
||||
val appCurrency = transferState.appCurrency
|
||||
val expectedFiatSending = (fromAmount * QUOTE).plus(feeValue).format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
val expectedFiatFee = feeValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
val result = sut.updateTransferButtonEnableState(
|
||||
dataState = dataState,
|
||||
transferState = transferState,
|
||||
actions = actions,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
)
|
||||
|
||||
assertThat(result.transferFooter).isEqualTo(
|
||||
resourceReference(
|
||||
id = com.tangem.features.send.v2.impl.R.string.send_summary_transaction_description,
|
||||
formatArgs = wrappedList(expectedFiatSending, expectedFiatFee),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-Tron fee and non-fiat-convertible network WHEN updateTransferButtonEnableState THEN transferFooter uses no-fiat-fee description`() =
|
||||
runTest {
|
||||
val fromAmount = BigDecimal("1")
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = fromAmount,
|
||||
toAmount = fromAmount,
|
||||
isAccountsMode = false,
|
||||
)
|
||||
val statusWithNetwork = buildStatusWithNetwork(hasFiatFeeRate = false)
|
||||
val dataState = SwapProcessDataState(fromSwapCurrencyStatus = statusWithNetwork)
|
||||
val feeValue = BigDecimal("0.001")
|
||||
val feeAmount = Amount(currencySymbol = "ETH", value = feeValue, decimals = 18)
|
||||
val fee = Fee.Common(amount = feeAmount)
|
||||
val uiState = baseStateHolder()
|
||||
val appCurrency = transferState.appCurrency
|
||||
val expectedFiatSending = (fromAmount * QUOTE).format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
val expectedFiatFee = feeValue.format {
|
||||
crypto(decimals = feeAmount.decimals, symbol = feeAmount.currencySymbol)
|
||||
.fee(canBeLower = false)
|
||||
}
|
||||
|
||||
val result = sut.updateTransferButtonEnableState(
|
||||
dataState = dataState,
|
||||
transferState = transferState,
|
||||
actions = actions,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
)
|
||||
|
||||
assertThat(result.transferFooter).isEqualTo(
|
||||
resourceReference(
|
||||
id = com.tangem.features.send.v2.impl.R.string.send_summary_transaction_description_no_fiat_fee,
|
||||
formatArgs = wrappedList(expectedFiatSending, expectedFiatFee),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN dataState with from-to currencies WHEN createSuccessState THEN success holder is built in transfer mode with given fee and txUrl`() {
|
||||
val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$")
|
||||
|
|
@ -338,6 +483,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
txUrl = txUrl,
|
||||
timestamp = timestamp,
|
||||
fee = fee,
|
||||
onExplorerClick = {},
|
||||
)
|
||||
|
||||
val success = requireNotNull(result.successState)
|
||||
|
|
@ -396,6 +542,14 @@ internal class SwapTransferStateBuilderTest {
|
|||
)
|
||||
}
|
||||
|
||||
private fun buildStatusWithNetwork(hasFiatFeeRate: Boolean): SwapCurrencyStatus {
|
||||
val networkId: Network.ID = mockk(relaxed = true)
|
||||
val status = buildSwapCurrencyStatus(coldWallet)
|
||||
every { status.status.currency.network.id } returns networkId
|
||||
every { status.status.currency.network.hasFiatFeeRate } returns hasFiatFeeRate
|
||||
return status
|
||||
}
|
||||
|
||||
private fun buildTransferState(
|
||||
fromAmount: BigDecimal,
|
||||
toAmount: BigDecimal,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue