Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-02 11:54:10 +05:00
parent 95f764c878
commit 9e98ebe77b
10 changed files with 370 additions and 73 deletions

View file

@ -39,8 +39,12 @@ internal class DefaultSendComponent @AssistedInject constructor(
popCallback = { onChildBack() }, popCallback = { onChildBack() },
) )
private val initialRoute = SendRoute.Empty private val initialRoute = if (params.amount == null) {
private val currentRoute = MutableStateFlow<SendRoute>(initialRoute) SendRoute.Destination(isEditMode = false)
} else {
SendRoute.Empty
}
private val currentRoute = MutableStateFlow(initialRoute)
private val model: SendModel = getOrCreateModel(params = params, router = innerRouter) private val model: SendModel = getOrCreateModel(params = params, router = innerRouter)
@ -77,12 +81,12 @@ internal class DefaultSendComponent @AssistedInject constructor(
private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) = private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) =
SendDestinationComponent( SendDestinationComponent(
appComponentContext = factoryContext, appComponentContext = factoryContext,
params = SendDestinationComponent.Params( params = SendDestinationComponentParams.DestinationParams(
state = model.uiState.value.destinationUM, state = model.uiState.value.destinationUM,
currentRoute = currentRoute.filterIsInstance<SendRoute.Destination>(), currentRoute = currentRoute.filterIsInstance<SendRoute.Destination>(),
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
userWallet = model.userWallet, userWallet = model.userWallet,
cryptoCurrencyStatus = model.cryptoCurrencyStatus, cryptoCurrency = params.currency,
callback = model, callback = model,
isEditMode = route.isEditMode, isEditMode = route.isEditMode,
), ),

View file

@ -18,6 +18,8 @@ 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.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent 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.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent 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.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent
@ -33,22 +35,22 @@ internal class SendConfirmComponent(
private val blockClickEnableFlow = MutableStateFlow(false) private val blockClickEnableFlow = MutableStateFlow(false)
// private val destinationBlockComponent = private val destinationBlockComponent =
// SendDestinationBlockComponent( SendDestinationBlockComponent(
// appComponentContext = child("sendConfirmDestinationBlock"), appComponentContext = child("sendConfirmDestinationBlock"),
// params = SendDestinationComponentParams.DestinationBlockParams( params = SendDestinationComponentParams.DestinationBlockParams(
// state = model.uiState.value.destinationUM, state = model.uiState.value.destinationUM,
// analyticsCategoryName = params.analyticsCategoryName, analyticsCategoryName = params.analyticsCategoryName,
// userWallet = params.userWallet, userWallet = params.userWallet,
// cryptoCurrency = params.cryptoCurrencyStatus.currency, cryptoCurrency = params.cryptoCurrencyStatus.currency,
// blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
// isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
// predefinedAddressValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.address, predefinedAddressValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.address,
// predefinedMemoValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.tag, predefinedMemoValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.tag,
// ), ),
// onResult = model::onDestinationResult, onResult = model::onDestinationResult,
// onClick = model::showEditDestination, onClick = model::showEditDestination,
// ) )
private val amountBlockComponent = SendAmountBlockComponent( private val amountBlockComponent = SendAmountBlockComponent(
appComponentContext = child("sendConfirmAmountBlock"), appComponentContext = child("sendConfirmAmountBlock"),
@ -59,9 +61,8 @@ internal class SendConfirmComponent(
cryptoCurrencyStatus = params.cryptoCurrencyStatus, cryptoCurrencyStatus = params.cryptoCurrencyStatus,
appCurrency = params.appCurrency, appCurrency = params.appCurrency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
blockEditDisabledFlow = TODO(), isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
// isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, predefinedAmountValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.amount,
// predefinedAmountValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.amount,
), ),
onResult = model::onAmountResult, onResult = model::onAmountResult,
onClick = model::showEditAmount, onClick = model::showEditAmount,
@ -109,8 +110,7 @@ internal class SendConfirmComponent(
} }
fun updateState(state: SendUM) { fun updateState(state: SendUM) {
// todo destinationBlockComponent.updateState(state.destinationUM)
// destinationBlockComponent.updateState(state.destinationUM)
amountBlockComponent.updateState(state.amountUM) amountBlockComponent.updateState(state.amountUM)
feeBlockComponent.updateState(state.feeUM) feeBlockComponent.updateState(state.feeUM)
model.updateState(state) model.updateState(state)
@ -123,7 +123,7 @@ internal class SendConfirmComponent(
SendConfirmContent( SendConfirmContent(
sendUM = state, sendUM = state,
// destinationBlockComponent = destinationBlockComponent, destinationBlockComponent = destinationBlockComponent,
amountBlockComponent = amountBlockComponent, amountBlockComponent = amountBlockComponent,
feeBlockComponent = feeBlockComponent, feeBlockComponent = feeBlockComponent,
notificationsComponent = notificationsComponent, notificationsComponent = notificationsComponent,

View file

@ -36,6 +36,7 @@ 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.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.SendUM 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.SendAmountBlockComponent
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent 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
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent
@ -50,7 +51,7 @@ private const val TAP_HELP_ANIMATION_DELAY = 500L
@Composable @Composable
internal fun SendConfirmContent( internal fun SendConfirmContent(
sendUM: SendUM, sendUM: SendUM,
// destinationBlockComponent: SendDestinationBlockComponent, destinationBlockComponent: SendDestinationBlockComponent,
amountBlockComponent: SendAmountBlockComponent, amountBlockComponent: SendAmountBlockComponent,
feeBlockComponent: SendFeeBlockComponent, feeBlockComponent: SendFeeBlockComponent,
notificationsComponent: NotificationsComponent, notificationsComponent: NotificationsComponent,
@ -64,7 +65,7 @@ internal fun SendConfirmContent(
) { ) {
blocks( blocks(
uiState = sendUM, uiState = sendUM,
// destinationBlockComponent = destinationBlockComponent, destinationBlockComponent = destinationBlockComponent,
amountBlockComponent = amountBlockComponent, amountBlockComponent = amountBlockComponent,
feeBlockComponent = feeBlockComponent, feeBlockComponent = feeBlockComponent,
) )
@ -121,7 +122,7 @@ private fun SendingText(confirmUM: ConfirmUM.Content?, modifier: Modifier = Modi
private fun LazyListScope.blocks( private fun LazyListScope.blocks(
uiState: SendUM, uiState: SendUM,
// destinationBlockComponent: SendDestinationBlockComponent, destinationBlockComponent: SendDestinationBlockComponent,
amountBlockComponent: SendAmountBlockComponent, amountBlockComponent: SendAmountBlockComponent,
feeBlockComponent: SendFeeBlockComponent, feeBlockComponent: SendFeeBlockComponent,
) { ) {
@ -144,8 +145,7 @@ private fun LazyListScope.blocks(
modifier = Modifier.padding(vertical = 12.dp), modifier = Modifier.padding(vertical = 12.dp),
) )
} }
// todo destinationBlockComponent.Content(modifier = Modifier)
// destinationBlockComponent.Content(modifier = Modifier)
amountBlockComponent.Content(modifier = Modifier) amountBlockComponent.Content(modifier = Modifier)
feeBlockComponent.Content(modifier = Modifier) feeBlockComponent.Content(modifier = Modifier)
} }

View file

@ -0,0 +1,28 @@
package com.tangem.features.send.v2.subcomponents.destination
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.features.send.v2.impl.R
import javax.inject.Inject
@ModelScoped
internal class SendDestinationAlertFactory @Inject constructor(
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,
),
),
)
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.send.v2.subcomponents.destination
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.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
import com.tangem.features.send.v2.subcomponents.destination.ui.DestinationBlock
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
internal class SendDestinationBlockComponent(
appComponentContext: AppComponentContext,
private val params: SendDestinationComponentParams.DestinationBlockParams,
val onClick: () -> Unit,
val onResult: (DestinationUM) -> Unit,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SendDestinationModel = getOrCreateModel(params = params)
init {
model.uiState.onEach {
onResult(it)
}.launchIn(componentScope)
}
fun updateState(destinationUM: DestinationUM) = model.updateState(destinationUM)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle()
DestinationBlock(
destinationUM = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.isPredefinedValues,
onClick = onClick,
)
}
}

View file

@ -1,44 +1,34 @@
package com.tangem.features.send.v2.subcomponents.destination package com.tangem.features.send.v2.subcomponents.destination
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.common.SendNavigationModelCallback
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
import com.tangem.features.send.v2.subcomponents.destination.ui.SendDestinationContent import com.tangem.features.send.v2.subcomponents.destination.ui.SendDestinationContent
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import kotlinx.coroutines.flow.Flow
internal class SendDestinationComponent( internal class SendDestinationComponent(
appComponentContext: AppComponentContext, appComponentContext: AppComponentContext,
private val params: Params, private val params: SendDestinationComponentParams.DestinationParams,
) : ComposableContentComponent, AppComponentContext by appComponentContext { ) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SendDestinationModel = getOrCreateModel(params = params, router = router) private val model: SendDestinationModel = getOrCreateModel(params = params)
fun updateState(state: DestinationUM) = model.updateState(state) fun updateState(state: DestinationUM) = model.updateState(state)
@Composable @Composable
override fun Content(modifier: Modifier) { override fun Content(modifier: Modifier) {
val state = model.uiState.collectAsStateWithLifecycle() val state by model.uiState.collectAsStateWithLifecycle()
val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle()
SendDestinationContent(state = state.value, model, isBalanceHidden = false) SendDestinationContent(state = state, clickIntents = model, isBalanceHidden = isBalanceHidden)
} }
data class Params(
val state: DestinationUM,
val analyticsCategoryName: String,
val userWallet: UserWallet,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val callback: ModelCallback,
val currentRoute: Flow<SendRoute.Destination>,
val isEditMode: Boolean,
)
interface ModelCallback : SendNavigationModelCallback { interface ModelCallback : SendNavigationModelCallback {
fun onDestinationResult(destinationUM: DestinationUM) fun onDestinationResult(destinationUM: DestinationUM)

View file

@ -0,0 +1,39 @@
package com.tangem.features.send.v2.subcomponents.destination
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent.ModelCallback
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
internal sealed class SendDestinationComponentParams {
abstract val state: DestinationUM
abstract val analyticsCategoryName: String
abstract val userWallet: UserWallet
abstract val cryptoCurrency: CryptoCurrency
data class DestinationParams(
override val state: DestinationUM,
override val analyticsCategoryName: String,
override val userWallet: UserWallet,
override val cryptoCurrency: CryptoCurrency,
val isBalanceHidingFlow: StateFlow<Boolean>,
val currentRoute: Flow<SendRoute.Destination>,
val callback: ModelCallback,
val isEditMode: Boolean,
) : SendDestinationComponentParams()
data class DestinationBlockParams(
override val state: DestinationUM,
override val analyticsCategoryName: String,
override val userWallet: UserWallet,
override val cryptoCurrency: CryptoCurrency,
val blockClickEnableFlow: StateFlow<Boolean>,
val predefinedAddressValue: String?,
val predefinedMemoValue: String?,
val isPredefinedValues: Boolean,
) : SendDestinationComponentParams()
}

View file

@ -1,18 +1,32 @@
package com.tangem.features.send.v2.subcomponents.destination.model package com.tangem.features.send.v2.subcomponents.destination.model
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import arrow.core.Either
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.Router
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.core.ui.extensions.resourceReference
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.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetNetworkAddressesUseCase import com.tangem.domain.tokens.GetNetworkAddressesUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.IsUtxoConsolidationAvailableUseCase import com.tangem.domain.transaction.usecase.IsUtxoConsolidationAvailableUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
@ -22,7 +36,10 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.common.NavigationUM import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent 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.subcomponents.destination.SendDestinationAlertFactory
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource 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.analytics.SendDestinationAnalyticEvents
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.* import com.tangem.features.send.v2.subcomponents.destination.model.transformers.*
@ -31,6 +48,7 @@ import com.tangem.features.send.v2.subcomponents.destination.ui.state.Destinatio
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.coroutines.waitForDelay
import com.tangem.utils.transformer.update import com.tangem.utils.transformer.update
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
@ -38,10 +56,11 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlin.properties.Delegates
@Stable @Stable
@ModelScoped @ModelScoped
@Suppress("LongParameterList") @Suppress("LongParameterList", "LargeClass")
internal class SendDestinationModel @Inject constructor( internal class SendDestinationModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
@ -55,43 +74,57 @@ internal class SendDestinationModel @Inject constructor(
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase, private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val parseQrCodeUseCase: ParseQrCodeUseCase, private val parseQrCodeUseCase: ParseQrCodeUseCase,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val sendDestinationAlertFactory: SendDestinationAlertFactory,
) : Model(), SendDestinationClickIntents { ) : Model(), SendDestinationClickIntents {
private val params: SendDestinationComponent.Params = paramsContainer.require() private val params: SendDestinationComponentParams = paramsContainer.require()
private val _uiState = MutableStateFlow(params.state) private val _uiState = MutableStateFlow(params.state)
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
private val analyticsCategoryName = params.analyticsCategoryName private val analyticsCategoryName = params.analyticsCategoryName
private val userWallet = params.userWallet private val userWallet = params.userWallet
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus private val cryptoCurrency = params.cryptoCurrency
private val cryptoCurrency = cryptoCurrencyStatus.currency private var cryptoCurrencyStatus by Delegates.notNull<CryptoCurrencyStatus>()
private var validationJobHolder = JobHolder() private var validationJobHolder = JobHolder()
init { init {
configDestinationNavigation() configDestinationNavigation()
initialState() initialState()
getWalletsAndRecent() subscribeOnCurrencyStatusUpdates()
subscribeOnQRScannerResult() subscribeOnQRScannerResult()
} }
private fun initialState() { private fun initialState() {
if (uiState.value is DestinationUM.Empty) { if ((uiState.value as? DestinationUM.Content)?.isInitialized == false) {
_uiState.update( _uiState.update(
SendDestinationInitialStateTransformer( SendDestinationInitialStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrency = cryptoCurrency,
onAddressChange = ::onRecipientAddressValueChange, isInitialized = true,
onMemoChange = ::onRecipientMemoValueChange,
), ),
) )
val params = params as? SendDestinationComponentParams.DestinationBlockParams
if (params?.predefinedAddressValue != null && params.predefinedMemoValue != null) {
_uiState.update(
SendDestinationPredefinedStateTransformer(
address = params.predefinedAddressValue,
memo = params.predefinedMemoValue,
),
)
}
} }
} }
fun updateState(state: DestinationUM) { fun updateState(destinationUM: DestinationUM) {
if (state !is DestinationUM.Empty) { if (destinationUM is DestinationUM.Content && destinationUM.isInitialized) {
_uiState.value = state _uiState.value = destinationUM
} }
} }
@ -124,6 +157,69 @@ internal class SendDestinationModel @Inject constructor(
) )
} }
private fun subscribeOnCurrencyStatusUpdates() {
val isSingleWalletWithToken = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
val isMultiCurrency = userWallet.isMultiCurrency
getCurrenciesStatusUpdates(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
)
}
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) {
getCurrencyStatus(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
).onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold(
ifRight = { cryptoStatus ->
cryptoCurrencyStatus = cryptoStatus
getWalletsAndRecent()
},
ifLeft = {
sendDestinationAlertFactory.getGenericErrorState {
onFailedTxEmailClick(it.toString())
}
},
)
}.launchIn(modelScope)
}
private fun getCurrencyStatus(
isSingleWalletWithToken: Boolean,
isMultiCurrency: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return if (isMultiCurrency) {
getCurrencyStatusUpdatesUseCase(
userWalletId = userWallet.walletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = isSingleWalletWithToken,
)
} else {
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWallet.walletId)
}
}
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 subscribeOnQRScannerResult() { private fun subscribeOnQRScannerResult() {
listenToQrScanningUseCase(SourceType.SEND) listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() } .getOrElse { emptyFlow() }
@ -146,13 +242,15 @@ internal class SendDestinationModel @Inject constructor(
private fun getWalletsAndRecent() { private fun getWalletsAndRecent() {
combine( combine(
flow = getWalletsUseCase().conflate().map { flow = getWalletsUseCase().conflate().map {
it.toAvailableWallets() waitForDelay(RECENT_LOAD_DELAY) { it.toAvailableWallets() }
}, },
flow2 = getFixedTxHistoryItemsUseCase( flow2 = getFixedTxHistoryItemsUseCase(
userWalletId = userWallet.walletId, userWalletId = userWallet.walletId,
currency = cryptoCurrency, currency = cryptoCurrency,
pageSize = RECENT_TX_SIZE, pageSize = RECENT_TX_SIZE,
).getOrElse { flowOf(emptyList()) }.conflate(), ).getOrElse { flowOf(emptyList()) }.map {
waitForDelay(RECENT_LOAD_DELAY) { it }
}.conflate(),
) { destinationWalletList, txHistoryList -> ) { destinationWalletList, txHistoryList ->
val isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync( val isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync(
userWalletId = userWallet.walletId, userWalletId = userWallet.walletId,
@ -240,12 +338,13 @@ internal class SendDestinationModel @Inject constructor(
} }
private fun saveResult() { private fun saveResult() {
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
params.callback.onDestinationResult(uiState.value) params.callback.onDestinationResult(uiState.value)
} }
private fun onNextClick() { private fun onNextClick() {
saveResult() saveResult()
if (params.isEditMode) { if ((params as? SendDestinationComponentParams.DestinationParams)?.isEditMode == true) {
router.pop() router.pop()
} else { } else {
router.push(SendRoute.Amount(isEditMode = false)) router.push(SendRoute.Amount(isEditMode = false))
@ -253,6 +352,7 @@ internal class SendDestinationModel @Inject constructor(
} }
private fun configDestinationNavigation() { private fun configDestinationNavigation() {
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
combine( combine(
flow = uiState, flow = uiState,
flow2 = params.currentRoute, flow2 = params.currentRoute,
@ -301,5 +401,6 @@ internal class SendDestinationModel @Inject constructor(
private companion object { private companion object {
const val RECENT_TX_SIZE = 100 const val RECENT_TX_SIZE = 100
const val RECENT_LOAD_DELAY = 500L
} }
} }

View file

@ -4,30 +4,28 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Network
import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM 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.destination.ui.state.DestinationUM
import com.tangem.utils.transformer.Transformer import com.tangem.utils.transformer.Transformer
internal class SendDestinationInitialStateTransformer( internal class SendDestinationInitialStateTransformer(
val cryptoCurrencyStatus: CryptoCurrencyStatus, val cryptoCurrency: CryptoCurrency,
val onAddressChange: (String, EnterAddressSource) -> Unit, val isInitialized: Boolean = false,
val onMemoChange: (String, Boolean) -> Unit,
) : Transformer<DestinationUM> { ) : Transformer<DestinationUM> {
override fun transform(prevState: DestinationUM): DestinationUM { override fun transform(prevState: DestinationUM): DestinationUM {
val memoType = when (cryptoCurrencyStatus.currency.network.transactionExtrasType) { val memoType = when (cryptoCurrency.network.transactionExtrasType) {
Network.TransactionExtrasType.NONE -> null Network.TransactionExtrasType.NONE -> null
Network.TransactionExtrasType.MEMO -> R.string.send_extras_hint_memo Network.TransactionExtrasType.MEMO -> R.string.send_extras_hint_memo
Network.TransactionExtrasType.DESTINATION_TAG -> R.string.send_destination_tag_field Network.TransactionExtrasType.DESTINATION_TAG -> R.string.send_destination_tag_field
} }
return DestinationUM.Content( return DestinationUM.Content(
isPrimaryButtonEnabled = false, isPrimaryButtonEnabled = false,
isInitialized = isInitialized,
addressTextField = DestinationTextFieldUM.RecipientAddress( addressTextField = DestinationTextFieldUM.RecipientAddress(
value = "", value = "",
onValueChange = { onAddressChange(it, EnterAddressSource.InputField) },
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next, imeAction = ImeAction.Next,
keyboardType = KeyboardType.Text, keyboardType = KeyboardType.Text,
@ -40,7 +38,6 @@ internal class SendDestinationInitialStateTransformer(
memoTextField = memoType?.let { memoTextField = memoType?.let {
DestinationTextFieldUM.RecipientMemo( DestinationTextFieldUM.RecipientMemo(
value = "", value = "",
onValueChange = { onMemoChange(it, false) },
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done, imeAction = ImeAction.Done,
keyboardType = KeyboardType.Text, keyboardType = KeyboardType.Text,
@ -55,7 +52,7 @@ internal class SendDestinationInitialStateTransformer(
}, },
wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT), wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT),
recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT), recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT),
networkName = cryptoCurrencyStatus.currency.network.name, networkName = cryptoCurrency.network.name,
isValidating = false, isValidating = false,
) )
} }

View file

@ -0,0 +1,93 @@
package com.tangem.features.send.v2.subcomponents.destination.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
@Composable
internal fun DestinationBlock(
destinationUM: DestinationUM,
isClickDisabled: Boolean,
isEditingDisabled: Boolean,
onClick: () -> Unit,
) {
val destinationUM = destinationUM as? DestinationUM.Content ?: return
val backgroundColor = if (isEditingDisabled) {
TangemTheme.colors.button.disabled
} else {
TangemTheme.colors.background.action
}
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(backgroundColor)
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing12),
) {
AddressBlock(destinationUM.addressTextField)
MemoBlock(destinationUM.memoTextField)
}
}
@Composable
private fun AddressBlock(address: DestinationTextFieldUM.RecipientAddress) {
Text(
text = address.label.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
) {
IdentIcon(
address = address.value,
modifier = Modifier
.size(TangemTheme.dimens.size36)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.background(TangemTheme.colors.background.tertiary),
)
Text(
text = address.value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
}
}
@Composable
private fun MemoBlock(memo: DestinationTextFieldUM.RecipientMemo?) {
if (memo != null && memo.value.isNotBlank()) {
HorizontalDivider(
color = TangemTheme.colors.icon.inactive,
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
)
Text(
text = memo.label.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = memo.value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
}