Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-02 11:55:09 +05:00
parent 9e98ebe77b
commit ed9e188459
22 changed files with 451 additions and 53 deletions

View file

@ -0,0 +1,11 @@
package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.utils.transformer.Transformer
object AmountIgnoreReduceTransformer : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
val state = prevState as? AmountState.Data ?: return prevState
return state.copy(isIgnoreReduce = true)
}
}

View file

@ -62,6 +62,7 @@ class AmountReduceByTransformer(
val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided
return prevState.copy(
isPrimaryButtonEnabled = !isZero && !isCheckFailed,
reduceAmountBy = value.reduceAmountBy,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,

View file

@ -31,7 +31,6 @@ 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> {
@ -68,7 +67,7 @@ class AmountFieldChangeTransformer(
val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided
return prevState.copy(
isPrimaryButtonEnabled = !isZero && !isCheckFailed,
reduceAmountBy = reduceAmountBy,
reduceAmountBy = BigDecimal.ZERO,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,
@ -76,10 +75,9 @@ class AmountFieldChangeTransformer(
error = when {
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
isLessThanMinimumIfProvided -> {
val minimumAmount =
minimumTransactionAmount.amount.format {
crypto(cryptoCurrencyStatus.currency)
}
val minimumAmount = minimumTransactionAmount?.amount.format {
crypto(cryptoCurrencyStatus.currency)
}
resourceReference(
R.string.transfer_notification_invalid_minimum_transaction_amount_text,

View file

@ -27,7 +27,6 @@ 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 {
@ -47,7 +46,7 @@ class AmountFieldSetMaxAmountTransformer(
val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } == true
return prevState.copy(
isPrimaryButtonEnabled = !isLessThanMinimumIfProvided,
reduceAmountBy = reduceAmountBy,
reduceAmountBy = BigDecimal.ZERO,
amountTextField = amountTextField.copy(
isValuePasted = true,
value = cryptoValue,
@ -55,7 +54,7 @@ class AmountFieldSetMaxAmountTransformer(
isError = isLessThanMinimumIfProvided,
error = when {
isLessThanMinimumIfProvided -> {
val minimumAmount = minAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val minimumAmount = minAmount?.amount.format { crypto(cryptoCurrencyStatus.currency) }
resourceReference(
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
wrappedList(minimumAmount, minimumAmount),

View file

@ -25,6 +25,7 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.configToggles)
implementation(projects.core.navigation)
api(projects.core.pagination)
/** Tangem SDK */
implementation(tangemDeps.blockchain)
@ -51,6 +52,8 @@ dependencies {
implementation(projects.domain.settings)
implementation(projects.domain.feedback)
implementation(projects.domain.txhistory)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.balanceHiding)
/** Compose libraries */
implementation(deps.compose.foundation)
@ -64,6 +67,7 @@ dependencies {
/** Other dependencies */
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
implementation(deps.androidx.paging.runtime)
/** DI */
implementation(deps.hilt.android)

View file

@ -1,9 +1,11 @@
package com.tangem.features.send.v2.common
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
@Immutable
internal sealed class NavigationUM {
data class Content(
val title: TextReference,

View file

@ -2,11 +2,17 @@ package com.tangem.features.send.v2.send
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.value.ObserveLifecycleMode
import com.arkivanov.decompose.value.subscribe
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -14,10 +20,14 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter
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.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.model.SendModel
import com.tangem.features.send.v2.send.ui.SendContent
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 com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
@ -26,10 +36,12 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
internal class DefaultSendComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: SendComponent.Params,
private val analyticsEventHandler: AnalyticsEventHandler,
) : SendComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<SendRoute>()
@ -65,9 +77,44 @@ internal class DefaultSendComponent @AssistedInject constructor(
},
)
init {
childStack.subscribe(
lifecycle = lifecycle,
mode = ObserveLifecycleMode.CREATE_DESTROY,
) { stack ->
componentScope.launch {
when (val activeComponent = stack.active.instance) {
is SendConfirmComponent -> if (currentRoute.value.isEditMode) {
analyticsEventHandler.send(SendAnalyticEvents.ConfirmationScreenOpened)
activeComponent.updateState(model.uiState.value)
}
is SendAmountComponent -> {
analyticsEventHandler.send(SendAnalyticEvents.AmountScreenOpened)
activeComponent.updateState(model.uiState.value.amountUM)
}
is SendDestinationComponent -> {
analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened)
activeComponent.updateState(model.uiState.value.destinationUM)
}
is SendFeeComponent -> {
analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened)
}
}
currentRoute.emit(stack.active.configuration)
}
}
}
@Composable
override fun Content(modifier: Modifier) {
val stackState by childStack.subscribeAsState()
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = ::onChildBack)
SendContent(
state = state,
stackState = stackState,
)
}
private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) {
@ -75,7 +122,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
is SendRoute.Destination -> getDestinationComponent(factoryContext, route)
is SendRoute.Amount -> getAmountComponent(factoryContext, route)
is SendRoute.Fee -> getFeeComponent(factoryContext)
SendRoute.Confirm -> getConfirmComponent()
SendRoute.Confirm -> getConfirmComponent(factoryContext)
}
private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) =
@ -84,6 +131,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
params = SendDestinationComponentParams.DestinationParams(
state = model.uiState.value.destinationUM,
currentRoute = currentRoute.filterIsInstance<SendRoute.Destination>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
userWallet = model.userWallet,
cryptoCurrency = params.currency,
@ -97,6 +145,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
params = SendAmountComponentParams.AmountParams(
state = model.uiState.value.amountUM,
currentRoute = currentRoute.filterIsInstance<SendRoute.Amount>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
userWallet = model.userWallet,
appCurrency = model.appCurrency,
@ -132,12 +181,46 @@ internal class DefaultSendComponent @AssistedInject constructor(
}
}
private fun getConfirmComponent() = getStubComponent() // todo
private fun getConfirmComponent(factoryContext: AppComponentContext): SendConfirmComponent {
val predefinedAmount = params.amount
val predefinedTxId = params.transactionId
val predefinedAddress = params.destinationAddress
val predefinedValues =
if (predefinedAmount != null && predefinedTxId != null && predefinedAddress != null) {
SendConfirmComponent.Params.PredefinedValues.Content(
amount = predefinedAmount,
address = predefinedAddress,
tag = params.tag,
transactionId = predefinedTxId,
)
} else {
SendConfirmComponent.Params.PredefinedValues.Empty
}
return SendConfirmComponent(
appComponentContext = factoryContext,
params = SendConfirmComponent.Params(
state = model.uiState.value,
userWallet = model.userWallet,
currentRoute = currentRoute.filterIsInstance<SendRoute.Confirm>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
appCurrency = model.appCurrency,
callback = model,
predefinedValues = predefinedValues,
),
)
}
private fun getStubComponent() = ComposableContentComponent { }
private fun onChildBack() {
if (childStack.value.active.configuration == SendRoute.Empty || childStack.value.backStack.isEmpty()) {
val isEmptyRoute = childStack.value.active.configuration == SendRoute.Empty
val isEmptyStack = childStack.value.backStack.isEmpty()
val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success
if (isEmptyRoute || isEmptyStack || isSuccess) {
router.pop()
} else {
stackNavigation.pop()

View file

@ -489,7 +489,7 @@ internal class SendConfirmModel @Inject constructor(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, _) ->
).distinctUntilChanged().onEach { (state, _) ->
val amountUM = state.amountUM as? AmountState.Data
val confirmUM = state.confirmUM
params.callback.onResult(

View file

@ -1,49 +1,108 @@
package com.tangem.features.send.v2.send.model
import androidx.compose.runtime.Stable
import arrow.core.Either
import arrow.core.getOrElse
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.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
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.model.transformers.SendDestinationInitialStateTransformer
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
internal interface SendComponentCallback :
SendAmountComponent.ModelCallback,
SendFeeComponent.ModelCallback,
SendDestinationComponent.ModelCallback,
SendConfirmComponent.ModelCallback
@Stable
@ModelScoped
@Suppress("LongParameterList")
internal class SendModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model(),
SendDestinationComponent.ModelCallback,
SendAmountComponent.ModelCallback,
SendFeeComponent.ModelCallback,
SendConfirmComponent.ModelCallback {
private val router: Router,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val parseQrCodeUseCase: ParseQrCodeUseCase,
private val sendConfirmAlertFactory: SendConfirmAlertFactory,
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : Model(), SendComponentCallback {
private val params: SendComponent.Params = paramsContainer.require()
private val userWalletId = params.userWalletId
private val cryptoCurrency = params.currency
private val _uiState = MutableStateFlow(initialState())
val uiState = _uiState.asStateFlow()
private val _isBalanceHiddenFlow = MutableStateFlow(false)
val isBalanceHiddenFlow = _isBalanceHiddenFlow.asStateFlow()
var userWallet: UserWallet by Delegates.notNull()
var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var appCurrency: AppCurrency = AppCurrency.Default
var predefinedAmountValue: String? = null
override fun onResult(sendUM: SendUM) {
_uiState.value = sendUM
private var balanceHidingJobHolder = JobHolder()
init {
subscribeOnBalanceHidden()
subscribeOnQRScannerResult()
subscribeOnCurrencyStatusUpdates()
initAppCurrency()
}
override fun onNavigationResult(navigationUM: NavigationUM) {
_uiState.update { it.copy(navigationUM = navigationUM) }
}
override fun onDestinationResult(destinationUM: DestinationUM) {
@ -58,10 +117,146 @@ internal class SendModel @Inject constructor(
_uiState.update { it.copy(feeUM = feeUM) }
}
override fun onResult(sendUM: SendUM) {
_uiState.update { sendUM }
}
private fun initAppCurrency() {
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
}
}
private fun subscribeOnCurrencyStatusUpdates() {
modelScope.launch {
getUserWalletUseCase(params.userWalletId).fold(
ifRight = { wallet ->
userWallet = wallet
val isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
val isMultiCurrency = wallet.isMultiCurrency
getCurrenciesStatusUpdates(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
)
},
ifLeft = {
sendConfirmAlertFactory.getGenericErrorState(::onFailedTxEmailClick)
return@launch
},
)
}
}
private fun subscribeOnBalanceHidden() {
getBalanceHidingSettingsUseCase()
.conflate()
.distinctUntilChanged()
.onEach {
_isBalanceHiddenFlow.value = it.isBalanceHidden
}
.launchIn(modelScope)
.saveIn(balanceHidingJobHolder)
}
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) {
getCurrencyStatus(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
).onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold(
ifRight = { cryptoCurrencyStatus ->
onDataLoaded(
currencyStatus = cryptoCurrencyStatus,
feeCurrencyStatus = getFeeCurrencyStatus(cryptoCurrencyStatus, isMultiCurrency),
)
},
ifLeft = {
sendConfirmAlertFactory.getGenericErrorState {
onFailedTxEmailClick(it.toString())
}
},
)
}.launchIn(modelScope)
}
private fun getCurrencyStatus(
isSingleWalletWithToken: Boolean,
isMultiCurrency: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return if (isMultiCurrency) {
getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = isSingleWalletWithToken,
)
} else {
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId)
}
}
private suspend fun getFeeCurrencyStatus(
cryptoCurrencyStatus: CryptoCurrencyStatus,
isMultiCurrency: Boolean,
): CryptoCurrencyStatus {
return if (isMultiCurrency) {
getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull() ?: cryptoCurrencyStatus
} else {
cryptoCurrencyStatus
}
}
private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, feeCurrencyStatus: CryptoCurrencyStatus) {
cryptoCurrencyStatus = currencyStatus
feeCryptoCurrencyStatus = feeCurrencyStatus
if (params.amount != null) {
router.replaceAll(SendRoute.Confirm)
}
}
private fun subscribeOnQRScannerResult() {
listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() }
.onEach(::onQrCodeScanned)
.launchIn(modelScope)
}
private fun onQrCodeScanned(address: String) {
val parsedQrCode = parseQrCodeUseCase(address, cryptoCurrency).getOrNull()
predefinedAmountValue = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals)
}
private fun onFailedTxEmailClick(errorMessage: String? = null) {
saveBlockchainErrorUseCase(
error = BlockchainErrorInfo(
errorMessage = errorMessage.orEmpty(),
blockchainId = cryptoCurrency.network.id.value,
derivationPath = cryptoCurrency.network.derivationPath.value,
destinationAddress = "",
tokenSymbol = "",
amount = "",
fee = "",
),
)
val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return
modelScope.launch {
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo))
}
}
private fun initialState(): SendUM = SendUM(
amountUM = AmountState.Empty(),
destinationUM = DestinationUM.Empty(),
destinationUM = SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency,
).transform(DestinationUM.Empty()),
feeUM = FeeUM.Empty(),
confirmUM = ConfirmUM.Empty,
navigationUM = NavigationUM.Empty,
)
}

View file

@ -0,0 +1,55 @@
package com.tangem.features.send.v2.send.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.ui.state.SendUM
@Composable
internal fun SendContent(state: SendUM, stackState: ChildStack<SendRoute, ComposableContentComponent>) {
Column(
modifier = Modifier.Companion
.background(color = TangemTheme.colors.background.tertiary)
.fillMaxSize()
.imePadding()
.systemBarsPadding(),
horizontalAlignment = Alignment.Companion.CenterHorizontally,
) {
SendAppBar(navigationUM = state.navigationUM)
Children(
stack = stackState,
animation = stackAnimation(slide()),
modifier = Modifier.weight(1f),
) {
it.instance.Content(Modifier.weight(1f))
}
SendNavigationButtons(navigationUM = state.navigationUM)
}
}
@Composable
private fun SendAppBar(navigationUM: NavigationUM) {
val navigationUM = navigationUM as? NavigationUM.Content ?: return
AppBarWithBackButtonAndIcon(
text = navigationUM.title.resolveReference(),
subtitle = navigationUM.subtitle?.resolveReference(),
onBackClick = navigationUM.backIconClick,
onIconClick = navigationUM.additionalIconClick,
backIconRes = navigationUM.backIconRes,
iconRes = navigationUM.additionalIconRes,
backgroundColor = TangemTheme.colors.background.tertiary,
modifier = Modifier.height(TangemTheme.dimens.size56),
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.send.v2.subcomponents.amount
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.models.AmountState
@ -20,7 +21,7 @@ internal class SendAmountBlockComponent(
val onClick: () -> Unit,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SendAmountModel = getOrCreateModel(params = params, router = router)
private val model: SendAmountModel = getOrCreateModel(params = params)
init {
model.uiState.onEach {
@ -32,14 +33,13 @@ internal class SendAmountBlockComponent(
@Composable
override fun Content(modifier: Modifier) {
val state = model.uiState.collectAsStateWithLifecycle()
val isClickEnabled = params.blockClickEnableFlow.collectAsStateWithLifecycle()
val isEditingDisabled = params.blockEditDisabledFlow.collectAsStateWithLifecycle()
val state by model.uiState.collectAsStateWithLifecycle()
val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle()
AmountBlock(
amountState = state.value,
isClickDisabled = !isClickEnabled.value,
isEditingDisabled = isEditingDisabled.value,
amountState = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.isPredefinedValues,
onClick = onClick,
)
}

View file

@ -27,10 +27,11 @@ internal class SendAmountComponent(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle()
AmountScreenContent(
amountState = state,
isBalanceHidden = false,
isBalanceHidden = isBalanceHidden,
clickIntents = model,
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
)

View file

@ -27,6 +27,7 @@ internal sealed class SendAmountComponentParams {
val callback: ModelCallback,
val currentRoute: Flow<SendRoute.Amount>,
val predefinedAmountValue: String?,
val isBalanceHidingFlow: StateFlow<Boolean>,
) : SendAmountComponentParams()
data class AmountBlockParams(
@ -35,7 +36,8 @@ internal sealed class SendAmountComponentParams {
override val userWallet: UserWallet,
override val appCurrency: AppCurrency,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
val blockEditDisabledFlow: StateFlow<Boolean>,
val blockClickEnableFlow: StateFlow<Boolean>,
val predefinedAmountValue: String?,
val isPredefinedValues: Boolean,
) : SendAmountComponentParams()
}

View file

@ -13,6 +13,7 @@ import javax.inject.Singleton
interface SendAmountReduceTrigger {
suspend fun triggerReduceBy(reduceBy: ReduceByData)
suspend fun triggerReduceTo(reduceTo: BigDecimal)
suspend fun triggerIgnoreReduce()
}
/**
@ -21,6 +22,7 @@ interface SendAmountReduceTrigger {
interface SendAmountReduceListener {
val reduceToTriggerFlow: Flow<BigDecimal>
val reduceByTriggerFlow: Flow<ReduceByData>
val ignoreReduceTriggerFlow: Flow<Unit>
}
@Singleton
@ -29,6 +31,7 @@ internal class DefaultSendAmountReduceTrigger @Inject constructor() :
SendAmountReduceListener {
override val reduceToTriggerFlow = MutableSharedFlow<BigDecimal>()
override val reduceByTriggerFlow = MutableSharedFlow<ReduceByData>()
override val ignoreReduceTriggerFlow = MutableSharedFlow<Unit>()
override suspend fun triggerReduceBy(reduceBy: ReduceByData) {
reduceByTriggerFlow.emit(reduceBy)
@ -37,4 +40,8 @@ internal class DefaultSendAmountReduceTrigger @Inject constructor() :
override suspend fun triggerReduceTo(reduceTo: BigDecimal) {
reduceToTriggerFlow.emit(reduceTo)
}
override suspend fun triggerIgnoreReduce() {
ignoreReduceTriggerFlow.emit(Unit)
}
}

View file

@ -14,11 +14,14 @@ 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.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
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
@ -60,6 +63,7 @@ internal class SendAmountModel @Inject constructor(
initMinBoundary()
subscribeOnAmountReduceByTriggerUpdates()
subscribeOnAmountReduceToTriggerUpdates()
subscribeOnAmountIgnoreReduceTriggerUpdates()
}
private fun initMinBoundary() {
@ -79,7 +83,6 @@ internal class SendAmountModel @Inject constructor(
}
private fun initialState() {
val predefinedAmountValue = (params as? SendAmountComponentParams.AmountParams)?.predefinedAmountValue
if (uiState.value is AmountState.Empty) {
_uiState.update {
AmountStateConverterV2(
@ -95,8 +98,9 @@ internal class SendAmountModel @Inject constructor(
),
)
}
if (predefinedAmountValue != null) {
onAmountValueChange(predefinedAmountValue)
val params = params as? SendAmountComponentParams.AmountBlockParams
if (params?.predefinedAmountValue != null) {
onAmountValueChange(params.predefinedAmountValue)
}
}
}
@ -191,6 +195,12 @@ internal class SendAmountModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeOnAmountIgnoreReduceTriggerUpdates() {
sendAmountReduceListener.ignoreReduceTriggerFlow
.onEach { _uiState.update(AmountIgnoreReduceTransformer::transform) }
.launchIn(modelScope)
}
private fun saveResult() {
val params = params as? SendAmountComponentParams.AmountParams ?: return
params.callback.onAmountResult(uiState.value)
@ -202,7 +212,7 @@ internal class SendAmountModel @Inject constructor(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, route) ->
).distinctUntilChanged().onEach { (state, route) ->
params.callback.onNavigationResult(
NavigationUM.Content(
title = resourceReference(R.string.send_amount_label),

View file

@ -29,7 +29,6 @@ internal class SendDestinationComponent(
SendDestinationContent(state = state, clickIntents = model, isBalanceHidden = isBalanceHidden)
}
interface ModelCallback : SendNavigationModelCallback {
fun onDestinationResult(destinationUM: DestinationUM)
}

View file

@ -357,7 +357,7 @@ internal class SendDestinationModel @Inject constructor(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, route) ->
).distinctUntilChanged().onEach { (state, route) ->
params.callback.onNavigationResult(
NavigationUM.Content(
title = resourceReference(R.string.send_recipient_label),

View file

@ -0,0 +1,19 @@
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.utils.transformer.Transformer
internal class SendDestinationPredefinedStateTransformer(
private val address: String,
private val memo: String?,
) : Transformer<DestinationUM> {
override fun transform(prevState: DestinationUM): DestinationUM {
val state = prevState as? DestinationUM.Content ?: return prevState
return state.copy(
addressTextField = state.addressTextField.copy(value = address, isValuePasted = false),
memoTextField = memo?.let { state.memoTextField?.copy(value = it, isValuePasted = false) },
)
}
}

View file

@ -10,15 +10,11 @@ internal sealed class DestinationTextFieldUM {
/** Current value */
abstract val value: String
/** Lambda be invoked when value is been changed */
abstract val onValueChange: (String) -> Unit
/** Keyboard options */
abstract val keyboardOptions: KeyboardOptions
data class RecipientAddress(
override val value: String,
override val onValueChange: (String) -> Unit,
override val keyboardOptions: KeyboardOptions,
val placeholder: TextReference,
val label: TextReference,
@ -29,7 +25,6 @@ internal sealed class DestinationTextFieldUM {
data class RecipientMemo(
override val value: String,
override val onValueChange: (String) -> Unit,
override val keyboardOptions: KeyboardOptions,
val placeholder: TextReference,
val label: TextReference,

View file

@ -16,7 +16,7 @@ internal sealed class DestinationUM {
val wallets: ImmutableList<DestinationRecipientListUM>,
val networkName: String,
val isValidating: Boolean = false,
val isEditingDisabled: Boolean = false,
val isInitialized: Boolean = false,
) : DestinationUM()
data class Empty(

View file

@ -77,7 +77,7 @@ internal class SendFeeAlertFactory @Inject constructor(
return isFeeTooHigh
}
fun getFeeUpdatedAlert(newFee: TransactionFee, feeUM: FeeUM, onFeeNotIncreased: () -> Unit) {
fun getFeeUpdatedAlert(newFee: TransactionFee, feeUM: FeeUM, proceedAction: () -> Unit, stopAction: () -> Unit) {
if (feeUM !is FeeUM.Content) return
val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
val newFee = when (newFee) {
@ -100,12 +100,16 @@ internal class SendFeeAlertFactory @Inject constructor(
DialogMessage(
message = resourceReference(id = R.string.send_notification_high_fee_title),
dismissOnFirstAction = true,
firstActionBuilder = { okAction() },
secondActionBuilder = { cancelAction() },
firstActionBuilder = {
okAction { proceedAction(); onDismissRequest() }
},
secondActionBuilder = {
cancelAction { stopAction(); onDismissRequest() }
},
),
)
} else {
onFeeNotIncreased()
proceedAction()
}
}

View file

@ -8,12 +8,12 @@ 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.navigation.url.UrlOpener
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
@ -208,7 +208,20 @@ internal class SendFeeModel @Inject constructor(
modelScope.launch {
callFeeUseCase().fold(
ifRight = {
feeCheckReloadTrigger.callbackCheckResult(true)
sendFeeAlertFactory.getFeeUpdatedAlert(
newFee = it,
feeUM = uiState.value,
proceedAction = {
modelScope.launch {
feeCheckReloadTrigger.callbackCheckResult(true)
}
},
stopAction = {
modelScope.launch {
feeCheckReloadTrigger.callbackCheckResult(false)
}
},
)
_uiState.update(
SendFeeLoadedTransformer(
fees = it,
@ -223,7 +236,7 @@ internal class SendFeeModel @Inject constructor(
ifLeft = { feeError ->
feeCheckReloadTrigger.callbackCheckResult(false)
_uiState.update(SendFeeFailedTransformer(feeError))
sendFeeAlertFactory.getFeeUnreachableErrorState(::checkLoadFee)
sendFeeAlertFactory.getFeeUnreachableErrorState(::loadFee)
updateFeeNotifications()
},
)
@ -265,7 +278,7 @@ internal class SendFeeModel @Inject constructor(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, _) ->
).distinctUntilChanged().onEach { (state, _) ->
params.callback.onNavigationResult(
NavigationUM.Content(
title = resourceReference(R.string.common_fee_selector_title),