Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-19 20:01:38 +05:00
parent 4d75db3cdd
commit 22dc2eb3e9
57 changed files with 931 additions and 258 deletions

View file

@ -2,4 +2,5 @@ package com.tangem.features.tokendetails
interface TokenDetailsFeatureToggles {
val isQuickTopUpEnabled: Boolean
val isManageFundsEnabled: Boolean
}

View file

@ -22,7 +22,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreenLegacy
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.ChooseAddressBottomSheetComponent
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.CloreMigrationBottomSheetComponent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.TransferBottomSheetComponent
@ -47,7 +47,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
expressTransactionsComponentFactory: ExpressTransactionsComponent.Factory,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
private val yieldSupplyWarningComponentFactory: YieldSupplyDepositedWarningComponent.Factory,
private val addFundsComponentFactory: AddFundsComponent.Factory,
private val manageFundsComponentFactory: ManageFundsComponent.Factory,
yieldSupplyComponentFactory: YieldSupplyComponent.Factory,
private val ratingComponentFactory: RatingComponent.Factory,
) : TokenDetailsComponent, AppComponentContext by appComponentContext {
@ -178,10 +178,10 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
dynamicAddressesDelegate = model.dynamicAddressesDelegate,
onDismiss = model.bottomSheetNavigation::dismiss,
)
is TokenDetailsBottomSheetConfig.AddFunds -> addFundsComponentFactory.create(
is TokenDetailsBottomSheetConfig.AddFunds -> manageFundsComponentFactory.create(
context = childByContext(componentContext),
params = AddFundsComponent.Params(
launchMode = AddFundsComponent.LaunchMode.TokenActionsOnly(
params = ManageFundsComponent.Params(
launchMode = ManageFundsComponent.LaunchMode.TokenActionsOnly(
userWalletId = route.userWalletId,
currency = route.currency,
),

View file

@ -6,10 +6,13 @@ import com.tangem.features.tokendetails.TokenDetailsFeatureToggles
import javax.inject.Inject
internal class DefaultTokenDetailsFeatureToggles @Inject constructor(
featureTogglesManager: FeatureTogglesManager,
private val featureTogglesManager: FeatureTogglesManager,
) : TokenDetailsFeatureToggles {
override val isQuickTopUpEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15258_QUICK_TOP_UP_ENABLED,
)
override val isManageFundsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1377_MANAGE_FUNDS)
}

View file

@ -22,6 +22,8 @@ interface TokenDetailsClickIntents {
fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onSwapAndSendClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason)
@ -151,6 +153,8 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents {
override fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onSwapAndSendClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onHideClick() { /* no op */ }

View file

@ -599,6 +599,15 @@ internal class TokenDetailsModel @Inject constructor(
}
override fun onTransferClick() {
val amount = cryptoCurrencyStatus?.value?.amount
if (amount == null || amount.signum() <= 0) {
uiMessageSender.send(
message = SnackbarMessage(
message = resourceReference(R.string.token_button_unavailability_reason_empty_balance_send),
),
)
return
}
bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.Transfer)
}
@ -832,6 +841,20 @@ internal class TokenDetailsModel @Inject constructor(
handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.FROM, checkYieldSupply = true)
}
override fun onSwapAndSendClick(unavailabilityReason: ScenarioUnavailabilityReason) {
if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) {
return
}
appRouter.push(
AppRoute.SendEntryPoint(
userWalletId = userWalletId,
currency = cryptoCurrency,
shouldStartWithSwap = true,
),
)
}
override fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason) {
handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.TO, checkYieldSupply = false)
}

View file

@ -21,6 +21,7 @@ internal sealed interface TransferUM : TangemBottomSheetConfigContent {
data class Content(
val send: Row?,
val swap: Row?,
val swapAndSend: Row?,
val sell: Row?,
) : TransferUM

View file

@ -43,6 +43,16 @@ internal class UpdateTransferTransformer(
},
)
}
val swapAndSendRow = swapAction?.let { action ->
TransferUM.Row(
isLoading = action.unavailabilityReason.isLoading,
isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None,
onClick = {
onActionDispatched()
clickIntents.onSwapAndSendClick(action.unavailabilityReason)
},
)
}
val sellRow = sellAction?.let { action ->
TransferUM.Row(
isLoading = action.unavailabilityReason.isOutdatedLoading(),
@ -55,7 +65,12 @@ internal class UpdateTransferTransformer(
}
return prevState.copy(
transferUM = TransferUM.Content(send = sendRow, swap = swapRow, sell = sellRow),
transferUM = TransferUM.Content(
send = sendRow,
swap = swapRow,
swapAndSend = swapAndSendRow,
sell = sellRow,
),
)
}

View file

@ -36,6 +36,7 @@ internal fun TransferBottomSheetContent(state: TransferUM, onCloseClick: () -> U
) {
SendActionRow(state = state)
SwapActionRow(state = state)
SwapAndSendActionRow(state = state)
SellActionRow(state = state)
SpacerH(TangemTheme.dimens2.x2)
@ -80,6 +81,19 @@ private fun SwapActionRow(state: TransferUM) {
)
}
@Composable
private fun SwapAndSendActionRow(state: TransferUM) {
val row = (state as? TransferUM.Content)?.swapAndSend
if (state is TransferUM.Content && row == null) return
ActionRow(
iconRes = CoreR.drawable.ic_exchange_mini_24,
title = resourceReference(CoreR.string.common_send_with_swap),
description = resourceReference(CoreR.string.quick_action_send_and_swap_description),
row = row,
isLoading = state is TransferUM.Loading,
)
}
@Composable
private fun SellActionRow(state: TransferUM) {
val row = (state as? TransferUM.Content)?.sell
@ -149,16 +163,19 @@ private class TransferPreviewProvider : PreviewParameterProvider<TransferUM> {
TransferUM.Content(
send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
swap = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
swapAndSend = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
sell = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
),
TransferUM.Content(
send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
swap = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}),
swapAndSend = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}),
sell = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}),
),
TransferUM.Content(
send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
swap = null,
swapAndSend = null,
sell = null,
),
)

View file

@ -319,6 +319,57 @@ class UpdateTransferTransformerTest {
assertThat(content.swap?.isEnabled).isTrue()
}
@Test
fun `GIVEN Swap action available WHEN transform THEN swapAndSend row is not null`() {
// Arrange
val transformer = createTransformer(
actions = listOf(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, false)),
)
// Act
val result = transformer.transform(initialState())
// Assert
val content = result.transferUM as TransferUM.Content
assertThat(content.swapAndSend).isNotNull()
}
@Test
fun `GIVEN Swap action available WHEN swapAndSend onClick invoked THEN onSwapAndSendClick is called`() {
// Arrange
val transformer = createTransformer(
actions = listOf(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, false)),
)
// Act
val content = transformer.transform(initialState()).transferUM as TransferUM.Content
content.swapAndSend!!.onClick()
// Assert
verifyOrder {
onActionDispatched.invoke()
clickIntents.onSwapAndSendClick(ScenarioUnavailabilityReason.None)
}
}
@Test
fun `GIVEN no Swap action WHEN transform THEN swapAndSend row is null`() {
// Arrange
val transformer = createTransformer(
actions = listOf(
TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None),
TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None),
),
)
// Act
val result = transformer.transform(initialState())
// Assert
val content = result.transferUM as TransferUM.Content
assertThat(content.swapAndSend).isNull()
}
@Test
fun `GIVEN row WHEN not clicked THEN no callbacks fire`() {
// GIVEN