Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-23 08:50:56 +01:00
commit f49a2bc054
22 changed files with 272 additions and 139 deletions

View file

@ -1,8 +1,10 @@
@file:Suppress("Filename")
package com.tangem.tap.di
import javax.inject.Qualifier
@Deprecated("Use one in Core Utils")
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DelayedWork

View file

@ -29,6 +29,7 @@ import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTr
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.*
import java.math.BigDecimal
import java.text.DecimalFormat
/**
@ -124,32 +125,49 @@ fun AmountTextField(
private fun prepareEnter(oldValue: String, newValue: String, decimalFormat: DecimalFormat, decimals: Int): String {
val decimalSymbol = decimalFormat.decimalFormatSymbols.decimalSeparator
return if (decimalFormat.isValidSymbols(newValue)) {
val parsedValue = newValue.parseBigDecimalOrNull()?.toPlainString()
?: if (newValue.isBlank()) "" else oldValue
val replacedWithSymbol = if (parsedValue.findLast { it != decimalSymbol } != null) {
when {
parsedValue.findLast { it == COMMA_SEPARATOR } != null -> {
parsedValue.replace(COMMA_SEPARATOR, decimalSymbol)
}
parsedValue.findLast { it == POINT_SEPARATOR } != null -> {
parsedValue.replace(POINT_SEPARATOR, decimalSymbol)
}
else -> parsedValue
}
} else {
parsedValue
}
val joinedSymbol = if (newValue.endsWith(COMMA_SEPARATOR) || newValue.endsWith(POINT_SEPARATOR)) {
replacedWithSymbol.plus(decimalSymbol)
} else {
replacedWithSymbol
}
decimalFormat.getValidatedNumberWithFixedDecimals(joinedSymbol, decimals)
val parsedDecimal = newValue.parseBigDecimalOrNull()
val parsedValue = parsedDecimal?.toPlainString() ?: if (newValue.isBlank()) "" else oldValue
val replacedWithSymbol = parsedValue.replaceDecimalSymbol(decimalSymbol)
val joinedSymbol = replacedWithSymbol.preserveDecimalSymbol(newValue, decimalSymbol)
val withPreservedZeros = joinedSymbol.preserveTrailingZeros(newValue, parsedDecimal, decimalSymbol)
decimalFormat.getValidatedNumberWithFixedDecimals(withPreservedZeros, decimals)
} else {
oldValue
}
}
private fun String.replaceDecimalSymbol(decimalSymbol: Char) = if (this.findLast { it != decimalSymbol } != null) {
when {
this.findLast { it == COMMA_SEPARATOR } != null -> {
this.replace(COMMA_SEPARATOR, decimalSymbol)
}
this.findLast { it == POINT_SEPARATOR } != null -> {
this.replace(POINT_SEPARATOR, decimalSymbol)
}
else -> this
}
} else {
this
}
private fun String.preserveDecimalSymbol(newValue: String, decimalSymbol: Char) = if (
newValue.endsWith(COMMA_SEPARATOR) || newValue.endsWith(POINT_SEPARATOR)
) {
this.plus(decimalSymbol)
} else {
this
}
private fun String.preserveTrailingZeros(newValue: String, parsedDecimal: BigDecimal?, decimalSymbol: Char): String {
val trailingZeros = newValue.split(decimalSymbol).getOrNull(1)?.takeLastWhile { it == '0' }.orEmpty()
return when {
this.endsWith('0') && parsedDecimal?.scale() != 0 -> this
parsedDecimal?.scale() == 0 && trailingZeros.isNotEmpty() -> "$this$decimalSymbol$trailingZeros"
else -> this.plus(trailingZeros)
}
}
private fun DecimalFormat.isValidSymbols(text: String): Boolean {
return checkDecimalSeparatorDuplicate(text)
}

View file

@ -0,0 +1,7 @@
package com.tangem.utils.coroutines
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DelayedWork

View file

@ -0,0 +1,23 @@
package com.tangem.utils.di
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DelayedWorkCoroutineModule {
@Provides
@Singleton
@DelayedWork
fun provideDelayedWorkCoroutineScope(coroutineDispatcherProvider: CoroutineDispatcherProvider): CoroutineScope {
return CoroutineScope(SupervisorJob() + coroutineDispatcherProvider.io)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.tokens.repository
import arrow.core.raise.catch
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
@ -16,6 +17,7 @@ import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.repository.NetworksRepository
@ -102,6 +104,27 @@ internal class DefaultNetworksRepository(
return blockchain == Blockchain.Aptos
}
override suspend fun getNetworkAddresses(
userWalletId: UserWalletId,
network: Network,
): List<CryptoCurrencyAddress> {
// Get list of currencies matching [network]
val currencies = getCurrencies(userWalletId)
.filter { currency -> network.id == currency.network.id }
// There is no currencies matching given [networks] in [userWalletId]
if (currencies.toList().isEmpty()) return emptyList()
return currencies.toList().map { currency ->
CryptoCurrencyAddress(
cryptoCurrency = currency,
address = walletManagersFacade.getAddresses(userWalletId, currency.network)
.firstOrNull { it.type == AddressType.Default }
?.value.orEmpty(),
)
}
}
private suspend fun fetchNetworksStatusesIfCacheExpired(
userWalletId: UserWalletId,
networks: Set<Network>,
@ -209,11 +232,16 @@ internal class DefaultNetworksRepository(
}
private suspend fun getCurrencies(userWalletId: UserWalletId, networks: Set<Network>): Sequence<CryptoCurrency> {
val currencies = getCurrencies(userWalletId)
return currencies.filter { networks.contains(it.network) }
}
private suspend fun getCurrencies(userWalletId: UserWalletId): Sequence<CryptoCurrency> {
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find user wallet with provided ID: $userWalletId"
}
val currencies = if (userWallet.isMultiCurrency) {
return if (userWallet.isMultiCurrency) {
val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
@ -229,8 +257,6 @@ internal class DefaultNetworksRepository(
sequenceOf(currency)
}
}
return currencies.filter { networks.contains(it.network) }
}
private suspend fun invalidateCacheKeyIfNeeded(

View file

@ -0,0 +1,6 @@
package com.tangem.domain.tokens.model
data class CryptoCurrencyAddress(
val cryptoCurrency: CryptoCurrency,
val address: String,
)

View file

@ -1,24 +1,15 @@
package com.tangem.domain.tokens
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class GetNetworkAddressesUseCase(
internal val networksRepository: NetworksRepository,
) {
operator fun invoke(userWalletId: UserWalletId, network: Network): Flow<String> =
networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network))
.map { networkStatuses ->
when (val networkStatus = networkStatuses.singleOrNull { it.network.id == network.id }?.value) {
is NetworkStatus.NoAccount -> networkStatus.address.defaultAddress.value
is NetworkStatus.Unreachable -> networkStatus.address?.defaultAddress?.value.orEmpty()
is NetworkStatus.Verified -> networkStatus.address.defaultAddress.value
else -> ""
}
}
suspend fun invokeSync(userWalletId: UserWalletId, network: Network): List<CryptoCurrencyAddress> {
return networksRepository.getNetworkAddresses(userWalletId, network)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
@ -60,4 +61,6 @@ interface NetworksRepository {
): Set<NetworkStatus>
fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean
suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List<CryptoCurrencyAddress>
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.tokens.repository
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.core.error.DataError
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.tokens.model.Network
@ -43,4 +44,10 @@ internal class MockNetworksRepository(
}
override fun isNeedToCreateAccountWithoutReserve(network: Network) = false
override suspend fun getNetworkAddresses(
userWalletId: UserWalletId,
network: Network,
): List<CryptoCurrencyAddress> {
return emptyList()
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.send.impl.presentation.domain
import androidx.compose.runtime.Immutable
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
/**
@ -15,4 +16,5 @@ data class AvailableWallet(
val name: String,
val userWalletId: UserWalletId,
val address: String,
val cryptoCurrency: CryptoCurrency,
)

View file

@ -296,6 +296,7 @@ internal class SendStateFactory(
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = state.sendState?.reduceAmountBy,
),
)
}
@ -331,6 +332,12 @@ internal class SendStateFactory(
fun getSendNotificationState(notifications: ImmutableList<SendNotification>): SendUiState {
val state = currentStateProvider()
val sendState = state.sendState ?: return state
val reducedBy = sendState.reduceAmountBy.takeIf {
notifications.none {
it is SendNotification.Error.ExistentialDeposit ||
it is SendNotification.Error.TransactionLimitError
}
}
return state.copy(
sendState = sendState.copy(
isPrimaryButtonEnabled = isPrimaryButtonEnabled(
@ -338,6 +345,7 @@ internal class SendStateFactory(
isSending = sendState.isSending,
notifications = notifications,
),
reduceAmountBy = reducedBy,
notifications = notifications,
showTapHelp = sendState.showTapHelp && notifications.isEmpty(),
),

View file

@ -142,7 +142,6 @@ internal sealed class SendStates {
val txUrl: String,
val ignoreAmountReduce: Boolean,
val reduceAmountBy: BigDecimal?,
val reduceAmountTo: BigDecimal?,
val isFromConfirmation: Boolean,
val showTapHelp: Boolean,
val notifications: ImmutableList<SendNotification>,

View file

@ -38,9 +38,6 @@ internal class SendAmountReduceToConverter(
val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isZero()
return state.copyWrapped(
isEditState = isEditState,
sendState = state.sendState?.copy(
reduceAmountBy = value,
),
amountState = amountState.copy(
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
amountTextField = amountTextField.copy(

View file

@ -17,7 +17,6 @@ internal class SendConfirmStateConverter(
txUrl = "",
ignoreAmountReduce = false,
reduceAmountBy = null,
reduceAmountTo = null,
isFromConfirmation = true,
showTapHelp = isTapHelpPreviewEnabledProvider(),
notifications = persistentListOf(),

View file

@ -66,18 +66,20 @@ internal class SendNotificationFactory(
val amountValue = amountState.amountTextField.cryptoAmount.value ?: BigDecimal.ZERO
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
val reduceAmountBy = sendState.reduceAmountBy ?: BigDecimal.ZERO
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isSubtractAvailableProvider(),
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
val sendingAmount = checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isFeeCoverage,
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = sendState.reduceAmountBy,
reduceAmountBy = reduceAmountBy,
)
buildList {
// errors
@ -221,7 +223,7 @@ internal class SendNotificationFactory(
),
onConfirmClick = {
clickIntents.onAmountReduceClick(
reduceAmountBy = currencyDeposit,
reduceAmountBy = currencyDeposit.minus(diff),
clazz = SendNotification.Error.ExistentialDeposit::class.java,
)
},

View file

@ -16,7 +16,7 @@ internal fun checkAndCalculateSubtractedAmount(
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
reduceAmountBy: BigDecimal?,
reduceAmountBy: BigDecimal,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
val isFeeCoverage = checkFeeCoverage(
@ -24,17 +24,12 @@ internal fun checkAndCalculateSubtractedAmount(
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
val subtractedAmount = calculateSubtractedAmount(
isFeeCoverage = isFeeCoverage,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
)
return if (reduceAmountBy != null) {
subtractedAmount.minus(reduceAmountBy)
return if (isFeeCoverage) {
balance.minus(reduceAmountBy).minus(feeValue)
} else {
subtractedAmount
amountValue.minus(reduceAmountBy)
}
}
@ -46,26 +41,11 @@ internal fun checkFeeCoverage(
balance: BigDecimal,
amountValue: BigDecimal,
feeValue: BigDecimal,
reduceAmountBy: BigDecimal?,
): Boolean {
if (!isSubtractAvailable) return false
return balance < amountValue + feeValue && balance > feeValue && balance >= amountValue
}
/**
* Calculates subtracted amount
*/
private fun calculateSubtractedAmount(
isFeeCoverage: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
return if (isFeeCoverage) {
minOf(amountValue, balance.minus(feeValue))
} else {
amountValue
}
val amountWithReduced = (reduceAmountBy ?: BigDecimal.ZERO) + amountValue
return balance < amountWithReduced + feeValue && balance > feeValue && balance >= amountWithReduced
}
/**

View file

@ -2,12 +2,25 @@ package com.tangem.features.send.impl.presentation.state.previewdata
import androidx.compose.foundation.text.KeyboardOptions
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import kotlinx.collections.immutable.persistentListOf
internal object RecipientStatePreviewData {
private val defaultRecentItem = SendRecipientListContent(
id = "sanctus",
title = stringReference("address"),
subtitle = stringReference("0.001 BTC"),
timestamp = stringReference("1.01.1970, 00:00"),
subtitleEndOffset = 0,
subtitleIconRes = R.drawable.ic_arrow_down_24,
isVisible = true,
isLoading = false,
)
val recipientState = SendStates.RecipientState(
addressTextField = SendTextField.RecipientAddress(
value = "0x23948239805671983476598176",
@ -19,8 +32,14 @@ internal object RecipientStatePreviewData {
error = null,
),
memoTextField = null,
recent = persistentListOf(),
wallets = persistentListOf(),
recent = persistentListOf(
defaultRecentItem.copy(id = "1"),
defaultRecentItem.copy(id = "2"),
defaultRecentItem.copy(id = "3"),
),
wallets = persistentListOf(
defaultRecentItem.copy(id = "4", subtitle = stringReference("Wallet")),
),
network = "Ethereum",
isValidating = false,
isPrimaryButtonEnabled = true,

View file

@ -21,20 +21,27 @@ internal class SendRecipientWalletListConverter :
private fun List<AvailableWallet?>.filterWallets(): PersistentList<SendRecipientListContent> {
var walletsCounter = 0
return this.filterNotNull()
.filter { it.address.isNotBlank() }
.groupBy { item -> item.name }
.values.map {
it.mapIndexed { index, item ->
val name = if (it.size > 1) {
"${item.name} ${index.inc()}"
} else {
item.name
.values.map { wallets ->
val groupedByWallet = wallets.groupBy { it.userWalletId }
var i = 0
groupedByWallet
.flatMap { item ->
item.value.map { wallet ->
val name = if (groupedByWallet.size > 1) {
"${wallet.name} ${++i}"
} else {
wallet.name
}
SendRecipientListContent(
id = "${WALLET_KEY_TAG}${walletsCounter++}",
title = TextReference.Str(wallet.address),
subtitle = TextReference.Str(name),
)
}
}
SendRecipientListContent(
id = "${WALLET_KEY_TAG}${walletsCounter++}",
title = TextReference.Str(item.address),
subtitle = TextReference.Str(name),
)
}
}
.flatten()
.toPersistentList()

View file

@ -38,6 +38,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.utils.getFiatString
@Composable
internal fun SendNavigationButtons(
@ -183,12 +184,12 @@ private fun SendingText(
fiatCurrencySymbol = feeState.appCurrency.symbol,
fiatCurrencyCode = feeState.appCurrency.code,
)
val feeValue = BigDecimalFormatter.formatFiatAmount(
fiatAmount = feeState.fee?.amount?.value,
fiatCurrencySymbol = feeState.appCurrency.symbol,
fiatCurrencyCode = feeState.appCurrency.code,
val feeValue = getFiatString(
value = feeState.fee?.amount?.value,
rate = feeState.rate,
appCurrency = feeState.appCurrency,
)
val textResource = remember(sendingValue, feeValue) {
val textResource = remember(uiState) {
resourceReference(
id = R.string.send_summary_transaction_description,
formatArgs = wrappedList(sendingValue, feeValue),

View file

@ -97,11 +97,13 @@ private fun ListItemWithIcon(
address = title,
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.size(TangemTheme.dimens.size36)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18)),
.size(TangemTheme.dimens.size40)
.clip(RoundedCornerShape(TangemTheme.dimens.radius20)),
)
Column(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
modifier = Modifier
.height(TangemTheme.dimens.size36)
.padding(start = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.SpaceBetween,
) {
EllipsisText(
@ -120,7 +122,8 @@ private fun ListItemWithIcon(
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size16)
.background(TangemTheme.colors.background.tertiary, CircleShape),
.background(TangemTheme.colors.background.tertiary, CircleShape)
.padding(TangemTheme.dimens.spacing2),
)
}
val (text, offset) = remember(subtitle, info) {
@ -154,11 +157,11 @@ private fun ListItemLoading(modifier: Modifier = Modifier) {
CircleShimmer(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.size(TangemTheme.dimens.size36),
.size(TangemTheme.dimens.size40),
)
Column(
modifier = Modifier
.height(TangemTheme.dimens.size32)
.height(TangemTheme.dimens.size36)
.padding(start = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.SpaceBetween,
) {

View file

@ -17,6 +17,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.Strings.STARS
import com.tangem.core.ui.components.inputrow.InputRowRecipient
import com.tangem.core.ui.extensions.resolveReference
@ -26,6 +29,8 @@ import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.state.previewdata.RecipientStatePreviewData
import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.ImmutableList
@ -150,7 +155,7 @@ private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Bo
TangemTheme.dimens.spacing0 to TangemTheme.dimens.spacing8
}
val topRadius = if (isFirst) {
TangemTheme.dimens.radius12
TangemTheme.dimens.radius16
} else {
TangemTheme.dimens.radius0
}
@ -170,7 +175,7 @@ private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Bo
.background(TangemTheme.colors.background.action)
.padding(
top = paddingFromTop,
bottom = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing12,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
),
@ -242,4 +247,25 @@ private fun AnimateRecentAppearance(isVisible: Boolean, content: @Composable ()
Box(modifier = Modifier.fillMaxWidth())
}
}
}
@Preview(widthDp = 360, heightDp = 800)
@Composable
private fun SendRecipientContent_Preview(
@PreviewParameter(SendRecipientContentPreviewProvider::class) recipientState: SendStates.RecipientState,
) {
TangemTheme(isDark = false) {
SendRecipientContent(
uiState = recipientState,
clickIntents = SendClickIntentsStub,
isBalanceHidden = false,
)
}
}
private class SendRecipientContentPreviewProvider : PreviewParameterProvider<SendStates.RecipientState> {
override val values: Sequence<SendStates.RecipientState>
get() = sequenceOf(
RecipientStatePreviewData.recipientState,
)
}

View file

@ -4,6 +4,7 @@ import android.os.SystemClock
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastDistinctBy
import androidx.lifecycle.*
import arrow.core.Either
import arrow.core.getOrElse
@ -52,10 +53,11 @@ import com.tangem.features.send.impl.presentation.state.fee.*
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
@ -70,7 +72,6 @@ internal class SendViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
@ -93,6 +94,9 @@ internal class SendViewModel @Inject constructor(
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
@DelayedWork private val coroutineScope: CoroutineScope,
validateTransactionUseCase: ValidateTransactionUseCase,
currencyChecksRepository: CurrencyChecksRepository,
isFeeApproximateUseCase: IsFeeApproximateUseCase,
@ -270,6 +274,7 @@ internal class SendViewModel @Inject constructor(
.saveIn(balanceHidingJobHolder)
}
// TODO [REDACTED_JIRA]
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) {
if (cryptoCurrency is CryptoCurrency.Coin) {
getCurrencyStatusUpdates(
@ -375,8 +380,11 @@ internal class SendViewModel @Inject constructor(
feeCryptoCurrencyStatus = feeCurrencyStatus
subscribeOnQRScannerResult()
when {
uiState.sendState?.isSuccess == true -> {
stateRouter.showSend()
uiState.sendState?.isSuccess != true -> {
uiState = stateFactory.getReadyState()
getWalletsAndRecent()
stateRouter.showRecipient()
updateNotifications()
}
transactionId != null && amount != null && destinationAddress != null -> {
loadFee()
@ -384,12 +392,6 @@ internal class SendViewModel @Inject constructor(
stateRouter.showSend()
updateNotifications()
}
else -> {
uiState = stateFactory.getReadyState()
getWalletsAndRecent()
stateRouter.showRecipient()
updateNotifications()
}
}
}
@ -407,41 +409,43 @@ internal class SendViewModel @Inject constructor(
?.toAvailableWallets()
.orEmpty()
}.onSuccess { result ->
combine(*result.toTypedArray()) { it }
.onEach {
userWallets = it.filterNotNull().toList()
uiState = stateFactory.onLoadedWalletsList(wallets = userWallets)
}
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
userWallets = result
uiState = stateFactory.onLoadedWalletsList(wallets = userWallets)
}.onFailure {
uiState = stateFactory.onLoadedWalletsList(wallets = emptyList())
}
}
}
private suspend fun List<UserWallet>.toAvailableWallets(): List<Flow<AvailableWallet?>> =
filterNot { it.walletId == userWalletId || it.isLocked }
private suspend fun List<UserWallet>.toAvailableWallets(): List<AvailableWallet> {
val currentAddress: String = kotlin.runCatching {
cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
}.getOrNull().orEmpty()
return filterNot { it.isLocked }
.mapNotNull { wallet ->
val status = if (!wallet.isMultiCurrency) {
val addresses = if (!wallet.isMultiCurrency) {
getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let {
if (it.network.id == cryptoCurrency.network.id) {
getNetworkAddressesUseCase(wallet.walletId, it.network)
getNetworkAddressesUseCase.invokeSync(wallet.walletId, it.network)
} else {
null
}
}
} else {
getNetworkAddressesUseCase(wallet.walletId, cryptoCurrency.network)
getNetworkAddressesUseCase.invokeSync(wallet.walletId, cryptoCurrency.network)
}
status?.map { address ->
AvailableWallet(
name = wallet.name,
address = address,
userWalletId = wallet.walletId,
)
}
}
addresses
?.filter { it.address != currentAddress }
?.map { (cryptoCurrency, address) ->
AvailableWallet(
name = wallet.name,
address = address,
cryptoCurrency = cryptoCurrency,
userWalletId = wallet.walletId,
)
}?.fastDistinctBy { it.address }
}.flatten()
}
private suspend fun getTxHistory() {
val txHistoryList = getFixedTxHistoryItemsUseCase.getSync(
@ -802,7 +806,7 @@ internal class SendViewModel @Inject constructor(
}
uiState = sendNotificationFactory.dismissNotificationState(clazz)
feeReload()
updateNotifications()
}
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
@ -822,7 +826,7 @@ internal class SendViewModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = uiState.sendState?.reduceAmountBy,
reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
)
viewModelScope.launch(dispatchers.main) {
@ -866,8 +870,8 @@ internal class SendViewModel @Inject constructor(
ifRight = {
uiState = stateFactory.getSendingStateUpdate(isSending = false)
updateTransactionStatus(txData)
scheduleBalanceUpdate()
addTokenToWalletIfNeeded()
scheduleUpdates()
sendScreenAnalyticSender.sendTransaction()
},
)
@ -894,12 +898,15 @@ internal class SendViewModel @Inject constructor(
uiState = stateFactory.getTransactionSendState(txData, txUrl)
}
private fun scheduleBalanceUpdate() {
viewModelScope.launch(dispatchers.io) {
delay(BALANCE_UPDATE_DELAY)
fetchCurrencyStatusUseCase.invoke(
userWalletId = userWalletId,
id = cryptoCurrency.id,
private fun scheduleUpdates() {
coroutineScope.launch {
// we should update network to find pending tx after 1 sec
fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network))
// we should update network for new balance
updateDelayedCurrencyStatusUseCase(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
delayMillis = BALANCE_UPDATE_DELAY,
refresh = true,
)
}
@ -954,7 +961,7 @@ internal class SendViewModel @Inject constructor(
private companion object {
const val CHECK_FEE_UPDATE_DELAY = 60_000L
const val BALANCE_UPDATE_DELAY = 10_000L
const val BALANCE_UPDATE_DELAY = 11_000L
const val RU_LOCALE = "ru"
const val EN_LOCALE = "en"