Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-04 12:26:39 +05:00
parent 7b862a2494
commit 57cc063cdf
15 changed files with 191 additions and 28 deletions

View file

@ -14,7 +14,12 @@ interface SendComponent : ComposableContentComponent {
val amount: String? = null,
val tag: String? = null,
val destinationAddress: String? = null,
val callback: ModelCallback? = null,
)
interface Factory : ComponentFactory<Params, SendComponent>
interface ModelCallback {
fun onConvertToAnotherToken(lastAmount: String)
}
}

View file

@ -3,4 +3,5 @@ package com.tangem.features.send.v2.api
interface SendFeatureToggles {
val isSendRedesignEnabled: Boolean
val isSendWithSwapEnabled: Boolean
}

View file

@ -1,10 +1,17 @@
package com.tangem.features.send.v2.api.subcomponents.destination
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
interface SendDestinationBlockComponent : ComposableContentComponent {
interface Factory :
ComponentFactory<SendDestinationComponentParams.DestinationBlockParams, SendDestinationBlockComponent>
interface Factory {
fun create(
context: AppComponentContext,
params: SendDestinationComponentParams.DestinationBlockParams,
onClick: () -> Unit,
onResult: (DestinationUM) -> Unit,
): SendDestinationBlockComponent
}
}

View file

@ -7,6 +7,8 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat
interface SendDestinationComponent : ComposableContentComponent {
fun updateState(destinationUM: DestinationUM)
interface ModelCallback : NavigationModelCallback {
fun onDestinationResult(destinationUM: DestinationUM)
}

View file

@ -8,4 +8,6 @@ internal class DefaultSendFeatureToggles(
) : SendFeatureToggles {
override val isSendRedesignEnabled: Boolean
get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED")
override val isSendWithSwapEnabled: Boolean
get() = featureToggles.isFeatureEnabled("SEND_VIA_SWAP_ENABLED")
}

View file

@ -49,7 +49,7 @@ import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateQRTrigger
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.SendDestinationInitialStateTransformer
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
@ -89,7 +89,7 @@ internal class SendModel @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val sendAmountUpdateQRTrigger: SendAmountUpdateQRTrigger,
private val sendAmountUpdateTrigger: SendAmountUpdateTrigger,
private val sendFeatureToggles: SendFeatureToggles,
) : Model(), SendComponentCallback {
@ -153,6 +153,10 @@ internal class SendModel @Inject constructor(
_uiState.update { sendUM }
}
override fun onConvertToAnotherToken(lastAmount: String) {
params.callback?.onConvertToAnotherToken(lastAmount = lastAmount)
}
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
val predefinedValues = predefinedValues
val transferTransaction = if (predefinedValues is PredefinedValues.Content.Deeplink) {
@ -349,7 +353,7 @@ internal class SendModel @Inject constructor(
)
// If it is in active state use flow to update value in amount component
modelScope.launch {
amount?.let { sendAmountUpdateQRTrigger.triggerUpdateAmount(it) }
amount?.let { sendAmountUpdateTrigger.triggerUpdateAmount(it) }
}
}

View file

@ -1,19 +1,17 @@
package com.tangem.features.send.v2.subcomponents.amount
import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.amountScreen.AmountScreenContent
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountParams
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
import com.tangem.features.send.v2.subcomponents.amount.ui.SendAmountContent
internal class SendAmountComponent(
appComponentContext: AppComponentContext,
@ -29,15 +27,17 @@ internal class SendAmountComponent(
val state by model.uiState.collectAsStateWithLifecycle()
val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle()
AmountScreenContent(
SendAmountContent(
amountState = state,
isBalanceHidden = isBalanceHidden,
clickIntents = model,
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
isSendWithSwapEnabled = model.isSendWithSwapEnabled,
modifier = modifier,
)
}
interface ModelCallback : NavigationModelCallback {
fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean)
fun onConvertToAnotherToken(lastAmount: String)
}
}

View file

@ -29,7 +29,7 @@ interface SendAmountReduceListener {
* Trigger amount change from another component.
* Different from another triggers because it takes raw string instead of BigDecimal
*/
interface SendAmountUpdateQRTrigger {
interface SendAmountUpdateTrigger {
suspend fun triggerUpdateAmount(amountValue: String)
}
@ -37,7 +37,7 @@ interface SendAmountUpdateQRTrigger {
* Trigger amount change from another component.
* Different from another triggers because it takes raw string instead of BigDecimal
*/
interface SendAmountUpdateQRListener {
interface SendAmountUpdateListener {
val updateAmountTriggerFlow: Flow<String>
}
@ -45,8 +45,8 @@ interface SendAmountUpdateQRListener {
internal class DefaultSendAmountReduceTrigger @Inject constructor() :
SendAmountReduceTrigger,
SendAmountReduceListener,
SendAmountUpdateQRTrigger,
SendAmountUpdateQRListener {
SendAmountUpdateTrigger,
SendAmountUpdateListener {
override val reduceToTriggerFlow = MutableSharedFlow<BigDecimal>()
override val reduceByTriggerFlow = MutableSharedFlow<ReduceByData>()

View file

@ -1,7 +1,6 @@
package com.tangem.features.send.v2.subcomponents.amount.di
import com.tangem.features.send.v2.subcomponents.amount.*
import com.tangem.features.send.v2.subcomponents.amount.DefaultSendAmountReduceTrigger
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -22,9 +21,9 @@ internal interface SendAmountModule {
@Singleton
@Binds
fun provideSendAmountUpdateQRListener(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateQRListener
fun provideSendAmountUpdateListener(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateListener
@Singleton
@Binds
fun provideSendAmountUpdateQRTrigger(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateQRTrigger
fun provideSendAmountUpdateTrigger(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateTrigger
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.send.v2.subcomponents.amount.model
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
interface SendAmountClickIntents : AmountScreenClickIntents {
fun onConvertToAnotherToken()
}

View file

@ -2,7 +2,6 @@ package com.tangem.features.send.v2.subcomponents.amount.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.converters.*
import com.tangem.common.ui.amountScreen.converters.field.AmountBoundaryUpdateTransformer
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
@ -31,7 +30,7 @@ import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateQRListener
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateListener
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType
import com.tangem.features.send.v2.subcomponents.fee.SendFeeData
@ -54,12 +53,12 @@ internal class SendAmountModel @Inject constructor(
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val sendAmountReduceListener: SendAmountReduceListener,
private val feeReloadTrigger: SendFeeReloadTrigger,
private val sendAmountUpdateQRListener: SendAmountUpdateQRListener,
private val sendAmountUpdateListener: SendAmountUpdateListener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeatureToggles: SendFeatureToggles,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model(), AmountScreenClickIntents {
) : Model(), SendAmountClickIntents {
private val params: SendAmountComponentParams = paramsContainer.require()
private var appCurrency: AppCurrency = AppCurrency.Default
@ -68,6 +67,8 @@ internal class SendAmountModel @Inject constructor(
private val _uiState = MutableStateFlow(params.state)
val uiState = _uiState.asStateFlow()
val isSendWithSwapEnabled = sendFeatureToggles.isSendWithSwapEnabled
private val analyticsCategoryName = params.analyticsCategoryName
private var cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = params.cryptoCurrency,
@ -99,7 +100,7 @@ internal class SendAmountModel @Inject constructor(
subscribeOnAmountReduceByTriggerUpdates()
subscribeOnAmountReduceToTriggerUpdates()
subscribeOnAmountIgnoreReduceTriggerUpdates()
subscribeOnAmountUpdateQRTriggerUpdates()
subscribeOnAmountUpdateTriggerUpdates()
}
initMinBoundary()
}
@ -217,6 +218,12 @@ internal class SendAmountModel @Inject constructor(
saveResult()
}
override fun onConvertToAnotherToken() {
val amountFieldData = uiState.value as? AmountState.Data
val amountParams = params as? SendAmountComponentParams.AmountParams
amountParams?.callback?.onConvertToAnotherToken(amountFieldData?.amountTextField?.value.orEmpty())
}
private fun subscribeOnAmountReduceToTriggerUpdates() {
sendAmountReduceListener.reduceToTriggerFlow
.onEach { reduceTo ->
@ -261,8 +268,8 @@ internal class SendAmountModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeOnAmountUpdateQRTriggerUpdates() {
sendAmountUpdateQRListener.updateAmountTriggerFlow
private fun subscribeOnAmountUpdateTriggerUpdates() {
sendAmountUpdateListener.updateAmountTriggerFlow
.onEach { amount ->
onAmountValueChange(amount)
saveResult()

View file

@ -0,0 +1,109 @@
package com.tangem.features.send.v2.subcomponents.amount.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.amountScreen.AmountScreenContent
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountClickIntents
import com.tangem.features.send.v2.subcomponents.amount.ui.preview.SendAmountClickIntentsStub
@Composable
fun SendAmountContent(
amountState: AmountState,
isBalanceHidden: Boolean,
clickIntents: SendAmountClickIntents,
isSendWithSwapEnabled: Boolean,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.background(TangemTheme.colors.background.tertiary)) {
AmountScreenContent(
amountState = amountState,
isBalanceHidden = isBalanceHidden,
clickIntents = clickIntents,
)
if (isSendWithSwapEnabled) {
SendConvertTokenButton(
onConvertToAnother = clickIntents::onConvertToAnotherToken,
)
}
}
}
@Composable
private fun SendConvertTokenButton(onConvertToAnother: () -> Unit) {
Box(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
.clickable(
indication = null,
interactionSource = null,
onClick = onConvertToAnother,
),
) {
Row(
modifier = Modifier
.padding(12.dp)
.align(Alignment.Center),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_convert_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(20.dp)
.background(TangemTheme.colors.control.unchecked, CircleShape)
.padding(2.dp),
)
Text(
text = stringResourceSafe(com.tangem.common.ui.R.string.send_amount_convert_to_another_token),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun SendAmountContent_Preview(@PreviewParameter(SendAmountContentPreviewProvider::class) params: AmountState) {
TangemThemePreview {
SendAmountContent(
amountState = params,
isBalanceHidden = true,
clickIntents = SendAmountClickIntentsStub,
isSendWithSwapEnabled = true,
)
}
}
private class SendAmountContentPreviewProvider : PreviewParameterProvider<AmountState> {
override val values: Sequence<AmountState>
get() = sequenceOf(
AmountStatePreviewData.amountStateV2,
)
}
// endregion

View file

@ -0,0 +1,17 @@
package com.tangem.features.send.v2.subcomponents.amount.ui.preview
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountClickIntents
internal object SendAmountClickIntentsStub : SendAmountClickIntents {
override fun onConvertToAnotherToken() {}
override fun onAmountValueChange(value: String) {}
override fun onAmountPasteTriggerDismiss() {}
override fun onMaxValueClick() {}
override fun onCurrencyChangeClick(isFiat: Boolean) {}
override fun onAmountNext() {}
}

View file

@ -21,8 +21,8 @@ import kotlinx.coroutines.flow.onEach
internal class DefaultSendDestinationBlockComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: SendDestinationComponentParams.DestinationBlockParams,
val onClick: () -> Unit,
val onResult: (DestinationUM) -> Unit,
@Assisted val onClick: () -> Unit,
@Assisted val onResult: (DestinationUM) -> Unit,
) : SendDestinationBlockComponent, AppComponentContext by appComponentContext {
private val model: SendDestinationModel = getOrCreateModel(params = params)
@ -54,6 +54,8 @@ internal class DefaultSendDestinationBlockComponent @AssistedInject constructor(
override fun create(
context: AppComponentContext,
params: SendDestinationComponentParams.DestinationBlockParams,
onClick: () -> Unit,
onResult: (DestinationUM) -> Unit,
): DefaultSendDestinationBlockComponent
}
}

View file

@ -22,7 +22,7 @@ internal class DefaultSendDestinationComponent @AssistedInject constructor(
private val model: SendDestinationModel = getOrCreateModel(params = params)
fun updateState(state: DestinationUM) = model.updateState(state)
override fun updateState(destinationUM: DestinationUM) = model.updateState(destinationUM)
@Composable
override fun Content(modifier: Modifier) {