Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-18 15:14:57 +05:00
parent cbe8cb0b3c
commit 4c7016557f
23 changed files with 731 additions and 31 deletions

View file

@ -40,7 +40,12 @@ fun AmountScreenContent(
bottom = TangemTheme.dimens.spacing16,
),
) {
amountField(amountState = amountState, isBalanceHidden = isBalanceHidden)
amountField(
amountState = amountState,
isBalanceHidden = isBalanceHidden,
onValueChange = clickIntents::onAmountValueChange,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
)
buttons(
segmentedButtonConfig = amountState.segmentedButtonConfig,
clickIntents = clickIntents,

View file

@ -6,7 +6,7 @@ import com.tangem.utils.transformer.Transformer
/**
* Dismisses indication on pasted value
*/
class AmountPastedTriggerDismissTransformer : Transformer<AmountState> {
object AmountPastedTriggerDismissTransformer : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
if (prevState !is AmountState.Data) return prevState

View file

@ -3,6 +3,7 @@ package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverterV2
import com.tangem.common.ui.amountScreen.models.AmountParameters
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
import com.tangem.common.ui.amountScreen.models.AmountState
@ -30,6 +31,7 @@ import kotlinx.collections.immutable.persistentListOf
* @property cryptoCurrencyStatusProvider current cryptocurrency status provider
* @property iconStateConverter currency icon converter
*/
@Deprecated("Use AmountStateConverterV2")
class AmountStateConverter(
private val clickIntents: AmountScreenClickIntents,
private val appCurrencyProvider: Provider<AppCurrency>,
@ -80,4 +82,63 @@ class AmountStateConverter(
selectedButton = 0,
)
}
}
/**
* Converts initial [String] to [AmountState]
*
* @property clickIntents amount screen clicks
* @property appCurrency selected app currency
* @property maxEnterAmount max enter amount data
* @property cryptoCurrencyStatus current cryptocurrency status
* @property iconStateConverter currency icon converter
*/
class AmountStateConverterV2(
private val clickIntents: AmountScreenClickIntents,
private val appCurrency: AppCurrency,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxEnterAmount: EnterAmountBoundary,
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
) : Converter<AmountParameters, AmountState> {
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
AmountFieldConverterV2(
clickIntents = clickIntents,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrency = appCurrency,
)
}
override fun convert(value: AmountParameters): AmountState {
val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) }
val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
return AmountState.Data(
title = value.title,
availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)),
tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus),
amountTextField = amountFieldConverter.convert(value.value),
isPrimaryButtonEnabled = false,
appCurrencyCode = appCurrency.code,
segmentedButtonConfig = persistentListOf(
AmountSegmentedButtonsConfig(
title = stringReference(cryptoCurrencyStatus.currency.symbol),
iconState = iconStateConverter.convertCustom(
value = cryptoCurrencyStatus,
forceGrayscale = noFeeRate,
showCustomTokenBadge = false,
),
isFiat = false,
),
AmountSegmentedButtonsConfig(
title = stringReference(appCurrency.code),
iconUrl = appCurrency.iconSmallUrl,
isFiat = true,
),
),
isSegmentedButtonsEnabled = !noFeeRate,
selectedButton = 0,
)
}
}

View file

@ -31,6 +31,7 @@ class AmountFieldChangeTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxEnterAmount: EnterAmountBoundary,
private val minimumTransactionAmount: EnterAmountBoundary?,
private val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
private val value: String,
) : Transformer<AmountState> {
@ -58,7 +59,7 @@ class AmountFieldChangeTransformer(
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField)
val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false
val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } == true
val isZero = if (amountTextField.isFiatValue) {
decimalFiatValue.isNullOrZero()
} else {
@ -67,6 +68,7 @@ class AmountFieldChangeTransformer(
val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided
return prevState.copy(
isPrimaryButtonEnabled = !isZero && !isCheckFailed,
reduceAmountBy = reduceAmountBy,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,
@ -74,10 +76,11 @@ class AmountFieldChangeTransformer(
error = when {
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
isLessThanMinimumIfProvided -> {
val minimumAmount = minimumTransactionAmount
?.amount
?.format { crypto(cryptoCurrencyStatus.currency) }
.orEmpty()
val minimumAmount =
minimumTransactionAmount.amount.format {
crypto(cryptoCurrencyStatus.currency)
}
resourceReference(
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
wrappedList(minimumAmount, minimumAmount),
@ -98,6 +101,7 @@ class AmountFieldChangeTransformer(
private fun AmountState.Data.emptyState(): AmountState.Data {
return copy(
isPrimaryButtonEnabled = false,
reduceAmountBy = BigDecimal.ZERO,
amountTextField = amountTextField.copy(
value = "",
fiatValue = "",

View file

@ -19,12 +19,13 @@ import com.tangem.utils.isNullOrZero
import java.math.BigDecimal
/**
* Converts initial [String] to [AmountField]
* Converts initial [String] to [AmountFieldModel]
*
* @property clickIntents amount screen clicks
* @property appCurrencyProvider selected app currency provider
* @property cryptoCurrencyStatusProvider current cryptocurrency status provider
*/
@Deprecated("Use AmountFieldConverterV2")
class AmountFieldConverter(
private val clickIntents: AmountScreenClickIntents,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
@ -76,6 +77,68 @@ class AmountFieldConverter(
type = AmountType.FiatType(appCurrency.code),
)
private companion object {
private const val FIAT_DECIMALS = 2
}
}
/**
* Converts initial [String] to [AmountFieldModel]
*
* @property clickIntents amount screen clicks
* @property appCurrency selected app currency
* @property cryptoCurrencyStatus current cryptocurrency status
*/
class AmountFieldConverterV2(
private val clickIntents: AmountScreenClickIntents,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
) : Converter<String, AmountFieldModel> {
override fun convert(value: String): AmountFieldModel {
val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val (fiatValue, fiatDecimal) = when {
fiatRate.isNullOrZero() -> "" to null
value.isEmpty() -> "" to BigDecimal.ZERO
else -> {
val fiatDecimal = fiatRate?.multiply(cryptoDecimal)
val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty()
fiatValue to fiatDecimal
}
}
val isDoneActionEnabled = !cryptoDecimal.isNullOrZero()
return AmountFieldModel(
value = value,
fiatValue = fiatValue,
onValueChange = clickIntents::onAmountValueChange,
keyboardOptions = KeyboardOptions(
imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None,
keyboardType = KeyboardType.Number,
),
keyboardActions = KeyboardActions(
onDone = { clickIntents.onAmountNext() },
),
isFiatValue = false,
cryptoAmount = cryptoAmount,
fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrency),
isError = false,
isWarning = false,
error = TextReference.EMPTY,
isFiatUnavailable = fiatRate == null,
isValuePasted = false,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
)
}
private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount(
currencySymbol = appCurrency.symbol,
value = fiatValue,
decimals = FIAT_DECIMALS,
type = AmountType.FiatType(appCurrency.code),
)
private companion object {
private const val FIAT_DECIMALS = 2
}

View file

@ -15,6 +15,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.extensions.isZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal
import java.math.RoundingMode
/**
@ -26,6 +27,7 @@ class AmountFieldSetMaxAmountTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxAmount: EnterAmountBoundary,
private val minAmount: EnterAmountBoundary?,
private val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
@ -42,9 +44,10 @@ class AmountFieldSetMaxAmountTransformer(
val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals)
val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty()
val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } ?: false
val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } == true
return prevState.copy(
isPrimaryButtonEnabled = !isLessThanMinimumIfProvided,
reduceAmountBy = reduceAmountBy,
amountTextField = amountTextField.copy(
isValuePasted = true,
value = cryptoValue,
@ -52,10 +55,7 @@ class AmountFieldSetMaxAmountTransformer(
isError = isLessThanMinimumIfProvided,
error = when {
isLessThanMinimumIfProvided -> {
val minimumAmount = minAmount
?.amount
?.format { crypto(cryptoCurrencyStatus.currency) }
.orEmpty()
val minimumAmount = minAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
resourceReference(
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
wrappedList(minimumAmount, minimumAmount),

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.PersistentList
import java.math.BigDecimal
/** Model for amount state */
@Stable
@ -21,6 +22,8 @@ sealed class AmountState {
* @param isSegmentedButtonsEnabled indicates if currency switches is enabled
* @param amountTextField amount field state
* @param appCurrencyCode app currency code
* @param isEditingDisabled indicated whether amount is editable
* @param reduceAmountBy reduces amount to be sent by specified value
*/
data class Data(
override val isPrimaryButtonEnabled: Boolean,
@ -32,6 +35,8 @@ sealed class AmountState {
val isSegmentedButtonsEnabled: Boolean,
val amountTextField: AmountFieldModel,
val appCurrencyCode: String,
val isEditingDisabled: Boolean = false,
val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
) : AmountState()
data class Empty(

View file

@ -25,12 +25,13 @@ import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTr
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
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.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.rememberDecimalFormat
import kotlinx.coroutines.delay
@Deprecated("Use AmountField with clicks")
@Composable
internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) {
val decimalFormat = rememberDecimalFormat()
@ -80,6 +81,60 @@ internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String)
AmountSecondary(amountField, appCurrencyCode)
}
@Composable
internal fun AmountField(
amountField: AmountFieldModel,
appCurrencyCode: String,
onValueChange: (String) -> Unit,
onValuePastedTriggerDismiss: () -> Unit,
) {
val decimalFormat = rememberDecimalFormat()
val isFiatValue = amountField.isFiatValue
val currencyCode = if (isFiatValue) appCurrencyCode else null
val (primaryAmount, primaryValue) = if (isFiatValue) {
amountField.fiatAmount to amountField.fiatValue
} else {
amountField.cryptoAmount to amountField.value
}
val requester = remember { FocusRequester() }
AmountTextField(
value = primaryValue,
decimals = primaryAmount.decimals,
visualTransformation = AmountVisualTransformation(
decimals = primaryAmount.decimals,
symbol = primaryAmount.currencySymbol,
currencyCode = currencyCode,
decimalFormat = decimalFormat,
),
onValueChange = onValueChange,
keyboardOptions = amountField.keyboardOptions,
keyboardActions = amountField.keyboardActions,
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
),
isAutoResize = true,
isValuePasted = amountField.isValuePasted,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
modifier = Modifier
.focusRequester(requester)
.padding(
top = TangemTheme.dimens.spacing24,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
)
.requiredHeightIn(min = TangemTheme.dimens.size32),
)
LaunchedEffect(key1 = Unit) {
delay(timeMillis = 200)
requester.requestFocus()
}
AmountSecondary(amountField, appCurrencyCode)
}
@Composable
private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: String) {
val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount
@ -96,11 +151,12 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
val text = if (amountField.isFiatValue) {
secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) }
} else {
BigDecimalFormatter.formatFiatAmount(
fiatAmount = secondaryAmount.value,
fiatCurrencySymbol = secondaryAmount.currencySymbol,
fiatCurrencyCode = appCurrencyCode,
)
secondaryAmount.value.format {
fiat(
fiatCurrencySymbol = secondaryAmount.currencySymbol,
fiatCurrencyCode = appCurrencyCode,
)
}
}
Text(
text = text,

View file

@ -24,6 +24,8 @@ internal fun LazyListScope.amountField(
amountState: AmountState.Data,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
onValueChange: (String) -> Unit,
onValuePastedTriggerDismiss: () -> Unit,
) {
item(key = AMOUNT_FIELD_KEY) {
Column(
@ -63,6 +65,8 @@ internal fun LazyListScope.amountField(
AmountField(
amountField = amountState.amountTextField,
appCurrencyCode = amountState.appCurrencyCode,
onValueChange = onValueChange,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
)
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.features.send.v2.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.send.model.SendModel
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
import dagger.Binds
import dagger.Module
@ -13,6 +15,16 @@ import dagger.multibindings.IntoMap
@InstallIn(ModelComponent::class)
internal interface SendModelModule {
@Binds
@IntoMap
@ClassKey(SendModel::class)
fun provideSendModel(model: SendModel): Model
@Binds
@IntoMap
@ClassKey(SendAmountModel::class)
fun provideSendAmountModel(model: SendAmountModel): Model
@Binds
@IntoMap
@ClassKey(SendDestinationModel::class)

View file

@ -14,6 +14,8 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.model.SendModel
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -63,7 +65,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) {
SendRoute.Empty -> getStubComponent()
is SendRoute.Destination -> getDestinationComponent(factoryContext, route)
is SendRoute.Amount -> getAmountComponent()
is SendRoute.Amount -> getAmountComponent(factoryContext, route)
is SendRoute.Fee -> getFeeComponent()
SendRoute.Confirm -> getConfirmComponent()
}
@ -82,7 +84,20 @@ internal class DefaultSendComponent @AssistedInject constructor(
),
)
private fun getAmountComponent() = getStubComponent() // todo
private fun getAmountComponent(factoryContext: AppComponentContext, route: SendRoute) = SendAmountComponent(
appComponentContext = factoryContext,
params = SendAmountComponentParams.AmountParams(
state = model.uiState.value.amountUM,
currentRoute = currentRoute.filterIsInstance<SendRoute.Amount>(),
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
userWallet = model.userWallet,
appCurrency = model.appCurrency,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
callback = model,
isEditMode = route.isEditMode,
predefinedAmountValue = model.predefinedAmountValue,
),
)
private fun getFeeComponent() = getStubComponent() // todo

View file

@ -4,9 +4,11 @@ import androidx.compose.runtime.Stable
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
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.destination.SendDestinationComponent
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -21,20 +23,28 @@ import kotlin.properties.Delegates
@Suppress("LongParameterList")
internal class SendModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model(), SendDestinationComponent.ModelCallback {
) : Model(),
SendDestinationComponent.ModelCallback,
SendAmountComponent.ModelCallback {
private val _uiState = MutableStateFlow(initialState())
val uiState = _uiState.asStateFlow()
var userWallet: UserWallet by Delegates.notNull()
var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private fun initialState(): SendUM = SendUM(
amountState = AmountState.Empty(),
destinationUM = DestinationUM.Empty(),
)
var appCurrency: AppCurrency = AppCurrency.Default
var predefinedAmountValue: String? = null
override fun onDestinationResult(destinationUM: DestinationUM) {
_uiState.update { it.copy(destinationUM = destinationUM) }
}
override fun onAmountResult(state: AmountState) {
_uiState.update { it.copy(amountUM = state) }
}
private fun initialState(): SendUM = SendUM(
amountUM = AmountState.Empty(),
destinationUM = DestinationUM.Empty(),
)
}

View file

@ -4,6 +4,6 @@ import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
internal data class SendUM(
val amountState: AmountState,
val amountUM: AmountState,
val destinationUM: DestinationUM,
)

View file

@ -0,0 +1,46 @@
package com.tangem.features.send.v2.subcomponents.amount
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.ui.AmountBlock
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountBlockParams
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
internal class SendAmountBlockComponent(
appComponentContext: AppComponentContext,
private val params: AmountBlockParams,
val onResult: (AmountState) -> Unit,
val onClick: () -> Unit,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SendAmountModel = getOrCreateModel(params = params, router = router)
init {
model.uiState.onEach {
onResult(it)
}.launchIn(componentScope)
}
fun updateState(amountUM: AmountState) = model.updateState(amountUM)
@Composable
override fun Content(modifier: Modifier) {
val state = model.uiState.collectAsStateWithLifecycle()
val isClickEnabled = params.blockClickEnableFlow.collectAsStateWithLifecycle()
val isEditingDisabled = params.blockEditDisabledFlow.collectAsStateWithLifecycle()
AmountBlock(
amountState = state.value,
isClickDisabled = !isClickEnabled.value,
isEditingDisabled = isEditingDisabled.value,
onClick = onClick,
)
}
}

View file

@ -0,0 +1,41 @@
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.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
internal class SendAmountComponent(
appComponentContext: AppComponentContext,
private val params: AmountParams,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SendAmountModel = getOrCreateModel(params = params)
fun updateState(amountUM: AmountState) = model.updateState(amountUM)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
AmountScreenContent(
amountState = state,
isBalanceHidden = false,
clickIntents = model,
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
)
}
interface ModelCallback {
fun onAmountResult(amountUM: AmountState)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.send.v2.subcomponents.amount
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
internal sealed class SendAmountComponentParams {
abstract val state: AmountState
abstract val analyticsCategoryName: String
abstract val userWallet: UserWallet
abstract val appCurrency: AppCurrency
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
data class AmountParams(
override val state: AmountState,
override val analyticsCategoryName: String,
override val userWallet: UserWallet,
override val appCurrency: AppCurrency,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
val isEditMode: Boolean,
val callback: ModelCallback,
val currentRoute: Flow<SendRoute.Amount>,
val predefinedAmountValue: String?,
) : SendAmountComponentParams()
data class AmountBlockParams(
override val state: AmountState,
override val analyticsCategoryName: String,
override val userWallet: UserWallet,
override val appCurrency: AppCurrency,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
val blockEditDisabledFlow: StateFlow<Boolean>,
val blockClickEnableFlow: StateFlow<Boolean>,
) : SendAmountComponentParams()
}

View file

@ -0,0 +1,40 @@
package com.tangem.features.send.v2.subcomponents.amount
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import java.math.BigDecimal
import javax.inject.Inject
import javax.inject.Singleton
/**
* Trigger for reducing amount from another component
*/
interface SendAmountReduceTrigger {
suspend fun triggerReduceBy(reduceBy: ReduceByData)
suspend fun triggerReduceTo(reduceTo: BigDecimal)
}
/**
* Trigger for reducing amount from another component
*/
interface SendAmountReduceListener {
val reduceToTriggerFlow: Flow<BigDecimal>
val reduceByTriggerFlow: Flow<ReduceByData>
}
@Singleton
internal class DefaultSendAmountReduceTrigger @Inject constructor() :
SendAmountReduceTrigger,
SendAmountReduceListener {
override val reduceToTriggerFlow = MutableSharedFlow<BigDecimal>()
override val reduceByTriggerFlow = MutableSharedFlow<ReduceByData>()
override suspend fun triggerReduceBy(reduceBy: ReduceByData) {
reduceByTriggerFlow.emit(reduceBy)
}
override suspend fun triggerReduceTo(reduceTo: BigDecimal) {
reduceToTriggerFlow.emit(reduceTo)
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.features.send.v2.subcomponents.amount.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
internal sealed class SendAmountAnalyticEvents(
category: String,
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = category, event = event, params = params) {
/** Selected currency */
data class SelectedCurrency(
val categoryName: String,
val type: SelectedCurrencyType,
) : SendAmountAnalyticEvents(
category = categoryName,
event = "Selected Currency",
params = mapOf(TYPE to type.value),
)
/** Max amount button clicked */
data class MaxAmountButtonClicked(
val categoryName: String,
) : SendAmountAnalyticEvents(category = categoryName, event = "Max Amount Taped")
internal enum class SelectedCurrencyType(val value: String) {
Token("Token"),
AppCurrency("App Currency"),
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.send.v2.subcomponents.amount.di
import com.tangem.features.send.v2.subcomponents.amount.DefaultSendAmountReduceTrigger
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
internal object SendAmountModule {
@Provides
@Singleton
fun provideSendAmountReduceTrigger(): SendAmountReduceTrigger {
return DefaultSendAmountReduceTrigger()
}
@Provides
@Singleton
fun provideSendAmountReduceListener(): SendAmountReduceListener {
return DefaultSendAmountReduceTrigger()
}
}

View file

@ -0,0 +1,239 @@
package com.tangem.features.send.v2.subcomponents.amount.model
import androidx.compose.runtime.Stable
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.converters.*
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer
import com.tangem.common.ui.amountScreen.models.AmountParameters
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.features.send.v2.send.SendRoute
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.analytics.SendAmountAnalyticEvents
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class SendAmountModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val sendAmountReduceListener: SendAmountReduceListener,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model(), AmountScreenClickIntents {
private val params: SendAmountComponentParams = paramsContainer.require()
private val _uiState = MutableStateFlow(params.state)
val uiState = _uiState.asStateFlow()
private val analyticsCategoryName = params.analyticsCategoryName
private val userWallet = params.userWallet
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
private var minAmountBoundary: EnterAmountBoundary? = null
private var maxAmountBoundary: EnterAmountBoundary = MaxEnterAmountConverter().convert(cryptoCurrencyStatus)
init {
configAmountNavigation()
initMinBoundary()
subscribeOnAmountReduceByTriggerUpdates()
subscribeOnAmountReduceToTriggerUpdates()
}
private fun initMinBoundary() {
modelScope.launch {
minAmountBoundary = getMinimumTransactionAmountSyncUseCase(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull()?.let {
EnterAmountBoundary(
amount = it,
fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(),
)
}
initialState()
}
}
private fun initialState() {
val predefinedAmountValue = (params as? SendAmountComponentParams.AmountParams)?.predefinedAmountValue
if (uiState.value is AmountState.Empty) {
_uiState.update {
AmountStateConverterV2(
clickIntents = this,
appCurrency = params.appCurrency,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxAmountBoundary,
iconStateConverter = CryptoCurrencyToIconStateConverter(),
).convert(
AmountParameters(
title = stringReference(userWallet.name),
value = "",
),
)
}
if (predefinedAmountValue != null) {
onAmountValueChange(predefinedAmountValue)
}
}
}
fun updateState(amountUM: AmountState) {
if (amountUM !is AmountState.Empty) {
_uiState.value = amountUM
}
}
override fun onCurrencyChangeClick(isFiat: Boolean) {
_uiState.update(AmountCurrencyTransformer(cryptoCurrencyStatus, isFiat))
}
override fun onAmountValueChange(value: String) {
_uiState.update(
AmountFieldChangeTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxAmountBoundary,
minimumTransactionAmount = minAmountBoundary,
value = value,
),
)
}
override fun onMaxValueClick() {
val decimalCryptoValue = cryptoCurrencyStatus.value.amount
if (decimalCryptoValue.isNullOrZero()) return
_uiState.update(
AmountFieldSetMaxAmountTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxAmount = maxAmountBoundary,
minAmount = minAmountBoundary,
),
)
analyticsEventHandler.send(
SendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = analyticsCategoryName),
)
}
override fun onAmountPasteTriggerDismiss() {
_uiState.update(AmountPastedTriggerDismissTransformer)
}
override fun onAmountNext() {
(uiState.value as? AmountState.Data)?.amountTextField?.isFiatValue?.let { isFiatSelected ->
analyticsEventHandler.send(
SendAmountAnalyticEvents.SelectedCurrency(
categoryName = analyticsCategoryName,
type = if (isFiatSelected) {
SelectedCurrencyType.AppCurrency
} else {
SelectedCurrencyType.Token
},
),
)
}
saveResult()
if ((params as? SendAmountComponentParams.AmountParams)?.isEditMode == true) {
router.pop()
} else {
router.push(SendRoute.Confirm)
}
}
private fun subscribeOnAmountReduceToTriggerUpdates() {
sendAmountReduceListener.reduceToTriggerFlow
.onEach { reduceTo ->
_uiState.update(
AmountReduceToTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minAmountBoundary,
value = reduceTo,
),
)
}
.launchIn(modelScope)
}
private fun subscribeOnAmountReduceByTriggerUpdates() {
sendAmountReduceListener.reduceByTriggerFlow
.onEach { reduceByData ->
_uiState.update(
AmountReduceByTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minAmountBoundary,
value = reduceByData,
),
)
}
.launchIn(modelScope)
}
private fun saveResult() {
val params = params as? SendAmountComponentParams.AmountParams ?: return
params.callback.onAmountResult(uiState.value)
}
private fun configAmountNavigation() {
val params = params as? SendAmountComponentParams.AmountParams ?: return
combine(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (_, _) ->
// todo
// params.callback.onNavigationResult(
// NavigationUM.Content(
// title = resourceReference(R.string.send_amount_label),
// subtitle = null,
// backIconRes = R.drawable.ic_back_24,
// backIconClick = {
// if (route.isEditMode) {
// saveResult()
// }
// router.pop()
// },
// primaryButton = ButtonsUM.PrimaryButtonUM(
// text = if (route.isEditMode) {
// resourceReference(R.string.common_continue)
// } else {
// resourceReference(R.string.common_next)
// },
// isEnabled = state.isPrimaryButtonEnabled,
// onClick = ::onAmountNext,
// ),
// prevButton = ButtonsUM.PrimaryButtonUM(
// text = TextReference.EMPTY,
// iconResId = R.drawable.ic_back_24,
// isEnabled = true,
// onClick = {
// saveResult()
// router.pop()
// },
// ).takeIf { route.isEditMode.not() },
// secondaryPairButtonsUM = null,
// ),
// )
}.launchIn(modelScope)
}
}

View file

@ -20,11 +20,11 @@ import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.*
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder

View file

@ -19,7 +19,7 @@ internal class SendAmountPastedTriggerDismissConverter(
return state.copyWrapped(
isEditState = isEditState,
amountState = AmountPastedTriggerDismissTransformer().transform(amountState),
amountState = AmountPastedTriggerDismissTransformer.transform(amountState),
)
}
}

View file

@ -8,7 +8,7 @@ internal class AmountPasteDismissStateTransformer : Transformer<StakingUiState>
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
amountState = AmountPastedTriggerDismissTransformer().transform(prevState.amountState),
amountState = AmountPastedTriggerDismissTransformer.transform(prevState.amountState),
)
}
}