Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-15 14:25:11 +03:00
commit 5ee5b4f61b
563 changed files with 13917 additions and 4692 deletions

View file

@ -175,6 +175,8 @@ internal class DefaultSendComponent @AssistedInject constructor(
userWalletId = params.userWalletId,
cryptoCurrency = params.currency,
cryptoCurrencyStatusFlow = model.cryptoCurrencyStatusFlow,
accountFlow = model.accountFlow,
isAccountModeFlow = model.isAccountModeFlow,
callback = model,
predefinedValues = model.predefinedValues,
analyticsSendSource = model.analyticsSendSource,
@ -201,6 +203,8 @@ internal class DefaultSendComponent @AssistedInject constructor(
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
cryptoCurrencyStatusFlow = model.cryptoCurrencyStatusFlow,
feeCryptoCurrencyStatusFlow = model.feeCryptoCurrencyStatusFlow,
accountFlow = model.accountFlow,
isAccountModeFlow = model.isAccountModeFlow,
appCurrency = model.appCurrency,
callback = model,
predefinedValues = model.predefinedValues,

View file

@ -11,6 +11,7 @@ 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.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.error.GetFeeError
@ -73,6 +74,8 @@ internal class SendConfirmComponent(
cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow,
isBalanceHidingFlow = params.isBalanceHidingFlow,
analyticsSendSource = params.analyticsSendSource,
accountFlow = params.accountFlow,
isAccountModeFlow = params.isAccountModeFlow,
),
onResult = model::onAmountResult,
onClick = model::showEditAmount,
@ -152,6 +155,8 @@ internal class SendConfirmComponent(
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
val feeCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
val accountFlow: StateFlow<Account.CryptoPortfolio?>,
val isAccountModeFlow: StateFlow<Boolean>,
val appCurrency: AppCurrency,
val callback: ModelCallback,
val currentRoute: Flow<CommonSendRoute>,

View file

@ -14,6 +14,9 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseBigDecimalOrNull
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -23,6 +26,7 @@ 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.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
@ -91,8 +95,11 @@ internal class SendModel @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val sendAmountUpdateTrigger: SendAmountUpdateTrigger,
private val analyticsEventHandler: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : Model(), SendComponentCallback {
private val params: SendComponent.Params = paramsContainer.require()
@ -115,21 +122,26 @@ internal class SendModel @Inject constructor(
val currentRoute = MutableStateFlow(initialRoute)
private val _cryptoCurrencyStatusFlow = MutableStateFlow(
CryptoCurrencyStatus(
params.currency,
value = CryptoCurrencyStatus.Loading,
),
)
val cryptoCurrencyStatusFlow = _cryptoCurrencyStatusFlow.asStateFlow()
val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
field = MutableStateFlow(
CryptoCurrencyStatus(
params.currency,
value = CryptoCurrencyStatus.Loading,
),
)
private val _feeCryptoCurrencyStatusFlow = MutableStateFlow(
CryptoCurrencyStatus(
params.currency,
value = CryptoCurrencyStatus.Loading,
),
)
val feeCryptoCurrencyStatusFlow = _feeCryptoCurrencyStatusFlow.asStateFlow()
val feeCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
field = MutableStateFlow(
CryptoCurrencyStatus(
params.currency,
value = CryptoCurrencyStatus.Loading,
),
)
val accountFlow: StateFlow<Account.CryptoPortfolio?>
field = MutableStateFlow(null)
val isAccountModeFlow: StateFlow<Boolean>
field = MutableStateFlow(false)
var userWallet: UserWallet by Delegates.notNull()
var appCurrency: AppCurrency = AppCurrency.Default
@ -317,13 +329,34 @@ internal class SendModel @Inject constructor(
ifRight = { wallet ->
userWallet = wallet
val isSingleWalletWithToken = wallet is UserWallet.Cold &&
wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
val isMultiCurrency = wallet.isMultiCurrency
getCurrenciesStatusUpdates(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
)
if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase(
userWalletId = params.userWalletId,
currency = cryptoCurrency,
).onEach { (account, cryptoCurrencyStatus) ->
cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus
feeCryptoCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = params.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull() ?: cryptoCurrencyStatus
isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync()
accountFlow.value = account
if (params.amount != null) {
router.replaceAll(Confirm)
}
}.flowOn(dispatchers.default)
.launchIn(modelScope)
} else {
val isSingleWalletWithToken = wallet is UserWallet.Cold &&
wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
val isMultiCurrency = wallet.isMultiCurrency
getCurrenciesStatusUpdates(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
)
}
},
ifLeft = {
Timber.w(it.toString())
@ -352,10 +385,12 @@ internal class SendModel @Inject constructor(
).onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold(
ifRight = { cryptoCurrencyStatus ->
onDataLoaded(
currencyStatus = cryptoCurrencyStatus,
feeCurrencyStatus = getFeeCurrencyStatus(cryptoCurrencyStatus, isMultiCurrency),
)
cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus
feeCryptoCurrencyStatusFlow.value = getFeeCurrencyStatus(cryptoCurrencyStatus, isMultiCurrency)
if (params.amount != null) {
router.replaceAll(CommonSendRoute.Confirm)
}
},
ifLeft = {
sendConfirmAlertFactory.getGenericErrorState(
@ -366,7 +401,8 @@ internal class SendModel @Inject constructor(
)
},
)
}.launchIn(modelScope)
}.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun getCurrencyStatus(
@ -402,15 +438,6 @@ internal class SendModel @Inject constructor(
}
}
private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, feeCurrencyStatus: CryptoCurrencyStatus) {
_cryptoCurrencyStatusFlow.value = currencyStatus
_feeCryptoCurrencyStatusFlow.value = feeCurrencyStatus
if (params.amount != null) {
router.replaceAll(CommonSendRoute.Confirm)
}
}
private fun subscribeOnQRScannerResult() {
listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() }
@ -454,7 +481,7 @@ internal class SendModel @Inject constructor(
}
private fun initialState(): SendUM = SendUM(
amountUM = AmountState.Empty(isRedesignEnabled = true),
amountUM = AmountState.Empty,
destinationUM = SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency,
).transform(DestinationUM.Empty()),

View file

@ -156,6 +156,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
isBalanceHidingFlow = model.isBalanceHiddenFlow,
onLoadFee = model::loadFee,
analyticsSendSource = analyticsSendSource,
account = model.account,
isAccountsMode = model.isAccountsMode,
onSendTransaction = { innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) },
),
)
@ -181,6 +183,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
callback = model,
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.ConfirmSuccess>(),
txUrl = txUrl,
account = model.account,
isAccountsMode = model.isAccountsMode,
),
)
}

View file

@ -13,6 +13,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.nft.models.NFTAsset
@ -88,7 +89,9 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
nftAsset = params.nftAsset,
nftCollectionName = params.nftCollectionName,
isSuccessScreen = false,
title = resourceReference(R.string.send_from_wallet_name, wrappedList(params.userWallet.name)),
account = params.account,
isAccountsMode = params.isAccountsMode,
walletTitle = resourceReference(R.string.send_from_wallet_name, wrappedList(params.userWallet.name)),
),
)
@ -150,6 +153,8 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
val nftCollectionName: String,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
val account: Account.CryptoPortfolio?,
val isAccountsMode: Boolean,
val callback: ModelCallback,
val currentRoute: Flow<CommonSendRoute.Confirm>,
val isBalanceHidingFlow: StateFlow<Boolean>,

View file

@ -11,6 +11,9 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.cardTypesResolver
@ -19,6 +22,7 @@ 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.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -71,6 +75,9 @@ internal class NFTSendModel @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val alertFactory: SendConfirmAlertFactory,
private val nftSendSuccessTrigger: NFTSendSuccessTrigger,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback {
val params: NFTSendComponent.Params = paramsContainer.require()
@ -94,6 +101,9 @@ internal class NFTSendModel @Inject constructor(
var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var appCurrency: AppCurrency = AppCurrency.Default
var account: Account.CryptoPortfolio? = null
var isAccountsMode: Boolean = false
init {
subscribeOnCurrencyStatusUpdates()
initAppCurrency()
@ -176,10 +186,31 @@ internal class NFTSendModel @Inject constructor(
?.firstOrNull { it is CryptoCurrency.Coin && it.network == nftAsset.network }
?: return@launch
getCurrenciesStatusUpdates(
isSingleWalletWithToken = wallet is UserWallet.Cold &&
wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase(
userWalletId,
cryptoCurrency,
).onEach { (maybeAccount, cryptoStatus) ->
account = maybeAccount
isAccountsMode = isAccountsModeEnabledUseCase.invokeSync()
cryptoCurrencyStatus = cryptoStatus
feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoStatus,
).getOrNull() ?: cryptoStatus
if (uiState.value.destinationUM is DestinationUM.Empty) {
router.replaceAll(Destination(isEditMode = false))
}
}.flowOn(dispatchers.default)
.launchIn(modelScope)
} else {
getCurrenciesStatusUpdates(
isSingleWalletWithToken = wallet is UserWallet.Cold &&
wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
}
},
ifLeft = {
alertFactory.getGenericErrorState(::onFailedTxEmailClick)

View file

@ -10,6 +10,7 @@ import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.nft.models.NFTAsset
@ -46,7 +47,9 @@ internal class NFTSendSuccessComponent @AssistedInject constructor(
nftAsset = params.nftAsset,
nftCollectionName = params.nftCollectionName,
isSuccessScreen = true,
title = resourceReference(R.string.nft_asset),
account = params.account,
isAccountsMode = params.isAccountsMode,
walletTitle = resourceReference(R.string.nft_asset),
),
)
@ -86,6 +89,8 @@ internal class NFTSendSuccessComponent @AssistedInject constructor(
val nftAsset: NFTAsset,
val nftCollectionName: String,
val txUrl: String,
val account: Account.CryptoPortfolio?,
val isAccountsMode: Boolean,
val callback: ModelCallback,
)

View file

@ -26,12 +26,10 @@ internal class SendAmountComponent(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle()
val isSendWithSwapAvailable by model.isSendWithSwapAvailable.collectAsStateWithLifecycle()
SendAmountContent(
amountState = state,
isBalanceHidden = isBalanceHidden,
clickIntents = model,
isSendWithSwapAvailable = isSendWithSwapAvailable,
modifier = modifier,

View file

@ -2,6 +2,7 @@ package com.tangem.features.send.v2.subcomponents.amount
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -23,6 +24,8 @@ internal sealed class SendAmountComponentParams {
abstract val cryptoCurrency: CryptoCurrency
abstract val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
abstract val isBalanceHidingFlow: StateFlow<Boolean>
abstract val accountFlow: StateFlow<Account.CryptoPortfolio?>
abstract val isAccountModeFlow: StateFlow<Boolean>
data class AmountParams(
override val state: AmountState,
@ -34,6 +37,8 @@ internal sealed class SendAmountComponentParams {
override val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
override val isBalanceHidingFlow: StateFlow<Boolean>,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
override val accountFlow: StateFlow<Account.CryptoPortfolio?>,
override val isAccountModeFlow: StateFlow<Boolean>,
val callback: ModelCallback,
val currentRoute: StateFlow<CommonSendRoute>,
) : SendAmountComponentParams()
@ -48,6 +53,8 @@ internal sealed class SendAmountComponentParams {
override val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
override val isBalanceHidingFlow: StateFlow<Boolean>,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
override val accountFlow: StateFlow<Account.CryptoPortfolio?>,
override val isAccountModeFlow: StateFlow<Boolean>,
val userWallet: UserWallet,
val blockClickEnableFlow: StateFlow<Boolean>,
) : SendAmountComponentParams()

View file

@ -21,6 +21,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
@ -118,42 +119,7 @@ internal class SendAmountModel @Inject constructor(
private fun subscribeOnBalanceHiddenUpdates() {
params.isBalanceHidingFlow.onEach { isBalanceHidden ->
_uiState.update(
AmountBoundaryUpdateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxAmountBoundary,
appCurrency = appCurrency,
isBalanceHidden = params.isBalanceHidingFlow.value,
),
)
}.launchIn(modelScope)
}
private fun subscribeOnCryptoCurrencyStatusFlow() {
params.cryptoCurrencyStatusFlow
.onEach { newCryptoCurrencyStatus ->
cryptoCurrencyStatus = newCryptoCurrencyStatus
maxAmountBoundary = MaxEnterAmountConverter().convert(cryptoCurrencyStatus)
initMinBoundary()
}
.launchIn(modelScope)
}
private fun initMinBoundary() {
modelScope.launch {
minAmountBoundary = getMinimumTransactionAmountSyncUseCase(
userWalletId = params.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull()?.let {
EnterAmountBoundary(
amount = it,
fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(),
)
}
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
if (uiState.value is AmountState.Data) {
if (cryptoCurrencyStatus.value != CryptoCurrencyStatus.Loading) {
_uiState.update(
AmountBoundaryUpdateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
@ -162,33 +128,87 @@ internal class SendAmountModel @Inject constructor(
isBalanceHidden = params.isBalanceHidingFlow.value,
),
)
} else {
initialState()
}
}.launchIn(modelScope)
}
private fun subscribeOnCryptoCurrencyStatusFlow() {
combine(
flow = params.cryptoCurrencyStatusFlow.distinctUntilChanged { old, new ->
old.value.amount == new.value.amount
}, // Check only balance changes,
flow2 = params.accountFlow,
flow3 = params.isAccountModeFlow,
) { newCryptoCurrencyStatus, account, isAccountsMode ->
maxAmountBoundary = MaxEnterAmountConverter().convert(newCryptoCurrencyStatus)
cryptoCurrencyStatus = newCryptoCurrencyStatus
initMinBoundary(cryptoCurrencyStatus, account, isAccountsMode)
}.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private suspend fun initMinBoundary(
cryptoCurrencyStatus: CryptoCurrencyStatus,
account: Account.CryptoPortfolio?,
isAccountsMode: Boolean,
) {
minAmountBoundary = getMinimumTransactionAmountSyncUseCase(
userWalletId = params.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull()?.let {
EnterAmountBoundary(
amount = it,
fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(),
)
}
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
if (uiState.value is AmountState.Data) {
_uiState.update(
AmountBoundaryUpdateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxAmountBoundary,
appCurrency = appCurrency,
isBalanceHidden = params.isBalanceHidingFlow.value,
),
)
} else {
initialState(cryptoCurrencyStatus, account, isAccountsMode)
}
}
private fun initialState() {
private fun initialState(
cryptoCurrencyStatus: CryptoCurrencyStatus,
@Suppress("UnusedParameter") account: Account.CryptoPortfolio?,
@Suppress("UnusedParameter") isAccountsMode: Boolean,
) {
if (uiState.value is AmountState.Empty && userWallet != null) {
val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1
val walletTitle = if (isOnlyOneWallet) {
resourceReference(R.string.send_from_title)
} else {
resourceReference(
R.string.send_from_wallet_name,
WrappedList(listOf(userWallet?.name.orEmpty())), // TODO [REDACTED_TASK_KEY]
)
}
_uiState.update {
AmountStateConverterV2(
AmountStateConverter(
clickIntents = this,
appCurrency = appCurrency,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxAmountBoundary,
iconStateConverter = CryptoCurrencyToIconStateConverter(),
isBalanceHidden = params.isBalanceHidingFlow.value,
accountTitleUM = AmountAccountConverter(
isAccountsMode = isAccountsMode,
walletTitle = walletTitle,
prefixText = resourceReference(R.string.common_from),
).convert(account),
).convert(
AmountParameters(
title = if (isOnlyOneWallet) {
resourceReference(R.string.send_from_title)
} else {
resourceReference(
R.string.send_from_wallet_name,
WrappedList(listOf(userWallet?.name.orEmpty())), // TODO [REDACTED_TASK_KEY]
)
},
title = walletTitle,
value = "",
),
)

View file

@ -30,7 +30,6 @@ import com.tangem.features.send.v2.subcomponents.amount.ui.preview.SendAmountCli
@Composable
fun SendAmountContent(
amountState: AmountState,
isBalanceHidden: Boolean,
clickIntents: SendAmountClickIntents,
isSendWithSwapAvailable: Boolean,
modifier: Modifier = Modifier,
@ -38,7 +37,6 @@ fun SendAmountContent(
Column(modifier = modifier.background(TangemTheme.colors.background.tertiary)) {
AmountScreenContent(
amountState = amountState,
isBalanceHidden = isBalanceHidden,
clickIntents = clickIntents,
extraContent = {
SendConvertTokenButton(
@ -95,7 +93,6 @@ private fun SendAmountContent_Preview(@PreviewParameter(SendAmountContentPreview
TangemThemePreview {
SendAmountContent(
amountState = params,
isBalanceHidden = true,
clickIntents = SendAmountClickIntentsStub,
isSendWithSwapAvailable = true,
)
@ -106,6 +103,7 @@ private class SendAmountContentPreviewProvider : PreviewParameterProvider<Amount
override val values: Sequence<AmountState>
get() = sequenceOf(
AmountStatePreviewData.amountStateV2,
AmountStatePreviewData.amountStateV2Accounts,
)
}
// endregion

View file

@ -11,6 +11,9 @@ 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.extensions.resourceReference
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.network.CryptoCurrencyAddress
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isLocked
@ -64,7 +67,10 @@ internal class SendDestinationModel @Inject constructor(
private val isSelfSendAvailableUseCase: IsSelfSendAvailableUseCase,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val parseQrCodeUseCase: ParseQrCodeUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : Model(), SendDestinationClickIntents {
private val params: SendDestinationComponentParams = paramsContainer.require()
@ -142,7 +148,7 @@ internal class SendDestinationModel @Inject constructor(
)
}
fun saveResult() {
private fun saveResult() {
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
params.callback.onDestinationResult(uiState.value)
}
@ -193,12 +199,17 @@ internal class SendDestinationModel @Inject constructor(
).getOrElse { flowOf(emptyList()) }.map {
waitForDelay(RECENT_LOAD_DELAY) { it }
}.conflate(),
) { destinationWalletList, txHistoryList ->
flow3 = isAccountsModeEnabledUseCase().distinctUntilChanged(),
flow4 = if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase(userWalletId, cryptoCurrency).distinctUntilChanged()
} else {
flowOf(null)
},
) { destinationWalletList, txHistoryList, isAccountsMode, accountCurrencyStatus ->
val isSelfSendAvailable = isSelfSendAvailableUseCase.invokeSync(
userWalletId = userWalletId,
network = cryptoCurrency.network,
)
_uiState.update(
SendDestinationRecentListTransformer(
cryptoCurrency = cryptoCurrency,
@ -206,9 +217,11 @@ internal class SendDestinationModel @Inject constructor(
isSelfSendAvailable = isSelfSendAvailable,
destinationWalletList = destinationWalletList,
txHistoryList = txHistoryList,
account = accountCurrencyStatus?.account,
isAccountsMode = isAccountsMode,
),
)
}.launchIn(modelScope)
}.flowOn(dispatchers.default).launchIn(modelScope)
}
private suspend fun List<UserWallet>.toAvailableWallets(): List<DestinationWalletUM> {

View file

@ -54,6 +54,7 @@ internal class SendDestinationInitialStateTransformer(
isValuePasted = false,
)
},
accountTitleUM = null,
wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT),
recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT),
networkName = cryptoCurrency.network.name,

View file

@ -1,24 +1,42 @@
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.TxInfo
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientHistoryListConverter
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientWalletListConverter
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM
import com.tangem.utils.StringsSigns
import com.tangem.utils.transformer.Transformer
@Suppress("LongParameterList")
internal class SendDestinationRecentListTransformer(
private val senderAddress: String?,
private val cryptoCurrency: CryptoCurrency,
private val isSelfSendAvailable: Boolean,
private val destinationWalletList: List<DestinationWalletUM>,
private val txHistoryList: List<TxInfo>,
private val account: Account.CryptoPortfolio?,
private val isAccountsMode: Boolean,
) : Transformer<DestinationUM> {
override fun transform(prevState: DestinationUM): DestinationUM {
val state = prevState as? DestinationUM.Content ?: return prevState
return state.copy(
accountTitleUM = if (account != null && isAccountsMode) {
AccountTitleUM.Account(
name = account.accountName.toUM().value,
icon = account.icon.toUM(),
prefixText = stringReference(StringsSigns.DOT),
)
} else {
AccountTitleUM.Text(TextReference.EMPTY)
},
wallets = SendRecipientWalletListConverter(
senderAddress = senderAddress,
isSelfSendAvailable = isSelfSendAvailable,

View file

@ -144,6 +144,7 @@ private class DestinationBlockPreviewProvider : PreviewParameterProvider<Destina
isValidating = false,
isInitialized = true,
isRecentHidden = false,
accountTitleUM = null,
)
override val values: Sequence<DestinationUM.Content>

View file

@ -23,15 +23,22 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.features.send.v2.impl.R
import com.tangem.utils.StringsSigns
/**
* Row item with title and subtitle
@ -50,6 +57,7 @@ fun ListItemWithIcon(
subtitle: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
accountTitleUM: AccountTitleUM? = null,
info: String? = null,
subtitleEndOffset: Int = 0,
@DrawableRes subtitleIconRes: Int? = null,
@ -68,6 +76,7 @@ fun ListItemWithIcon(
subtitle = subtitle,
onClick = onClick,
info = info,
accountTitleUM = accountTitleUM,
subtitleEndOffset = subtitleEndOffset,
subtitleIconRes = subtitleIconRes,
modifier = modifier,
@ -80,6 +89,7 @@ fun ListItemWithIcon(
private fun ListItemWithIcon(
title: String,
subtitle: String,
accountTitleUM: AccountTitleUM?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
info: String? = null,
@ -115,7 +125,7 @@ private fun ListItemWithIcon(
ellipsis = TextEllipsis.Middle,
modifier = Modifier,
)
Row {
Row(verticalAlignment = Alignment.CenterVertically) {
if (subtitleIconRes != null) {
Icon(
painter = painterResource(id = subtitleIconRes),
@ -142,6 +152,13 @@ private fun ListItemWithIcon(
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(offsetEnd = offset),
)
if (accountTitleUM != null) {
AccountTitle(
accountTitleUM = accountTitleUM,
textStyle = TangemTheme.typography.caption2,
modifier = Modifier.padding(start = 4.dp),
)
}
}
}
}
@ -195,6 +212,7 @@ private fun ListItemWithIconPreview(
ListItemWithIcon(
title = config.title,
subtitle = config.subtitle,
accountTitleUM = config.accountTitleUM,
subtitleEndOffset = config.subtitleEndOffset,
subtitleIconRes = config.iconRes,
onClick = {},
@ -206,6 +224,7 @@ private fun ListItemWithIconPreview(
private data class ListItemWithIconPreviewConfig(
val title: String,
val subtitle: String,
val accountTitleUM: AccountTitleUM.Account? = null,
val info: String? = null,
val subtitleEndOffset: Int = 0,
val iconRes: Int? = null,
@ -240,6 +259,15 @@ private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvid
iconRes = R.drawable.ic_arrow_down_24,
isLoading = true,
),
ListItemWithIconPreviewConfig(
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
subtitle = "Wallet",
accountTitleUM = AccountTitleUM.Account(
name = AccountNameUM.DefaultMain.value,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
prefixText = stringReference(StringsSigns.DOT),
),
),
),
)
//endregion

View file

@ -21,7 +21,9 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowRecipient
import com.tangem.core.ui.extensions.*
@ -46,7 +48,6 @@ internal fun SendDestinationContent(
if (state !is DestinationUM.Content) return
val recipients = state.recent
val wallets = state.wallets
val memoField = state.memoTextField
val address = state.addressTextField
val isValidating by remember(state.isValidating) { derivedStateOf { state.isValidating } }
val isError by remember(address.isError) { derivedStateOf { address.isError } }
@ -64,18 +65,23 @@ internal fun SendDestinationContent(
onQrCodeClick = clickIntents::onQrCodeScanClick,
)
memoField(
memoField = memoField,
memoField = state.memoTextField,
onMemoChange = clickIntents::onRecipientMemoValueChange,
)
listHeaderItem(
titleRes = R.string.send_recipient_wallets_title,
titleRes = when (state.accountTitleUM) {
is AccountTitleUM.Account -> R.string.common_accounts
else -> R.string.send_recipient_wallets_title
},
isLoading = state.accountTitleUM == null,
isVisible = wallets.isNotEmpty() && wallets.first().isVisible && !state.isRecentHidden,
isFirst = true,
)
listItem(
list = wallets,
isLast = recipients.any { !it.isVisible },
isBalanceHidden = isBalanceHidden,
accountTitleUM = state.accountTitleUM,
isBalanceHidden = false,
isRecentHidden = state.isRecentHidden,
onClick = { title ->
clickIntents.onRecipientAddressValueChange(
@ -86,6 +92,7 @@ internal fun SendDestinationContent(
)
listHeaderItem(
titleRes = R.string.send_recent_transactions,
isLoading = state.accountTitleUM == null,
isVisible = recipients.isNotEmpty() && recipients.first().isVisible && !state.isRecentHidden,
isFirst = wallets.any { !it.isVisible },
)
@ -180,7 +187,12 @@ private fun LazyListScope.memoField(
}
}
private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Boolean, isFirst: Boolean) {
private fun LazyListScope.listHeaderItem(
@StringRes titleRes: Int,
isVisible: Boolean,
isLoading: Boolean,
isFirst: Boolean,
) {
item(key = titleRes) {
AnimateRecentAppearance(isVisible) {
val (topPadding, paddingFromTop) = if (isFirst) {
@ -189,10 +201,8 @@ private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Bo
0.dp to 8.dp
}
val topRadius = if (isFirst) 16.dp else 0.dp
Text(
text = stringResourceSafe(titleRes),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
AnimatedContent(
targetState = isLoading,
modifier = Modifier
.fillMaxWidth()
.padding(top = topPadding)
@ -209,7 +219,22 @@ private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Bo
start = 12.dp,
end = 12.dp,
),
)
) { currentIsLoading ->
if (currentIsLoading) {
Box {
TextShimmer(
style = TangemTheme.typography.subtitle2,
text = stringResourceSafe(titleRes),
)
}
} else {
Text(
text = stringResourceSafe(titleRes),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
}
}
@ -220,6 +245,7 @@ private fun LazyListScope.listItem(
isBalanceHidden: Boolean,
isRecentHidden: Boolean,
onClick: (String) -> Unit,
accountTitleUM: AccountTitleUM? = null,
) {
items(
count = list.size,
@ -233,6 +259,7 @@ private fun LazyListScope.listItem(
ListItemWithIcon(
title = title,
subtitle = item.subtitle.orMaskWithStars(isBalanceHidden).resolveReference(),
accountTitleUM = accountTitleUM,
info = item.timestamp?.resolveReference(),
subtitleEndOffset = item.subtitleEndOffset,
subtitleIconRes = item.subtitleIconRes,

View file

@ -201,16 +201,11 @@ class SendConfirmationNotificationsTransformerV2Test {
return AmountState.Data(
isPrimaryButtonEnabled = true,
isRedesignEnabled = false,
title = mockk(relaxed = true),
availableBalance = mockk(relaxed = true),
accountTitleUM = mockk(relaxed = true),
availableBalanceCrypto = mockk(relaxed = true),
availableBalanceFiat = mockk(relaxed = true),
tokenName = mockk(relaxed = true),
tokenIconState = mockk(relaxed = true),
segmentedButtonConfig = persistentListOf(),
selectedButton = 0,
isSegmentedButtonsEnabled = false,
amountTextField = AmountFieldModel(
value = "1.5",
onValueChange = {},