Updated on 2026-08-14
This commit is contained in:
parent
2415a8a37b
commit
83446489ba
4 changed files with 179 additions and 48 deletions
|
|
@ -6,9 +6,9 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.buttons.predefined.PredefinedPercentButtonUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.swap.models.PredefinedPercentAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
|
|
@ -30,10 +30,10 @@ internal data class SwapStateHolder(
|
|||
val bottomSheetConfig: TangemBottomSheetConfig? = null,
|
||||
val swapButton: SwapButton,
|
||||
val shouldShowMaxAmount: Boolean,
|
||||
val predefinedButtons: ImmutableList<PredefinedPercentButtonUM> = persistentListOf(),
|
||||
val tosState: TosState? = null,
|
||||
val swapUIMode: SwapUIMode = SwapUIMode.Detailed,
|
||||
val shouldShowAbMenu: Boolean = false,
|
||||
val isPredefinedButtonsEnabled: Boolean = false,
|
||||
|
||||
val transferFooter: TextReference? = null,
|
||||
|
||||
|
|
@ -43,7 +43,6 @@ internal data class SwapStateHolder(
|
|||
val onSelectTokenClick: ((TokenSelectionDirection) -> Unit),
|
||||
val onSuccess: (() -> Unit),
|
||||
val onMaxAmountSelected: (() -> Unit)? = null,
|
||||
val onPredefinedPercentSelected: ((PredefinedPercentAmount) -> Unit)? = null,
|
||||
val onShowPermissionBottomSheet: () -> Unit = {},
|
||||
val onSwapUIModeChange: (SwapUIMode) -> Unit = {},
|
||||
val onSwapTypeMenuOpened: () -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToI
|
|||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.buttons.predefined.PredefinedPercentButtonUM
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
|
|
@ -26,6 +27,7 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.isHotWallet
|
||||
import com.tangem.domain.swap.models.PredefinedPercentAmount
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
|
|
@ -97,7 +99,6 @@ internal class StateBuilder(
|
|||
onBackClicked = actions.onBackClicked,
|
||||
onChangeCardsClicked = actions.onChangeCardsClicked,
|
||||
onMaxAmountSelected = actions.onMaxAmountSelected,
|
||||
onPredefinedPercentSelected = actions.onPredefinedPercentSelected,
|
||||
changeCardsButtonState = ChangeCardsButtonState.DISABLED,
|
||||
onShowPermissionBottomSheet = actions.openPermissionBottomSheet,
|
||||
onSelectTokenClick = actions.onSelectTokenClick,
|
||||
|
|
@ -110,7 +111,6 @@ internal class StateBuilder(
|
|||
onSwapUIModeChange = actions.onSwapUIModeChange,
|
||||
onSwapTypeMenuOpened = actions.onSwapTypeMenuOpened,
|
||||
shouldShowAbMenu = swapFeatureToggles.isSwapAbEnabled,
|
||||
isPredefinedButtonsEnabled = swapFeatureToggles.isSwapPredefinedButtonsEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +142,10 @@ internal class StateBuilder(
|
|||
onClick = { },
|
||||
),
|
||||
shouldShowMaxAmount = shouldShowMaxAmount(fromSwapCurrencyStatus?.currency, toSwapCurrencyStatus?.currency),
|
||||
predefinedButtons = createPredefinedButtons(
|
||||
fromSwapCurrencyStatus?.currency,
|
||||
toSwapCurrencyStatus?.currency,
|
||||
),
|
||||
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
providerState = ProviderState.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
|
|
@ -215,6 +219,7 @@ internal class StateBuilder(
|
|||
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
|
||||
priceImpact = PriceImpact.Empty,
|
||||
shouldShowMaxAmount = shouldShowMaxAmount(fromCurrency, toCurrency),
|
||||
predefinedButtons = createPredefinedButtons(fromCurrency, toCurrency),
|
||||
transferFooter = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -250,6 +255,10 @@ internal class StateBuilder(
|
|||
onClick = { },
|
||||
),
|
||||
shouldShowMaxAmount = shouldShowMaxAmount(fromSwapCurrencyStatus?.currency, toSwapCurrencyStatus?.currency),
|
||||
predefinedButtons = createPredefinedButtons(
|
||||
fromSwapCurrencyStatus?.currency,
|
||||
toSwapCurrencyStatus?.currency,
|
||||
),
|
||||
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
providerState = ProviderState.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
|
|
@ -446,6 +455,7 @@ internal class StateBuilder(
|
|||
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
|
||||
priceImpact = PriceImpact.Empty,
|
||||
shouldShowMaxAmount = shouldShowMaxAmount(fromCurrency, toCurrency),
|
||||
predefinedButtons = createPredefinedButtons(fromCurrency, toCurrency),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -576,6 +586,7 @@ internal class StateBuilder(
|
|||
priceImpact = priceImpact,
|
||||
tosState = createTosState(swapProvider),
|
||||
shouldShowMaxAmount = shouldShowMaxAmount(fromSwapCurrencyStatus.currency, toSwapCurrencyStatus.currency),
|
||||
predefinedButtons = createPredefinedButtons(fromSwapCurrencyStatus.currency, toSwapCurrencyStatus.currency),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -611,6 +622,37 @@ internal class StateBuilder(
|
|||
return !(fromToken is CryptoCurrency.Coin && fromToken.network.id == toCurrency?.network?.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the predefined percent buttons once per state update (off the composition path).
|
||||
* The row is gated by the feature toggle; the MAX button is included only when
|
||||
* [shouldShowMaxAmount] is `true` (e.g. it is dropped for a native coin swapped within the same
|
||||
* network, where spending the full balance would leave nothing for the network fee).
|
||||
*/
|
||||
private fun createPredefinedButtons(
|
||||
fromToken: CryptoCurrency?,
|
||||
toCurrency: CryptoCurrency?,
|
||||
): ImmutableList<PredefinedPercentButtonUM> {
|
||||
if (!swapFeatureToggles.isSwapPredefinedButtonsEnabled) return persistentListOf()
|
||||
val shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toCurrency)
|
||||
return PredefinedPercentAmount.entries
|
||||
.filter { it != PredefinedPercentAmount.MAX || shouldShowMaxAmount }
|
||||
.map { percent ->
|
||||
PredefinedPercentButtonUM(
|
||||
id = percent.name,
|
||||
label = percent.toLabel(),
|
||||
onClick = { actions.onPredefinedPercentSelected(percent) },
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private fun PredefinedPercentAmount.toLabel(): TextReference = when (this) {
|
||||
PredefinedPercentAmount.PERCENT_25 -> stringReference("25%")
|
||||
PredefinedPercentAmount.PERCENT_50 -> stringReference("50%")
|
||||
PredefinedPercentAmount.PERCENT_75 -> stringReference("75%")
|
||||
PredefinedPercentAmount.MAX -> resourceReference(R.string.send_max_amount)
|
||||
}
|
||||
|
||||
private fun createTosState(swapProvider: SwapProvider): TosState {
|
||||
return TosState(
|
||||
tosLink = swapProvider.termsOfUse?.let { termsUrl ->
|
||||
|
|
|
|||
|
|
@ -33,17 +33,14 @@ 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.buttons.predefined.PredefinedPercentButtonUM
|
||||
import com.tangem.core.ui.components.buttons.predefined.PredefinedPercentButtonsRow
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SwapTokenScreenTestTags
|
||||
import com.tangem.domain.swap.models.PredefinedPercentAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
import com.tangem.feature.swap.models.*
|
||||
|
|
@ -53,7 +50,6 @@ import com.tangem.feature.swap.presentation.R
|
|||
import com.tangem.feature.swap.ui.preview.SwapTransactionCardPreview.receiveCard
|
||||
import com.tangem.feature.swap.ui.preview.SwapTransactionCardPreview.sendCard
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
|
|
@ -115,50 +111,39 @@ internal fun SwapScreenContent(
|
|||
MainButton(state = state)
|
||||
}
|
||||
|
||||
if (state.shouldShowMaxAmount && keyboard is Keyboard.Opened) {
|
||||
val onPercentClick = state.onPredefinedPercentSelected
|
||||
if (state.isPredefinedButtonsEnabled && onPercentClick != null) {
|
||||
PredefinedPercentButtonsRow(
|
||||
items = PredefinedPercentAmount.entries.map { percent ->
|
||||
PredefinedPercentButtonUM(
|
||||
id = percent.name,
|
||||
label = percent.toLabel(),
|
||||
onClick = { onPercentClick(percent) },
|
||||
)
|
||||
}.toImmutableList(),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.imePadding(),
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.send_max_amount_label),
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable { state.onMaxAmountSelected?.invoke() }
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing14,
|
||||
vertical = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
if (keyboard is Keyboard.Opened) {
|
||||
when {
|
||||
state.predefinedButtons.isNotEmpty() -> {
|
||||
PredefinedPercentButtonsRow(
|
||||
items = state.predefinedButtons,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.imePadding(),
|
||||
)
|
||||
}
|
||||
state.shouldShowMaxAmount -> {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.send_max_amount_label),
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable { state.onMaxAmountSelected?.invoke() }
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing14,
|
||||
vertical = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PredefinedPercentAmount.toLabel() = when (this) {
|
||||
PredefinedPercentAmount.PERCENT_25 -> stringReference("25%")
|
||||
PredefinedPercentAmount.PERCENT_50 -> stringReference("50%")
|
||||
PredefinedPercentAmount.PERCENT_75 -> stringReference("75%")
|
||||
PredefinedPercentAmount.MAX -> resourceReference(R.string.send_max_amount)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainInfo(state: SwapStateHolder) {
|
||||
ConstraintLayout(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ import com.google.common.truth.Truth.assertThat
|
|||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.swap.models.PredefinedPercentAmount
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
|
|
@ -381,4 +387,103 @@ internal class StateBuilderPairsTest {
|
|||
toSwapCurrencyStatus = toStatus,
|
||||
)
|
||||
}
|
||||
|
||||
// region predefined buttons visibility
|
||||
|
||||
@Nested
|
||||
inner class PredefinedButtonsVisibility {
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle on and native coin within same network WHEN updateCurrenciesState THEN MAX button is dropped but percents stay`() {
|
||||
every { swapFeatureToggles.isSwapPredefinedButtonsEnabled } returns true
|
||||
val baseState = buildReadyState(coldWallet)
|
||||
val networkId: Network.ID = mockk(relaxed = true)
|
||||
val fromStatus = buildCoinSwapCurrencyStatus(coldWallet, networkId)
|
||||
val toStatus = buildCoinSwapCurrencyStatus(coldWallet, networkId)
|
||||
|
||||
val result = sut.updateCurrenciesState(
|
||||
uiStateHolder = baseState,
|
||||
emptyAmountState = emptyAmountState,
|
||||
fromSwapCurrencyStatus = fromStatus,
|
||||
toSwapCurrencyStatus = toStatus,
|
||||
shouldResetAmount = false,
|
||||
)
|
||||
|
||||
// Legacy MAX text stays gated by shouldShowMaxAmount ([REDACTED_TASK_KEY] behavior preserved)...
|
||||
assertThat(result.shouldShowMaxAmount).isFalse()
|
||||
// ...and MAX is also dropped from the predefined row, but the percents remain.
|
||||
assertThat(result.predefinedButtons.map { it.id }).containsExactly(
|
||||
PredefinedPercentAmount.PERCENT_25.name,
|
||||
PredefinedPercentAmount.PERCENT_50.name,
|
||||
PredefinedPercentAmount.PERCENT_75.name,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle on and non-coin WHEN updateCurrenciesState THEN all percents including MAX are built`() {
|
||||
every { swapFeatureToggles.isSwapPredefinedButtonsEnabled } returns true
|
||||
val baseState = buildReadyState(coldWallet)
|
||||
val fromStatus = buildSwapCurrencyStatus(coldWallet)
|
||||
val toStatus = buildSwapCurrencyStatus(coldWallet)
|
||||
|
||||
val result = sut.updateCurrenciesState(
|
||||
uiStateHolder = baseState,
|
||||
emptyAmountState = emptyAmountState,
|
||||
fromSwapCurrencyStatus = fromStatus,
|
||||
toSwapCurrencyStatus = toStatus,
|
||||
shouldResetAmount = false,
|
||||
)
|
||||
|
||||
assertThat(result.shouldShowMaxAmount).isTrue()
|
||||
assertThat(result.predefinedButtons.map { it.id })
|
||||
.containsExactlyElementsIn(PredefinedPercentAmount.entries.map { it.name })
|
||||
.inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle off WHEN updateCurrenciesState THEN no predefined buttons are built`() {
|
||||
every { swapFeatureToggles.isSwapPredefinedButtonsEnabled } returns false
|
||||
val baseState = buildReadyState(coldWallet)
|
||||
val fromStatus = buildSwapCurrencyStatus(coldWallet)
|
||||
val toStatus = buildSwapCurrencyStatus(coldWallet)
|
||||
|
||||
val result = sut.updateCurrenciesState(
|
||||
uiStateHolder = baseState,
|
||||
emptyAmountState = emptyAmountState,
|
||||
fromSwapCurrencyStatus = fromStatus,
|
||||
toSwapCurrencyStatus = toStatus,
|
||||
shouldResetAmount = false,
|
||||
)
|
||||
|
||||
assertThat(result.predefinedButtons).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN createInitialLoadingState THEN no predefined buttons are built`() {
|
||||
val result = sut.createInitialLoadingState()
|
||||
|
||||
assertThat(result.predefinedButtons).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun buildCoinSwapCurrencyStatus(userWallet: UserWallet, networkId: Network.ID): SwapCurrencyStatus {
|
||||
val account = Account.CryptoPortfolio.createMainAccount(userWallet.walletId)
|
||||
val coin: CryptoCurrency.Coin = mockk(relaxed = true) {
|
||||
every { decimals } returns 18
|
||||
every { symbol } returns "ETH"
|
||||
every { network } returns mockk(relaxed = true) {
|
||||
every { id } returns networkId
|
||||
}
|
||||
}
|
||||
val statusValue: CryptoCurrencyStatus.Value = mockk(relaxed = true) {
|
||||
every { amount } returns java.math.BigDecimal("1.0")
|
||||
}
|
||||
return SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = CryptoCurrencyStatus(currency = coin, value = statusValue),
|
||||
account = account,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue