Updated on 2026-08-14
This commit is contained in:
commit
f49a2bc054
22 changed files with 272 additions and 139 deletions
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.utils.coroutines
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class DelayedWork
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.tokens.model
|
||||
|
||||
data class CryptoCurrencyAddress(
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val address: String,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ internal class SendConfirmStateConverter(
|
|||
txUrl = "",
|
||||
ignoreAmountReduce = false,
|
||||
reduceAmountBy = null,
|
||||
reduceAmountTo = null,
|
||||
isFromConfirmation = true,
|
||||
showTapHelp = isTapHelpPreviewEnabledProvider(),
|
||||
notifications = persistentListOf(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue