Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-01 13:40:00 +05:00
commit 6ddf9a25ae
20 changed files with 1383 additions and 3 deletions

View file

@ -24,6 +24,7 @@ sealed class AmountState {
* @param appCurrencyCode app currency code
* @param isEditingDisabled indicated whether amount is editable
* @param reduceAmountBy reduces amount to be sent by specified value
* @param isIgnoreReduce ignores reduce amount value
*/
data class Data(
override val isPrimaryButtonEnabled: Boolean,
@ -37,6 +38,7 @@ sealed class AmountState {
val appCurrencyCode: String,
val isEditingDisabled: Boolean = false,
val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
val isIgnoreReduce: Boolean = false,
) : AmountState()
data class Empty(

View file

@ -59,6 +59,7 @@ dependencies {
implementation(deps.compose.material3)
implementation(deps.decompose.ext.compose)
implementation(deps.androidx.activity.compose)
implementation(deps.androidx.paging.runtime)
/** Other dependencies */
implementation(deps.kotlin.immutable.collections)

View file

@ -2,6 +2,7 @@ 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.confirm.model.SendConfirmModel
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
@ -35,4 +36,9 @@ internal interface SendModelModule {
@IntoMap
@ClassKey(SendFeeModel::class)
fun provideSendFeeModel(model: SendFeeModel): Model
@Binds
@IntoMap
@ClassKey(SendConfirmModel::class)
fun provideSendConfirmModel(model: SendConfirmModel): Model
}

View file

@ -1,6 +1,9 @@
package com.tangem.features.send.v2.send.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
/**
@ -23,6 +26,18 @@ internal sealed class SendAnalyticEvents(
/** Confirmation screen opened */
data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened")
/** Transaction send screen opened */
data class TransactionScreenOpened(
val token: String,
val feeType: AnalyticsParam.FeeType,
) : SendAnalyticEvents(
event = "Transaction Sent Screen Opened",
params = mapOf(
TOKEN_PARAM to token,
FEE_TYPE to feeType.value,
),
)
/** If transaction delays notification is present */
data class NoticeTransactionDelays(
val token: String,
@ -37,7 +52,40 @@ internal sealed class SendAnalyticEvents(
params = mapOf(TOKEN_PARAM to token),
)
/** Close button clicked */
data class CloseButtonClicked(
val source: SendScreenSource,
val isFromSummary: Boolean,
val isValid: Boolean,
) : SendAnalyticEvents(
event = "Button - Close",
params = mapOf(
SOURCE to source.name,
"FromSummary" to if (isFromSummary) "Yes" else "No",
"isValid" to if (isValid) "Yes" else "No",
),
)
/** Share button clicked */
data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
/** Expore button clicked */
data object ExploreButtonClicked : SendAnalyticEvents(event = "Button - Explore")
/** Screen reopened from confirmation screen */
data class ScreenReopened(val source: SendScreenSource) : SendAnalyticEvents(
event = "Screen Reopened",
params = mapOf(SOURCE to source.name),
)
companion object {
const val SEND_CATEGORY = "Token / Send"
}
internal enum class SendScreenSource {
Address,
Amount,
Fee,
Confirm,
}
}

View file

@ -0,0 +1,53 @@
package com.tangem.features.send.v2.send.analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import javax.inject.Inject
@ModelScoped
internal class SendAnalyticHelper @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun sendSuccessAnalytics(cryptoCurrency: CryptoCurrency, sendUM: SendUM) {
val destinationUM = sendUM.destinationUM as? DestinationUM.Content
val feeUM = sendUM.feeUM as? FeeUM.Content
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content ?: return
val feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM)
analyticsEventHandler.send(
SendAnalyticEvents.TransactionScreenOpened(
token = cryptoCurrency.symbol,
feeType = feeType,
),
)
analyticsEventHandler.send(
Basic.TransactionSent(
sentFrom = AnalyticsParam.TxSentFrom.Send(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = feeType,
),
memoType = getSendTransactionMemoType(destinationUM?.memoTextField),
),
)
}
private fun getSendTransactionMemoType(
recipientMemo: DestinationTextFieldUM.RecipientMemo?,
): Basic.TransactionSent.MemoType {
val memo = recipientMemo?.value
return when {
memo?.isBlank() == true -> Basic.TransactionSent.MemoType.Empty
memo?.isNotBlank() == true -> Basic.TransactionSent.MemoType.Full
else -> Basic.TransactionSent.MemoType.Null
}
}
}

View file

@ -0,0 +1,160 @@
package com.tangem.features.send.v2.send.confirm
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
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.send.confirm.model.SendConfirmModel
import com.tangem.features.send.v2.send.confirm.ui.SendConfirmContent
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.SendAmountBlockComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.*
internal class SendConfirmComponent(
appComponentContext: AppComponentContext,
params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SendConfirmModel = getOrCreateModel(params = params)
private val blockClickEnableFlow = MutableStateFlow(false)
// private val destinationBlockComponent =
// SendDestinationBlockComponent(
// appComponentContext = child("sendConfirmDestinationBlock"),
// params = SendDestinationComponentParams.DestinationBlockParams(
// state = model.uiState.value.destinationUM,
// analyticsCategoryName = params.analyticsCategoryName,
// userWallet = params.userWallet,
// cryptoCurrency = params.cryptoCurrencyStatus.currency,
// blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
// isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
// predefinedAddressValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.address,
// predefinedMemoValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.tag,
// ),
// onResult = model::onDestinationResult,
// onClick = model::showEditDestination,
// )
private val amountBlockComponent = SendAmountBlockComponent(
appComponentContext = child("sendConfirmAmountBlock"),
params = SendAmountComponentParams.AmountBlockParams(
state = model.uiState.value.amountUM,
analyticsCategoryName = params.analyticsCategoryName,
userWallet = params.userWallet,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
appCurrency = params.appCurrency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
blockEditDisabledFlow = TODO(),
// isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
// predefinedAmountValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.amount,
),
onResult = model::onAmountResult,
onClick = model::showEditAmount,
)
private val feeBlockComponent = SendFeeBlockComponent(
appComponentContext = child("sendConfirmFeeBlock"),
params = SendFeeComponentParams.FeeBlockParams(
state = model.uiState.value.feeUM,
analyticsCategoryName = params.analyticsCategoryName,
userWallet = params.userWallet,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
appCurrency = params.appCurrency,
sendAmount = model.enteredAmount.orZero(),
destinationAddress = model.enteredDestination.orEmpty(),
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
),
onResult = model::onFeeResult,
onClick = model::showEditFee,
)
private val notificationsComponent = NotificationsComponent(
appComponentContext = child("sendConfirmNotifications"),
params = NotificationsComponent.Params(
analyticsCategoryName = params.analyticsCategoryName,
userWalletId = params.userWallet.walletId,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
appCurrency = params.appCurrency,
destinationAddress = model.enteredDestination.orEmpty(),
amountValue = model.enteredAmount.orZero(),
reduceAmountBy = model.reduceAmountBy.orZero(),
isIgnoreReduce = model.isIgnoreReduce,
fee = model.fee,
feeError = model.feeError,
),
)
init {
model.uiState.onEach { state ->
val confirmUM = state.confirmUM as? ConfirmUM.Content
blockClickEnableFlow.value = confirmUM?.isSending == false
}.launchIn(componentScope)
}
fun updateState(state: SendUM) {
// todo
// destinationBlockComponent.updateState(state.destinationUM)
amountBlockComponent.updateState(state.amountUM)
feeBlockComponent.updateState(state.feeUM)
model.updateState(state)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val notificationState by notificationsComponent.state.collectAsStateWithLifecycle()
SendConfirmContent(
sendUM = state,
// destinationBlockComponent = destinationBlockComponent,
amountBlockComponent = amountBlockComponent,
feeBlockComponent = feeBlockComponent,
notificationsComponent = notificationsComponent,
notificationsUM = notificationState,
)
}
data class Params(
val state: SendUM,
val analyticsCategoryName: String,
val userWallet: UserWallet,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
val appCurrency: AppCurrency,
val callback: ModelCallback,
val currentRoute: Flow<SendRoute.Confirm>,
val isBalanceHidingFlow: StateFlow<Boolean>,
val predefinedValues: PredefinedValues,
) {
sealed class PredefinedValues {
data object Empty : PredefinedValues()
data class Content(
val transactionId: String,
val amount: String,
val address: String,
val tag: String?,
) : PredefinedValues()
}
}
interface ModelCallback {
fun onResult(sendUM: SendUM)
}
}

View file

@ -0,0 +1,63 @@
package com.tangem.features.send.v2.send.confirm.model
import com.tangem.common.ui.alerts.TransactionErrorAlertConverter
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.features.send.v2.impl.R
import javax.inject.Inject
@ModelScoped
internal class SendConfirmAlertFactory @Inject constructor(
private val messageSender: UiMessageSender,
) {
fun getGenericErrorState(onFailedTxEmailClick: () -> Unit) {
messageSender.send(
DialogMessage(
title = resourceReference(id = R.string.send_alert_transaction_failed_title),
message = resourceReference(id = R.string.common_unknown_error),
firstAction = EventMessageAction(
title = resourceReference(R.string.common_support),
onClick = onFailedTxEmailClick,
),
),
)
}
fun getSendTransactionErrorState(
error: SendTransactionError?,
popBack: () -> Unit,
onFailedTxEmailClick: (String) -> Unit,
) {
val transactionErrorAlertConverter = TransactionErrorAlertConverter(
popBackStack = popBack,
onFailedTxEmailClick = onFailedTxEmailClick,
)
val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return
val onConfirmClick = errorAlert.onConfirmClick ?: return
messageSender.send(
DialogMessage(
title = errorAlert.title,
message = errorAlert.message,
firstActionBuilder = {
EventMessageAction(
title = errorAlert.confirmButtonText,
onClick = onConfirmClick,
)
},
secondActionBuilder = if (errorAlert !is AlertDemoModeUM) {
{ cancelAction() }
} else {
null
},
),
)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.send.v2.send.confirm.model
internal interface SendConfirmClickIntents {
fun showEditDestination()
fun showEditAmount()
fun showEditFee()
fun onSendClick()
fun onExploreClick()
fun onShareClick()
fun onFailedTxEmailClick(errorMessage: String)
}

View file

@ -0,0 +1,554 @@
package com.tangem.features.send.v2.send.confirm.model
import android.os.SystemClock
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.amountScreen.models.AmountState
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.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
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.settings.IsSendTapHelpEnabledUseCase
import com.tangem.domain.settings.NeverShowTapHelpUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.send.analytics.SendAnalyticHelper
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmInitialStateTransformer
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSendingStateTransformer
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSentStateTransformer
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformer
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.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger
import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData
import com.tangem.features.txhistory.TxHistoryFeatureToggles
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import com.tangem.utils.extensions.orZero
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.utils.transformer.update
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass")
@Stable
@ModelScoped
internal class SendConfirmModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val router: Router,
private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase,
private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase,
private val createTransactionUseCase: CreateTransactionUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter,
private val notificationsUpdateTrigger: NotificationsUpdateTrigger,
private val alertFactory: SendConfirmAlertFactory,
private val sendAnalyticHelper: SendAnalyticHelper,
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
@DelayedWork private val coroutineScope: CoroutineScope,
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
) : Model(), SendConfirmClickIntents {
private val params: SendConfirmComponent.Params = paramsContainer.require()
private val userWallet = params.userWallet
private val appCurrency = params.appCurrency
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
private val cryptoCurrency = cryptoCurrencyStatus.currency
private val _uiState = MutableStateFlow(params.state)
val uiState = _uiState.asStateFlow()
private val amountState
get() = uiState.value.amountUM as? AmountState.Data
private val destinationUM
get() = uiState.value.destinationUM as? DestinationUM.Content
private val feeUM
get() = uiState.value.feeUM as? FeeUM.Content
private val feeSelectorUM
get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
val enteredAmount: BigDecimal?
get() = amountState?.amountTextField?.cryptoAmount?.value
val reduceAmountBy: BigDecimal
get() = amountState?.reduceAmountBy.orZero()
val isIgnoreReduce: Boolean
get() = amountState?.isIgnoreReduce == true
val enteredDestination: String?
get() = destinationUM?.addressTextField?.value
val fee: Fee?
get() = feeSelectorUM?.selectedFee
val feeError: GetFeeError?
get() = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error
private var sendIdleTimer: Long = 0L
private var isAmountSubtractAvailable = false
init {
modelScope.launch {
isAmountSubtractAvailable =
isAmountSubtractAvailableUseCase(userWallet.walletId, cryptoCurrency).getOrElse { false }
}
configConfirmNavigation()
subscribeOnNotificationsUpdateTrigger()
subscribeOnCheckFeeResultUpdates()
initialState()
}
fun updateState(state: SendUM) {
_uiState.value = state
updateConfirmNotifications()
}
fun onFeeResult(feeUM: FeeUM) {
sendIdleTimer = SystemClock.elapsedRealtime()
_uiState.update { it.copy(feeUM = feeUM) }
updateConfirmNotifications()
}
fun onAmountResult(amountUM: AmountState) {
_uiState.update { it.copy(amountUM = amountUM) }
updateConfirmNotifications()
}
fun onDestinationResult(destinationUM: DestinationUM) {
_uiState.update { it.copy(destinationUM = destinationUM) }
updateConfirmNotifications()
}
override fun showEditDestination() {
modelScope.launch {
neverShowTapHelpUseCase()
_uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
router.push(SendRoute.Destination(isEditMode = true))
}
}
override fun showEditAmount() {
modelScope.launch {
neverShowTapHelpUseCase()
_uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
router.push(SendRoute.Amount(isEditMode = true))
}
}
override fun showEditFee() {
modelScope.launch {
neverShowTapHelpUseCase()
_uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
router.push(SendRoute.Fee())
}
}
override fun onSendClick() {
_uiState.update(SendConfirmSendingStateTransformer(isSending = true))
if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) {
verifyAndSendTransaction()
} else {
modelScope.launch {
sendFeeCheckReloadTrigger.triggerCheckUpdate()
}
}
}
override fun onExploreClick() {
val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return
analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
urlOpener.openUrl(confirmUM.txUrl)
}
override fun onShareClick() {
val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return
analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked)
shareManager.shareText(confirmUM.txUrl)
}
override fun onFailedTxEmailClick(errorMessage: String) {
val amountValue = amountState?.amountTextField?.cryptoAmount?.value
val feeValue = fee?.amount?.value
val receivingAmount = if (amountValue != null && feeValue != null) {
checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = enteredAmount.orZero(),
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
} else {
null
}
val amount = receivingAmount?.convertToSdkAmount(cryptoCurrency)
saveBlockchainErrorUseCase(
error = BlockchainErrorInfo(
errorMessage = errorMessage,
blockchainId = cryptoCurrency.network.id.value,
derivationPath = cryptoCurrency.network.derivationPath.value,
destinationAddress = enteredDestination.orEmpty(),
tokenSymbol = if (amount?.type is AmountType.Token) {
amount.currencySymbol
} else {
""
},
amount = amount?.value?.stripZeroPlainString() ?: "unknown",
fee = feeValue?.convertToSdkAmount(cryptoCurrency)
?.value?.stripZeroPlainString() ?: "unknown",
),
)
val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return
modelScope.launch {
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo))
}
}
private fun initialState() {
val confirmUM = uiState.value.confirmUM
val amountUM = uiState.value.amountUM
val feeUM = uiState.value.feeUM
modelScope.launch {
val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false }
if (confirmUM is ConfirmUM.Empty || feeUM is FeeUM.Empty) {
_uiState.update {
it.copy(
confirmUM = SendConfirmInitialStateTransformer(
appCurrency = appCurrency,
feeUM = feeUM,
amountUM = amountUM,
isShowTapHelp = isShowTapHelp,
isSubtracted = false,
).transform(uiState.value.confirmUM),
)
}
updateConfirmNotifications()
}
}
}
private fun subscribeOnNotificationsUpdateTrigger() {
notificationsUpdateTrigger.hasErrorFlow
.onEach { hasError ->
_uiState.update {
val feeUM = it.feeUM as? FeeUM.Content
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
it.copy(
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
isPrimaryButtonEnabled = !hasError && feeSelectorUM != null,
) ?: it.confirmUM,
)
}
}
.launchIn(modelScope)
}
private fun verifyAndSendTransaction() {
val amountValue = amountState?.amountTextField?.cryptoAmount?.value ?: return
val destination = destinationUM?.addressTextField?.value ?: return
val memo = destinationUM?.memoTextField?.value
val fee = feeSelectorUM?.selectedFee
val feeValue = fee?.amount?.value ?: return
val receivingAmount = checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy.orZero(),
)
modelScope.launch {
createTransactionUseCase(
amount = receivingAmount.convertToSdkAmount(cryptoCurrency),
fee = fee,
memo = memo,
destination = destination,
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
).fold(
ifLeft = { error ->
Timber.e(error)
_uiState.update(SendConfirmSendingStateTransformer(isSending = false))
alertFactory.getGenericErrorState {
onFailedTxEmailClick(error.localizedMessage.orEmpty())
}
},
ifRight = { txData ->
sendTransaction(txData)
},
)
}
}
private suspend fun sendTransaction(txData: TransactionData.Uncompiled) {
val result = sendTransactionUseCase(
txData = txData,
userWallet = userWallet,
network = cryptoCurrency.network,
)
_uiState.update(SendConfirmSendingStateTransformer(isSending = false))
result.fold(
ifLeft = { error ->
alertFactory.getSendTransactionErrorState(
error = error,
popBack = appRouter::pop,
onFailedTxEmailClick = ::onFailedTxEmailClick,
)
analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol))
},
ifRight = {
updateTransactionStatus(txData)
addTokenToWalletIfNeeded()
scheduleUpdates()
sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value)
},
)
}
private fun addTokenToWalletIfNeeded() {
if (cryptoCurrency !is CryptoCurrency.Token) return
val wallets = destinationUM?.wallets ?: return
val receivingUserWallet = wallets
.firstOrNull { it.address == enteredDestination }
?: return
val userWalletId = receivingUserWallet.userWalletId ?: return
val network = receivingUserWallet.network ?: return
modelScope.launch {
addCryptoCurrenciesUseCase(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
network = network,
)
}
}
private suspend fun updateTransactionStatus(txData: TransactionData.Uncompiled) {
val txUrl = getExplorerTransactionUrlUseCase(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
).getOrElse { "" }
_uiState.update(SendConfirmSentStateTransformer(txData, txUrl))
}
private fun scheduleUpdates() {
coroutineScope.launch {
listOf(
// we should update network to find pending tx after 1 sec
async {
fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network))
},
// we should update tx history and network for new balance
async {
updateTxHistory()
},
async {
updateDelayedCurrencyStatusUseCase(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
delayMillis = BALANCE_UPDATE_DELAY,
refresh = true,
)
},
).awaitAll()
}
}
private suspend fun updateTxHistory() {
delay(BALANCE_UPDATE_DELAY)
val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
txHistoryItemsCountEither.onRight {
if (txHistoryFeatureToggles.isFeatureEnabled) {
txHistoryContentUpdateEmitter.triggerUpdate()
} else {
getTxHistoryItemsUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
refresh = true,
)
}
}
}
private fun subscribeOnCheckFeeResultUpdates() {
sendFeeCheckReloadTrigger.checkReloadResultFlow.onEach { isFeeResultSuccess ->
if (isFeeResultSuccess) {
sendIdleTimer = SystemClock.elapsedRealtime()
_uiState.update(SendConfirmSendingStateTransformer(isSending = true))
verifyAndSendTransaction()
} else {
_uiState.update(SendConfirmSendingStateTransformer(isSending = false))
}
}.launchIn(modelScope)
}
private fun updateConfirmNotifications() {
modelScope.launch {
notificationsUpdateTrigger.triggerUpdate(
data = NotificationData(
destinationAddress = enteredDestination.orEmpty(),
amountValue = enteredAmount.orZero(),
reduceAmountBy = reduceAmountBy.orZero(),
isIgnoreReduce = isIgnoreReduce,
fee = fee,
feeError = feeError,
),
)
}
_uiState.update {
it.copy(
confirmUM = SendConfirmationNotificationsTransformer(
feeUM = uiState.value.feeUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
).transform(uiState.value.confirmUM),
)
}
}
fun configConfirmNavigation() {
combine(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, _) ->
// todo
// val amountUM = state.amountUM as? AmountState.Data
// val confirmUM = state.confirmUM
// params.callback.onResult(
// state.copy(
// navigationUM = NavigationUM.Content(
// title = resourceReference(
// id = R.string.send_summary_title,
// formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name),
// ),
// subtitle = amountUM?.title,
// backIconRes = R.drawable.ic_close_24,
// backIconClick = {
// analyticsEventHandler.send(
// SendAnalyticEvents.CloseButtonClicked(
// source = SendScreenSource.Confirm,
// isFromSummary = true,
// isValid = confirmUM.isPrimaryButtonEnabled,
// ),
// )
// appRouter.pop()
// },
// primaryButton = ButtonsUM.PrimaryButtonUM(
// text = when (confirmUM) {
// is ConfirmUM.Success -> resourceReference(R.string.common_close)
// is ConfirmUM.Content -> if (confirmUM.isSending) {
// resourceReference(R.string.send_sending)
// } else {
// resourceReference(R.string.common_send)
// }
// else -> resourceReference(R.string.common_send)
// },
// iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content },
// isEnabled = confirmUM.isPrimaryButtonEnabled,
// isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending,
// onClick = {
// when (confirmUM) {
// is ConfirmUM.Success -> appRouter.pop()
// is ConfirmUM.Content -> if (confirmUM.isSending) {
// return@PrimaryButtonUM
// } else {
// onSendClick()
// }
// else -> return@PrimaryButtonUM
// }
// },
// ),
// prevButton = null,
// secondaryPairButtonsUM = ButtonsUM.SecondaryPairButtonsUM(
// leftText = resourceReference(R.string.common_explore),
// leftIconResId = R.drawable.ic_web_24,
// onLeftClick = ::onExploreClick,
// rightText = resourceReference(R.string.common_share),
// rightIconResId = R.drawable.ic_share_24,
// onRightClick = ::onShareClick,
// ).takeIf { confirmUM is ConfirmUM.Success },
// ),
// ),
// )
}.launchIn(modelScope)
}
private companion object {
const val CHECK_FEE_UPDATE_DELAY = 10_000L
const val BALANCE_UPDATE_DELAY = 11_000L
}
}

View file

@ -0,0 +1,137 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
internal class SendConfirmInitialStateTransformer(
private val appCurrency: AppCurrency,
private val feeUM: FeeUM,
private val amountUM: AmountState,
private val isShowTapHelp: Boolean,
private val isSubtracted: Boolean,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
return ConfirmUM.Content(
isSending = false,
showTapHelp = isShowTapHelp,
sendingFooter = getSendingFooterText(),
notifications = persistentListOf(),
)
}
private fun getSendingFooterText(): TextReference {
val feeUM = feeUM as? FeeUM.Content
val amountUM = amountUM as? AmountState.Data
if (feeUM == null || amountUM == null) return TextReference.EMPTY
val fee = (feeUM.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee
val fiatAmountValue = amountUM.amountTextField.fiatAmount.value
val fiatFeeValue = feeUM.rate?.let { fee?.amount?.value?.multiply(it) }
val fiatSendingValue = if (isSubtracted) {
fiatAmountValue
} else {
if (feeUM.isFeeConvertibleToFiat) {
fiatFeeValue?.let { fiatAmountValue?.plus(it) }
} else {
fiatAmountValue
}
}
val fiatSending = fiatSendingValue.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
val fiatFee = formatFiatFee(
amount = fee?.amount,
isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat,
isFeeApproximate = feeUM.isFeeApproximate,
)
return if (feeUM.isTronToken && fee is Fee.Tron) {
getTokenFeeSendingText(
fee = fee,
fiatFee = fiatFee,
fiatSending = fiatSending,
)
} else {
resourceReference(
id = if (feeUM.isFeeConvertibleToFiat) {
R.string.send_summary_transaction_description
} else {
R.string.send_summary_transaction_description_no_fiat_fee
},
formatArgs = wrappedList(fiatSending, fiatFee),
)
}
}
private fun formatFiatFee(amount: Amount?, isFeeConvertibleToFiat: Boolean, isFeeApproximate: Boolean): String {
return if (isFeeConvertibleToFiat) {
amount?.value.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
} else {
amount?.value.format {
crypto(
decimals = amount?.decimals ?: 0,
symbol = amount?.currencySymbol.orEmpty(),
).fee(
canBeLower = isFeeApproximate,
)
}
}
}
private fun getTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: String): TextReference {
val suffix = when {
fee.remainingEnergy == 0L -> {
resourceReference(
R.string.send_summary_transaction_description_suffix_including,
wrappedList(fiatFee),
)
}
fee.feeEnergy <= fee.remainingEnergy -> {
resourceReference(
R.string.send_summary_transaction_description_suffix_fee_covered,
wrappedList(fee.feeEnergy),
)
}
else -> {
resourceReference(
R.string.send_summary_transaction_description_suffix_fee_reduced,
wrappedList(fee.remainingEnergy),
)
}
}
val prefix = resourceReference(
R.string.send_summary_transaction_description_prefix,
wrappedList(fiatSending),
)
return combinedReference(prefix, COMMA_SEPARATOR, suffix)
}
companion object {
private val COMMA_SEPARATOR = stringReference(", ")
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.utils.transformer.Transformer
internal class SendConfirmSendingStateTransformer(
val isSending: Boolean,
) : Transformer<SendUM> {
override fun transform(prevState: SendUM): SendUM {
val confirmUM = prevState.confirmUM as? ConfirmUM.Content ?: return prevState
return prevState.copy(
confirmUM = confirmUM.copy(
isPrimaryButtonEnabled = !isSending,
isSending = isSending,
),
)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.blockchain.common.TransactionData
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.utils.transformer.Transformer
internal class SendConfirmSentStateTransformer(
val txData: TransactionData.Uncompiled,
val txUrl: String,
) : Transformer<SendUM> {
override fun transform(prevState: SendUM): SendUM {
return prevState.copy(
confirmUM = ConfirmUM.Success(
transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(),
txUrl = txUrl,
),
)
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class SendConfirmationNotificationsTransformer(
private val feeUM: FeeUM,
private val analyticsEventHandler: AnalyticsEventHandler,
private val cryptoCurrency: CryptoCurrency,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
val state = prevState as? ConfirmUM.Content ?: return prevState
val feeUM = feeUM as? FeeUM.Content ?: return prevState
return state.copy(
notifications = buildList {
addTooHighNotification(feeUM.feeSelectorUM)
addTooLowNotification(feeUM)
}.toPersistentList(),
)
}
private fun MutableList<NotificationUM>.addTooLowNotification(feeUM: FeeUM.Content) {
val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return
val minimumValue = multipleFees.minimum.amount.value ?: return
val customAmount = feeSelectorUM.customValues.firstOrNull() ?: return
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) {
add(NotificationUM.Warning.FeeTooLow)
analyticsEventHandler.send(
SendAnalyticEvents.NoticeTransactionDelays(cryptoCurrency.symbol),
)
}
}
private fun MutableList<NotificationUM>.addTooHighNotification(feeSelectorUM: FeeSelectorUM) {
if (feeSelectorUM !is FeeSelectorUM.Content) return
val (isFeeTooHigh, diff) = checkIfFeeTooHigh(feeSelectorUM)
if (isFeeTooHigh) {
add(NotificationUM.Warning.TooHigh(diff))
}
}
}

View file

@ -0,0 +1,194 @@
package com.tangem.features.send.v2.send.confirm.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.features.send.v2.impl.R
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.SendAmountBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
import com.tangem.features.send.v2.subcomponents.notifications
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay
private const val TAP_HELP_KEY = "TAP_HELP_KEY"
private const val BLOCKS_KEY = "BLOCKS_KEY"
private const val TAP_HELP_ANIMATION_DELAY = 500L
@Suppress("LongParameterList")
@Composable
internal fun SendConfirmContent(
sendUM: SendUM,
// destinationBlockComponent: SendDestinationBlockComponent,
amountBlockComponent: SendAmountBlockComponent,
feeBlockComponent: SendFeeBlockComponent,
notificationsComponent: NotificationsComponent,
notificationsUM: ImmutableList<NotificationUM>,
) {
val confirmUM = sendUM.confirmUM as? ConfirmUM.Content
Column {
LazyColumn(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
blocks(
uiState = sendUM,
// destinationBlockComponent = destinationBlockComponent,
amountBlockComponent = amountBlockComponent,
feeBlockComponent = feeBlockComponent,
)
if (confirmUM != null) {
tapHelp(isDisplay = confirmUM.showTapHelp)
with(notificationsComponent) {
content(
state = notificationsUM,
isClickDisabled = confirmUM.isSending,
)
}
notifications(
notifications = confirmUM.notifications,
isClickDisabled = confirmUM.isSending,
)
}
}
SendingText(confirmUM = confirmUM)
}
}
@Composable
private fun SendingText(confirmUM: ConfirmUM.Content?, modifier: Modifier = Modifier) {
var isVisibleProxy by remember { mutableStateOf(confirmUM != null) }
val keyboard by keyboardAsState()
// the text should appear when the keyboard is closed
LaunchedEffect(confirmUM != null, keyboard) {
if (confirmUM != null && keyboard is Keyboard.Opened) {
return@LaunchedEffect
}
isVisibleProxy = confirmUM != null
}
AnimatedVisibility(
visible = isVisibleProxy,
modifier = modifier,
enter = slideInVertically() + fadeIn(),
exit = fadeOut(tween(durationMillis = 300)),
label = "Animate show sending state text",
) {
val wrappedConfirmUM = remember(this) { requireNotNull(confirmUM?.sendingFooter) }
Text(
text = wrappedConfirmUM.resolveAnnotatedReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
)
}
}
private fun LazyListScope.blocks(
uiState: SendUM,
// destinationBlockComponent: SendDestinationBlockComponent,
amountBlockComponent: SendAmountBlockComponent,
feeBlockComponent: SendFeeBlockComponent,
) {
item(key = BLOCKS_KEY) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
AnimatedVisibility(
visible = uiState.confirmUM is ConfirmUM.Success,
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
) {
val wrappedConfirmUM = remember(this) { uiState.confirmUM as ConfirmUM.Success }
TransactionDoneTitle(
title = resourceReference(R.string.sent_transaction_sent_title),
subtitle = resourceReference(
R.string.send_date_format,
wrappedList(
wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter),
wrappedConfirmUM.transactionDate.toTimeFormat(),
),
),
modifier = Modifier.padding(vertical = 12.dp),
)
}
// todo
// destinationBlockComponent.Content(modifier = Modifier)
amountBlockComponent.Content(modifier = Modifier)
feeBlockComponent.Content(modifier = Modifier)
}
}
}
private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) {
item(key = TAP_HELP_KEY) {
var wrappedIsDisplay by remember { mutableStateOf(false) }
LaunchedEffect(key1 = isDisplay) {
delay(TAP_HELP_ANIMATION_DELAY)
wrappedIsDisplay = isDisplay
}
if (wrappedIsDisplay) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxWidth()
.animateItem()
.padding(top = TangemTheme.dimens.spacing20),
) {
val background = TangemTheme.colors.button.secondary
Icon(
painter = painterResource(id = R.drawable.ic_send_hint_shape_12),
tint = TangemTheme.colors.button.secondary,
contentDescription = null,
modifier = Modifier,
)
Text(
text = stringResourceSafe(id = R.string.send_summary_tap_hint),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(background)
.padding(
horizontal = TangemTheme.dimens.spacing14,
vertical = TangemTheme.dimens.spacing12,
),
)
}
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.features.send.v2.send.confirm.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class ConfirmUM {
abstract val isPrimaryButtonEnabled: Boolean
data class Content(
override val isPrimaryButtonEnabled: Boolean = false,
val isSending: Boolean,
val showTapHelp: Boolean,
val sendingFooter: TextReference,
val notifications: ImmutableList<NotificationUM>,
) : ConfirmUM()
data class Success(
val transactionDate: Long,
val txUrl: String,
) : ConfirmUM() {
override val isPrimaryButtonEnabled: Boolean = true
}
data object Empty : ConfirmUM() {
override val isPrimaryButtonEnabled: Boolean = false
}
}

View file

@ -7,6 +7,8 @@ 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.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
@ -28,7 +30,8 @@ internal class SendModel @Inject constructor(
) : Model(),
SendDestinationComponent.ModelCallback,
SendAmountComponent.ModelCallback,
SendFeeComponent.ModelCallback {
SendFeeComponent.ModelCallback,
SendConfirmComponent.ModelCallback {
private val _uiState = MutableStateFlow(initialState())
val uiState = _uiState.asStateFlow()
@ -39,6 +42,10 @@ internal class SendModel @Inject constructor(
var appCurrency: AppCurrency = AppCurrency.Default
var predefinedAmountValue: String? = null
override fun onResult(sendUM: SendUM) {
_uiState.value = sendUM
}
override fun onDestinationResult(destinationUM: DestinationUM) {
_uiState.update { it.copy(destinationUM = destinationUM) }
}
@ -55,5 +62,6 @@ internal class SendModel @Inject constructor(
amountUM = AmountState.Empty(),
destinationUM = DestinationUM.Empty(),
feeUM = FeeUM.Empty(),
confirmUM = ConfirmUM.Empty,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.send.v2.send.ui.state
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
@ -8,4 +9,5 @@ internal data class SendUM(
val amountUM: AmountState,
val destinationUM: DestinationUM,
val feeUM: FeeUM,
val confirmUM: ConfirmUM,
)

View file

@ -25,6 +25,6 @@ internal class SendFeeComponent(
}
interface ModelCallback {
fun onFeeResult(state: FeeUM)
fun onFeeResult(feeUM: FeeUM)
}
}

View file

@ -55,7 +55,7 @@ import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
class NotificationsModel @Inject constructor(
internal class NotificationsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val appRouter: AppRouter,

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="7dp"
android:viewportWidth="12"
android:viewportHeight="7">
<path
android:fillColor="#EBEBEB"
android:pathData="M4.988,0.785C3.819,3.428 1.89,7 0,7H12C10.115,7 8.269,3.447 7.157,0.806C6.784,-0.08 5.376,-0.094 4.988,0.785Z" />
</vector>