Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-27 16:12:21 +05:00
parent 0fae3c188c
commit f53a79e29c
9 changed files with 245 additions and 334 deletions

View file

@ -1,23 +1,14 @@
package com.tangem.features.send.impl.presentation.domain
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.PersistentList
@Immutable
internal sealed class SendRecipientListContent {
data class Item(
val id: String,
val title: TextReference,
val subtitle: TextReference,
val timestamp: TextReference? = null,
val subtitleEndOffset: Int = 0,
@DrawableRes val subtitleIconRes: Int? = null,
) : SendRecipientListContent()
data class Wallets(
val list: PersistentList<Item>,
val isWalletsOnly: Boolean,
) : SendRecipientListContent()
}
data class SendRecipientListContent(
val id: String,
val title: TextReference,
val subtitle: TextReference,
val timestamp: TextReference? = null,
val subtitleEndOffset: Int = 0,
@DrawableRes val subtitleIconRes: Int? = null,
val isVisible: Boolean = true,
)

View file

@ -1,6 +1,5 @@
package com.tangem.features.send.impl.presentation.state
import androidx.paging.PagingData
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
@ -23,7 +22,6 @@ import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import timber.log.Timber
@Suppress("LongParameterList")
@ -78,7 +76,6 @@ internal class SendStateFactory(
// region UI states
fun getInitialState(): SendUiState = SendUiState(
clickIntents = clickIntents,
currentState = MutableStateFlow(SendUiCurrentScreen(type = SendUiStateType.None, isFromConfirmation = false)),
event = consumedEvent(),
isEditingDisabled = false,
isBalanceHidden = false,
@ -109,17 +106,11 @@ internal class SendStateFactory(
//endregion
//region recipient
fun onLoadedRecipientList(
wallets: List<AvailableWallet?>,
txHistory: PagingData<TxHistoryItem>,
txHistoryCount: Int,
) {
fun onLoadedRecipientList(wallets: List<AvailableWallet?>, txHistory: List<TxHistoryItem>): SendUiState =
recipientListStateConverter.convert(
wallets = wallets,
txHistory = txHistory,
txHistoryCount = txHistoryCount,
)
}
fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState {
val state = currentStateProvider()

View file

@ -59,7 +59,8 @@ internal sealed class SendStates {
override val isPrimaryButtonEnabled: Boolean,
val addressTextField: SendTextField.RecipientAddress,
val memoTextField: SendTextField.RecipientMemo?,
val recipients: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
val recent: ImmutableList<SendRecipientListContent>,
val wallets: ImmutableList<SendRecipientListContent>,
val network: String,
val isValidating: Boolean = false,
) : SendStates()

View file

@ -1,6 +1,5 @@
package com.tangem.features.send.impl.presentation.state.recipient
import androidx.paging.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
@ -17,77 +16,66 @@ import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.utils.Provider
import com.tangem.utils.toFormattedCurrencyString
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.update
internal class SendRecipientListConverter(
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) {
fun convert(wallets: List<AvailableWallet?>, txHistory: PagingData<TxHistoryItem>, txHistoryCount: Int) {
val filteredWallets = wallets.filterNotNull()
.groupBy { item -> item.name }
.values.flatten()
.mapIndexed { index, item ->
item.copy(
name = "${item.name} ${index.inc()}",
)
}
val walletsItem = getWalletItems(filteredWallets, txHistoryCount)
fun convert(wallets: List<AvailableWallet?>, txHistory: List<TxHistoryItem>): SendUiState {
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
currentStateProvider().recipientList.update {
if (txHistoryCount == 0) {
PagingData.from(listOf(walletsItem))
} else {
txHistory.filter { item ->
val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer
val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User
val isSingleAddress = if (item.isOutgoing) {
item.destinationType is TxHistoryItem.DestinationType.Single
} else {
item.sourceType is TxHistoryItem.SourceType.Single
}
isTransfer && isSingleAddress && isNotContract
}.map<TxHistoryItem, SendRecipientListContent> { tx ->
SendRecipientListContent.Item(
id = tx.txHash,
title = tx.extractAddress(),
subtitle = stringReference(tx.getAmount(cryptoCurrency).trim()),
timestamp = tx.extractTimestamp(),
subtitleEndOffset = cryptoCurrency.symbol.length,
subtitleIconRes = tx.extractIconRes(),
)
}.insertWallets(walletsItem)
}
}
}
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
private fun PagingData<SendRecipientListContent>.insertWallets(
wallets: SendRecipientListContent.Wallets,
): PagingData<SendRecipientListContent> {
return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after ->
return@insertSeparators when {
before == null && after is SendRecipientListContent.Item -> wallets
else -> null
}
}
}
private fun getWalletItems(wallets: List<AvailableWallet>, txHistoryCount: Int): SendRecipientListContent.Wallets {
return SendRecipientListContent.Wallets(
wallets.map {
SendRecipientListContent.Item(
id = it.address,
title = TextReference.Str(it.address),
subtitle = TextReference.Str(it.name),
)
}.toPersistentList(),
isWalletsOnly = txHistoryCount == 0,
return state.copy(
recipientState = recipientState.copy(
wallets = wallets.filterWallets(),
recent = txHistory.filterRecipients(cryptoCurrency),
),
)
}
private fun List<AvailableWallet?>.filterWallets() = this.filterNotNull()
.groupBy { item -> item.name }
.values.map {
it.mapIndexed { index, item ->
val name = if (it.size > 1) {
"${item.name} ${index.inc()}"
} else {
item.name
}
SendRecipientListContent(
id = item.address,
title = TextReference.Str(item.address),
subtitle = TextReference.Str(name),
)
}
}
.flatten()
.toPersistentList()
private fun List<TxHistoryItem>.filterRecipients(cryptoCurrency: CryptoCurrency) = this.filter { item ->
val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer
val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User
val isSingleAddress = if (item.isOutgoing) {
item.destinationType is TxHistoryItem.DestinationType.Single
} else {
item.sourceType is TxHistoryItem.SourceType.Single
}
isTransfer && isSingleAddress && isNotContract
}
.take(RECENT_LIST_SIZE)
.map { tx ->
SendRecipientListContent(
id = tx.txHash,
title = tx.extractAddress(),
subtitle = stringReference(tx.getAmount(cryptoCurrency).trim()),
timestamp = tx.extractTimestamp(),
subtitleEndOffset = cryptoCurrency.symbol.length,
subtitleIconRes = tx.extractIconRes(),
)
}.toPersistentList()
private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) {
when (val destination = destinationType) {
is TxHistoryItem.DestinationType.Multiple -> TextReference.Res(
@ -122,4 +110,8 @@ internal class SendRecipientListConverter(
val time = timestampInMillis.toTimeFormat()
return TextReference.Res(R.string.send_date_format, wrappedList(date, time))
}
companion object {
private const val RECENT_LIST_SIZE = 10
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
internal class SendRecipientStateConverter(
private val clickIntents: SendClickIntents,
@ -25,6 +26,8 @@ internal class SendRecipientStateConverter(
memoTextField = memoFieldConverter.convertOrNull(),
network = cryptoCurrencyStatusProvider().currency.network.name,
isPrimaryButtonEnabled = false,
wallets = persistentListOf(),
recent = persistentListOf(),
)
}
}

View file

@ -82,7 +82,6 @@ private fun SendScreenContent(
currentState: State<SendUiCurrentScreen>,
modifier: Modifier = Modifier,
) {
val recipientList = uiState.recipientList.collectAsLazyPagingItems()
AnimatedContent(
targetState = currentState.value,
label = "Send Scree Navigation",
@ -97,7 +96,6 @@ private fun SendScreenContent(
SendUiStateType.Recipient -> SendRecipientContent(
uiState = uiState.recipientState,
clickIntents = uiState.clickIntents,
recipientList = recipientList,
)
SendUiStateType.Fee -> SendSpeedAndFeeContent(
state = uiState.feeState,

View file

@ -3,9 +3,7 @@ package com.tangem.features.send.impl.presentation.ui.recipient
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
@ -18,9 +16,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import androidx.constraintlayout.compose.Visibility
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
@ -39,7 +34,6 @@ import com.tangem.features.send.impl.R
* @param subtitleEndOffset offset for subtitle ellipsis
* @param subtitleIconRes icon
*/
@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod")
@Composable
fun ListItemWithIcon(
title: String,
@ -51,80 +45,60 @@ fun ListItemWithIcon(
@DrawableRes subtitleIconRes: Int? = null,
) {
val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick)
ConstraintLayout(
Row(
modifier = modifier
.fillMaxWidth()
.clickable { hapticFeedback() }
.padding(horizontal = TangemTheme.dimens.spacing12),
) {
val (iconRef, titleRef, subtitleRef, subtitleIconRef) = createRefs()
val spacing2 = TangemTheme.dimens.spacing2
val spacing8 = TangemTheme.dimens.spacing8
val spacing10 = TangemTheme.dimens.spacing10
val spacing12 = TangemTheme.dimens.spacing12
IdentIcon(
address = title,
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.size(TangemTheme.dimens.size40)
.clip(RoundedCornerShape(TangemTheme.dimens.radius20))
.constrainAs(iconRef) {
start.linkTo(parent.start)
top.linkTo(parent.top, margin = spacing8)
bottom.linkTo(parent.bottom, margin = spacing8)
},
.clip(RoundedCornerShape(TangemTheme.dimens.radius20)),
)
EllipsisText(
text = title,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Justify,
ellipsis = TextEllipsis.Middle,
Column(
modifier = Modifier
.constrainAs(titleRef) {
start.linkTo(iconRef.end, margin = spacing12)
end.linkTo(parent.end)
top.linkTo(parent.top, margin = spacing10)
width = Dimension.fillToConstraints
},
)
Icon(
painter = painterResource(id = subtitleIconRes ?: R.drawable.ic_arrow_down_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size16)
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f), CircleShape)
.constrainAs(subtitleIconRef) {
start.linkTo(iconRef.end, margin = spacing12)
top.linkTo(titleRef.bottom)
bottom.linkTo(parent.bottom, margin = spacing10)
visibility = if (subtitleIconRes == null) Visibility.Gone else Visibility.Visible
},
)
val (text, offset) = remember(subtitle, info) {
if (info != null) {
val suffix = ", $info"
subtitle + suffix to suffix.length + subtitleEndOffset
} else {
subtitle to 0
.padding(vertical = TangemTheme.dimens.spacing10)
.padding(start = TangemTheme.dimens.spacing12),
) {
EllipsisText(
text = title,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Justify,
ellipsis = TextEllipsis.Middle,
modifier = Modifier,
)
Row {
if (subtitleIconRes != null) {
Icon(
painter = painterResource(id = subtitleIconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size16)
.background(TangemTheme.colors.background.tertiary, CircleShape),
)
}
val (text, offset) = remember(subtitle, info) {
if (info != null) {
val suffix = ", $info"
subtitle + suffix to suffix.length + subtitleEndOffset
} else {
subtitle to 0
}
}
EllipsisText(
text = text,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(offsetEnd = offset),
modifier = Modifier.padding(start = TangemTheme.dimens.spacing2),
)
}
}
EllipsisText(
text = text,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(offsetEnd = offset),
modifier = Modifier
.constrainAs(subtitleRef) {
start.linkTo(subtitleIconRef.end, margin = spacing2, goneMargin = spacing12)
end.linkTo(parent.end)
top.linkTo(titleRef.bottom)
bottom.linkTo(parent.bottom, margin = spacing10)
width = Dimension.fillToConstraints
},
)
}
}

View file

@ -1,8 +1,9 @@
package com.tangem.features.send.impl.presentation.ui.recipient
import androidx.annotation.StringRes
import androidx.compose.animation.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -17,9 +18,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.tangem.core.ui.components.inputrow.InputRowRecipient
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -29,18 +27,17 @@ import com.tangem.features.send.impl.presentation.domain.SendRecipientListConten
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.ImmutableList
private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY"
private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY"
private const val MY_WALLETS_HEADER_KEY = "MY_WALLETS_HEADER_KEY"
@Composable
internal fun SendRecipientContent(
uiState: SendStates.RecipientState?,
clickIntents: SendClickIntents,
recipientList: LazyPagingItems<SendRecipientListContent>,
) {
internal fun SendRecipientContent(uiState: SendStates.RecipientState?, clickIntents: SendClickIntents) {
if (uiState == null) return
val recipients = uiState.recent
val wallets = uiState.wallets
val memoField = uiState.memoTextField
val address = uiState.addressTextField
val isValidating by remember(uiState.isValidating) { derivedStateOf { uiState.isValidating } }
val isError by remember(address.isError) { derivedStateOf { address.isError } }
@ -72,7 +69,7 @@ internal fun SendRecipientContent(
)
}
}
uiState.memoTextField?.let { memoField ->
if (memoField != null) {
item(key = MEMO_FIELD_KEY) {
val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText
TextFieldWithPaste(
@ -89,144 +86,115 @@ internal fun SendRecipientContent(
)
}
}
recipientListItem(
recipientList = recipientList,
clickIntents = clickIntents,
listHeaderItem(
titleRes = R.string.send_recipient_wallets_title,
isVisible = wallets.isNotEmpty() && wallets.first().isVisible,
isFirst = true,
)
listItem(wallets, clickIntents, isLast = recipients.isEmpty())
listHeaderItem(
titleRes = R.string.send_recent_transactions,
isVisible = recipients.isNotEmpty() && recipients.first().isVisible,
isFirst = wallets.isEmpty(),
)
listItem(recipients, clickIntents, isLast = true)
}
}
@Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.recipientListItem(
recipientList: LazyPagingItems<SendRecipientListContent>,
private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Boolean, isFirst: Boolean) {
item(
key = titleRes,
) {
AnimatedVisibility(
visible = isVisible,
label = "Header Appearance Animation",
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut(),
modifier = Modifier
.animateItemPlacement()
.animateContentSize(),
) {
val (topPadding, paddingFromTop) = if (isFirst) {
TangemTheme.dimens.spacing20 to TangemTheme.dimens.spacing12
} else {
TangemTheme.dimens.spacing0 to TangemTheme.dimens.spacing8
}
val topRadius = if (isFirst) {
TangemTheme.dimens.radius12
} else {
TangemTheme.dimens.radius0
}
Text(
text = stringResource(titleRes),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.padding(top = topPadding)
.clip(
RoundedCornerShape(
topEnd = topRadius,
topStart = topRadius,
),
)
.background(TangemTheme.colors.background.action)
.padding(
top = paddingFromTop,
bottom = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
),
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.listItem(
list: ImmutableList<SendRecipientListContent>,
clickIntents: SendClickIntents,
isLast: Boolean,
) {
items(
count = recipientList.itemCount,
key = recipientList.itemKey {
when (it) {
is SendRecipientListContent.Wallets -> MY_WALLETS_HEADER_KEY
is SendRecipientListContent.Item -> it.id
}
},
contentType = recipientList.itemContentType { it::class.java },
count = list.size,
key = { list[it].id },
contentType = { list[it]::class.java },
) { index ->
recipientList[index]?.let { item ->
when (item) {
is SendRecipientListContent.Wallets -> {
RecipientWalletListItem(
item = item,
clickIntents = clickIntents,
modifier = Modifier
.animateItemPlacement()
.padding(top = TangemTheme.dimens.spacing20)
.then(
if (index == 0) {
val bottomRadius = if (item.isWalletsOnly) {
TangemTheme.dimens.radius12
} else {
TangemTheme.dimens.radius0
}
Modifier.clip(
RoundedCornerShape(
topEnd = TangemTheme.dimens.radius12,
topStart = TangemTheme.dimens.radius12,
bottomStart = bottomRadius,
bottomEnd = bottomRadius,
),
)
} else {
Modifier
},
),
)
}
is SendRecipientListContent.Item -> {
val title = item.title.resolveReference()
ListItemWithIcon(
title = item.title.resolveReference(),
subtitle = item.subtitle.resolveReference(),
info = item.timestamp?.resolveReference(),
subtitleEndOffset = item.subtitleEndOffset,
subtitleIconRes = item.subtitleIconRes,
modifier = Modifier
.then(
if (index == recipientList.itemCount - 1) {
Modifier
.padding(bottom = TangemTheme.dimens.spacing20)
.clip(
RoundedCornerShape(
bottomEnd = TangemTheme.dimens.radius12,
bottomStart = TangemTheme.dimens.radius12,
),
)
} else {
Modifier
},
)
.background(TangemTheme.colors.background.action),
onClick = {
clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress)
val item = list[index]
val title = item.title.resolveReference()
AnimatedVisibility(
visible = item.isVisible,
label = "Header Appearance Animation",
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut(),
modifier = Modifier
.animateItemPlacement()
.animateContentSize(),
) {
ListItemWithIcon(
title = title,
subtitle = item.subtitle.resolveReference(),
info = item.timestamp?.resolveReference(),
subtitleEndOffset = item.subtitleEndOffset,
subtitleIconRes = item.subtitleIconRes,
onClick = { clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress) },
modifier = Modifier
.then(
if (isLast && index == list.lastIndex) {
Modifier
.padding(bottom = TangemTheme.dimens.spacing12)
.clip(
shape = RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
),
)
} else {
Modifier
},
)
}
}
}
}
}
@Composable
private fun RecipientWalletListItem(
item: SendRecipientListContent.Wallets,
clickIntents: SendClickIntents,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.action)
.padding(top = TangemTheme.dimens.spacing12),
) {
if (item.list.isNotEmpty()) {
Text(
text = stringResource(R.string.send_recipient_wallets_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing8,
),
)
}
item.list.forEachIndexed { _, wallet ->
val title = wallet.title.resolveReference()
ListItemWithIcon(
title = wallet.title.resolveReference(),
subtitle = wallet.subtitle.resolveReference(),
onClick = { clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress) },
)
}
if (!item.isWalletsOnly) {
val topPadding = if (item.list.isNotEmpty()) {
TangemTheme.dimens.spacing8
} else {
TangemTheme.dimens.spacing0
}
Text(
text = stringResource(R.string.send_recent_transactions),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.padding(
top = topPadding,
bottom = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
),
.background(TangemTheme.colors.background.action),
)
}
}

View file

@ -4,8 +4,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.*
import androidx.paging.PagingData
import androidx.paging.cachedIn
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
@ -28,8 +26,7 @@ import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.models.TxHistoryItem
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.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -54,6 +51,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
@ -72,8 +70,7 @@ internal class SendViewModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val createTransactionUseCase: CreateTransactionUseCase,
@ -106,7 +103,8 @@ internal class SendViewModel @Inject constructor(
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var innerRouter: InnerSendRouter by Delegates.notNull()
private var stateRouter: StateRouter by Delegates.notNull()
var stateRouter: StateRouter by Delegates.notNull()
private set
private val stateFactory = SendStateFactory(
clickIntents = this,
@ -142,6 +140,7 @@ internal class SendViewModel @Inject constructor(
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
currentStateProvider = Provider { uiState },
userWalletProvider = Provider { userWallet },
stateRouterProvider = Provider { stateRouter },
clickIntents = this,
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
)
@ -151,6 +150,7 @@ internal class SendViewModel @Inject constructor(
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
currentStateProvider = Provider { uiState },
userWalletProvider = Provider { userWallet },
stateRouterProvider = Provider { stateRouter },
currencyChecksRepository = currencyChecksRepository,
clickIntents = this,
)
@ -190,7 +190,6 @@ internal class SendViewModel @Inject constructor(
fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) {
innerRouter = router
this.stateRouter = stateRouter
uiState = uiState.copy(currentState = stateRouter.currentState)
}
private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) {
@ -301,12 +300,10 @@ internal class SendViewModel @Inject constructor(
combine(
flow = getUserWallets().conflate(),
flow2 = getTxHistory().conflate(),
flow3 = getTxHistoryCount().conflate(),
) { wallets, txHistory, txHistoryCount ->
stateFactory.onLoadedRecipientList(
) { wallets, txHistory ->
uiState = stateFactory.onLoadedRecipientList(
wallets = wallets,
txHistory = txHistory,
txHistoryCount = txHistoryCount,
)
}
.flowOn(dispatchers.io)
@ -348,34 +345,18 @@ internal class SendViewModel @Inject constructor(
}
}
private fun getTxHistory(): Flow<PagingData<TxHistoryItem>> {
return txHistoryItemsUseCase(
private fun getTxHistory(): Flow<List<TxHistoryItem>> {
return getFixedTxHistoryItemsUseCase(
userWalletId = userWalletId,
currency = cryptoCurrency,
).fold(
ifRight = {
it.distinctUntilChanged().cachedIn(viewModelScope)
},
ifLeft = {
emptyFlow()
},
ifRight = { it.distinctUntilChanged() },
ifLeft = { emptyFlow() },
)
}
private fun getTxHistoryCount(): Flow<Int> {
return flow {
txHistoryItemsCountUseCase(
userWalletId = userWalletId,
currency = cryptoCurrency,
).fold(
ifRight = { emit(it) },
ifLeft = { emit(0) },
)
}
}
private fun onStateActive() {
uiState.currentState
stateRouter.currentState
.onEach {
when (it.type) {
SendUiStateType.Fee -> if (!it.isFromConfirmation) loadFee()
@ -499,11 +480,13 @@ internal class SendViewModel @Inject constructor(
}
private suspend fun validateAddress(value: String): Boolean {
return validateWalletAddressUseCase(
val isValidAddress = validateWalletAddressUseCase(
userWalletId = userWalletId,
network = cryptoCurrency.network,
address = value,
).getOrElse { false }
onEnteredValidAddress(isValidAddress)
return isValidAddress
}
private suspend fun checkIfXrpAddressValue(value: String): Boolean {
@ -515,6 +498,16 @@ internal class SendViewModel @Inject constructor(
true
} ?: false
}
private fun onEnteredValidAddress(isValidAddress: Boolean) {
val recipientState = uiState.recipientState ?: return
uiState = uiState.copy(
recipientState = recipientState.copy(
recent = recipientState.recent.map { it.copy(isVisible = !isValidAddress) }.toPersistentList(),
wallets = recipientState.wallets.map { it.copy(isVisible = !isValidAddress) }.toPersistentList(),
),
)
}
// endregion
// region fee
@ -613,9 +606,9 @@ internal class SendViewModel @Inject constructor(
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
}
override fun onExploreClick(txUrl: String) {
override fun onExploreClick() {
analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
innerRouter.openUrl(txUrl)
innerRouter.openUrl(uiState.sendState.txUrl)
}
override fun onShareClick() {