From 6465307d0ad616ee58090d52a1e3e86065e2c90a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Oct 2024 12:07:48 +0300 Subject: [PATCH 01/18] Updated on 2026-08-14 --- build.gradle.kts | 12 + core/ui/build.gradle.kts | 5 + .../AmountVisualTransformation.kt | 33 +- .../bigdecimal/BigDecimalCompactFormat.kt | 159 ++++++++ .../bigdecimal/BigDecimalCryptoFormat.kt | 218 ++++++++++ .../format/bigdecimal/BigDecimalFiatFormat.kt | 144 +++++++ .../ui/format/bigdecimal/BigDecimalFormat.kt | 29 ++ .../bigdecimal/BigDecimalFormatConstants.kt | 20 + .../bigdecimal/BigDecimalPercentFormat.kt | 39 ++ .../tangem/core/ui/format/bigdecimal/Utils.kt | 15 + .../core/ui/utils/BigDecimalFormatter.kt | 287 +------------- .../bigdecimal/BigDecimalCryptoFormatTest.kt | 373 ++++++++++++++++++ .../bigdecimal/BigDecimalFiatFormatTest.kt | 297 ++++++++++++++ .../format/bigdecimal/BigDecimalFormatTest.kt | 29 ++ .../bigdecimal/BigDecimalPercentFormatTest.kt | 70 ++++ .../core/ui/format/bigdecimal/TestUtils.kt | 7 + .../model/converters/InsightsConverter.kt | 22 +- .../impl/model/converters/MetricsConverter.kt | 26 +- .../converters/MarketsTokenItemConverter.kt | 17 +- 19 files changed, 1499 insertions(+), 303 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCompactFormat.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatConstants.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/Utils.kt create mode 100644 core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt create mode 100644 core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt create mode 100644 core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatTest.kt create mode 100644 core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormatTest.kt create mode 100644 core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/TestUtils.kt diff --git a/build.gradle.kts b/build.gradle.kts index e71d217293..188c0a6ef6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + plugins { alias(deps.plugins.kotlin.android) apply false alias(deps.plugins.kotlin.jvm) apply false @@ -20,6 +22,16 @@ interface Injected { val fs: FileSystemOperations } +// Test Logging +subprojects { + tasks.withType { + testLogging { + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = true + } + } +} + val assembleInternalQA by tasks.registering { group = "build" description = "Builds internal APK to 'build/outputs' directory" diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 3781bde624..c7fb1b5984 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -52,4 +52,9 @@ dependencies { api(deps.jodatime) implementation(deps.timber) implementation(deps.markdown) + + /** Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index 8f10352030..17521b19b9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt @@ -4,10 +4,15 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE +import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode import com.tangem.core.ui.utils.defaultFormat import com.tangem.core.ui.utils.formatWithThousands +import timber.log.Timber import java.text.DecimalFormat +import java.text.NumberFormat +import java.util.Locale class AmountVisualTransformation( private val decimals: Int, @@ -25,13 +30,13 @@ class AmountVisualTransformation( val formattedText = if (formattedAmount.isNotEmpty() && symbol != null) { AnnotatedString( if (currencyCode != null) { - BigDecimalFormatter.formatFiatEditableAmount( + formatFiatEditableAmount( fiatAmount = formattedAmount, fiatCurrencyCode = currencyCode, fiatCurrencySymbol = symbol, ) } else { - BigDecimalFormatter.formatWithSymbol(formattedAmount, symbol) + formatWithSymbol(formattedAmount, symbol) }, ) } else { @@ -45,6 +50,28 @@ class AmountVisualTransformation( ) } + private fun formatFiatEditableAmount( + fiatAmount: String?, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), + ): String { + if (fiatAmount == null) return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN + + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + } + val formatter = requireNotNull(numberFormatter as? DecimalFormat) { + Timber.e("NumberFormat is null") + return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN + } + return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}" + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + } + + private fun formatWithSymbol(amount: String, symbol: String) = "$amount$CURRENCY_SPACE$symbol" + private class OffsetMappingImpl( private val text: String, private val formattedText: AnnotatedString, diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCompactFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCompactFormat.kt new file mode 100644 index 0000000000..f083a63c47 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCompactFormat.kt @@ -0,0 +1,159 @@ +package com.tangem.core.ui.format.bigdecimal + +import android.icu.text.CompactDecimalFormat +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Locale + +// == Formatters == + +/** + * Formats the amount in compact format. + * "123456.6" -> "$123.457K" + * "12345.6" -> "$123.046K" + * @param threeDigitsMethod if true, will format the amount always with 3 significant digits + */ +fun BigDecimalFiatFormat.compact(threeDigitsMethod: Boolean = false): BigDecimalFormat = BigDecimalFormat { value -> + if (value < BigDecimal.ONE) { + return@BigDecimalFormat defaultAmount()(value) + } + + val rawAmount = formatCompactAmount( + amount = value, + locale = locale, + threeDigitsMethod = threeDigitsMethod, + ) + + addFiatCurrencySymbolToStringAmount( + amount = rawAmount, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) +} + +/** + * Formats the amount in compact format. + * "123456.6" -> "ETH 123.457K" + * "12345.6" -> "123.046K ETH" + * @param threeDigitsMethod if true, will format the amount always with 3 significant digits + */ +fun BigDecimalCryptoFormat.compact(threeDigitsMethod: Boolean = false): BigDecimalFormat = BigDecimalFormat { value -> + if (value < BigDecimal.ONE) { + return@BigDecimalFormat defaultAmount()(value) + } + + val rawAmount = formatCompactAmount( + amount = value, + locale = locale, + threeDigitsMethod = threeDigitsMethod, + ) + + addFiatCurrencySymbolToStringAmount( + amount = rawAmount, + fiatCurrencyCode = BigDecimalFormatConstants.usdCurrency.currencyCode, + fiatCurrencySymbol = BigDecimalFormatConstants.usdCurrency.symbol, + locale = locale, + ).replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = BigDecimalFormatConstants.usdCurrency.symbol, + cryptoCurrencySymbol = symbol, + ) +} + +/** + * Formats the amount in compact format. + * ex. "123456.6" -> "123.46K", "12345.6" -> "123.05K" + * Negative amount is not supported! + */ +fun BigDecimalFormatScope.rawCompact(locale: Locale = Locale.getDefault()) = BigDecimalFormat { value -> + if (value < BigDecimal.ZERO) { + return@BigDecimalFormat value.toPlainString() + } + + formatCompactAmount( + amount = value, + locale = locale, + threeDigitsMethod = false, + ) +} + +// == Helpers == + +/** + * "123456.6" -> "123.457K" + * "12345.6" -> "123.046K" + * Negative amount is not supported + * @param threeDigitsMethod if true, will format the amount always with 3 significant digits + * @param scale the number of digits to the right of the decimal point + */ +@Suppress("MagicNumber") +private fun formatCompactAmount( + amount: BigDecimal, + locale: Locale = Locale.getDefault(), + threeDigitsMethod: Boolean = false, +): String { + if (threeDigitsMethod) { + val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP) + val digitsCount = scaledAmount.toString().count() + val digitsToFormat = 6 - when (digitsCount % 3) { + 0 -> 0 + 1 -> 2 + else -> 1 + } + + val formatter = CompactDecimalFormat.getInstance( + locale, + CompactDecimalFormat.CompactStyle.SHORT, + ).apply { + minimumSignificantDigits = 4 + maximumSignificantDigits = digitsToFormat + } + + return formatter.format(scaledAmount) + } else { + val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP) + val digitsCount = scaledAmount.toString().count() + val digitsToFormat = 5 - when (digitsCount % 3) { + 0 -> 0 + 1 -> 2 + else -> 1 + } + + val formatter = CompactDecimalFormat.getInstance( + locale, + CompactDecimalFormat.CompactStyle.SHORT, + ).apply { + minimumSignificantDigits = 2 + maximumSignificantDigits = digitsToFormat + } + + return formatter.format(scaledAmount) + } +} + +/** + * Adds a proper currency symbol for the provided formatted [amount] + * ex. '10.0k" -> "$10.0k", "string" -> "$string" + */ +private fun addFiatCurrencySymbolToStringAmount( + amount: String, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), +): String { + val sampleAmount = BigDecimal.TEN + val currency = getJavaCurrencyByCode(fiatCurrencyCode) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + maximumFractionDigits = 0 + minimumFractionDigits = 0 + this.currency = currency + } + + val formatted = formatter.format(sampleAmount) + .replace(currency.getSymbol(locale), fiatCurrencySymbol) + .replace(sampleAmount.toString(), amount) + + return formatted +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt new file mode 100644 index 0000000000..6973261be0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt @@ -0,0 +1,218 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.FORMAT_THRESHOLD +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.extensions.isNotWhitespace +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Currency +import java.util.Locale + +open class BigDecimalCryptoFormat( + val symbol: String, + val decimals: Int, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormat { + + override fun invoke(value: BigDecimal): String = defaultAmount()(value) +} + +class BigDecimalCryptoFormatFull( + val cryptoCurrency: CryptoCurrency, + locale: Locale = Locale.getDefault(), +) : BigDecimalCryptoFormat( + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + locale = locale, +) { + override fun invoke(value: BigDecimal): String = defaultAmount()(value) +} + +// == Initializers == + +fun BigDecimalFormatScope.crypto( + symbol: String, + decimals: Int, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormat { + return BigDecimalCryptoFormat( + symbol = symbol, + decimals = decimals, + locale = locale, + ) +} + +fun BigDecimalFormatScope.crypto( + cryptoCurrency: CryptoCurrency, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormat { + return BigDecimalCryptoFormat( + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + locale = locale, + ) +} + +// == Formatters == + +fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) +} + +fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value -> + val formatter = if (value.isMoreThanThreshold()) { + NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = 2 + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + } else { + NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.DOWN + } + } + + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) +} + +/** + * Format for displaying crypto amounts with their original decimals. + */ +fun BigDecimalCryptoFormat.uncapped() = BigDecimalFormat { value -> + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) +} + +/** + * Format for displaying fees. + * If the fee is less than the threshold, it will be displayed as a fixed value "<0.000001 BTC", " + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + if (value.lessThanFeeCryptoThreshold()) { + buildString { + append(CAN_BE_LOWER_SIGN) + append( + formatter + .format(CRYPTO_FEE_FORMAT_THRESHOLD) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + addStartSpace = true, + ), + ) + } + } else { + buildString { + if (canBeLower) { + append(CAN_BE_LOWER_SIGN) + } + append( + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + addStartSpace = canBeLower, + ), + ) + } + } +} + +// == Helpers == + +private fun BigDecimal.isMoreThanThreshold() = this > FORMAT_THRESHOLD + +private fun BigDecimal.lessThanFeeCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD + +private val usdCurrency = Currency.getInstance(Locale.US) + +// Replaces fiat currency symbol with crypto currency symbol +// with respect to the position of the symbol and whitespace +internal fun String.replaceFiatSymbolWithCrypto( + fiatCurrencySymbol: String, + cryptoCurrencySymbol: String, + addStartSpace: Boolean = false, +): String { + val str = this + if (str.isEmpty()) return str + + return buildString { + when { + str.endsWith(fiatCurrencySymbol) -> { + val withoutSymbol = str.dropLast(fiatCurrencySymbol.length) + val last = withoutSymbol.lastOrNull() ?: return cryptoCurrencySymbol + + append(withoutSymbol) + + if (last.isNotWhitespace()) { + append(CURRENCY_SPACE) + } + + append(cryptoCurrencySymbol) + } + str.startsWith(fiatCurrencySymbol) -> { + if (addStartSpace) { + append(CURRENCY_SPACE) + } + + append(cryptoCurrencySymbol) + + val withoutSymbol = str.drop(fiatCurrencySymbol.length) + val first = withoutSymbol.firstOrNull() + ?: return cryptoCurrencySymbol + + if (first.isNotWhitespace()) { + append(CURRENCY_SPACE) + } + + append(withoutSymbol) + } + else -> append(str) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt new file mode 100644 index 0000000000..4822754075 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -0,0 +1,144 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN +import com.tangem.utils.StringsSigns.TILDE_SIGN +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Locale + +open class BigDecimalFiatFormat( + val fiatCurrencyCode: String, + val fiatCurrencySymbol: String, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormat { + override fun invoke(p1: BigDecimal): String = error("") +} + +// == Initializers == + +fun BigDecimalFormatScope.fiat( + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), +): BigDecimalFiatFormat { + return BigDecimalFiatFormat( + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) +} + +// == Formatters == + +/** + * Formats fiat amount with default precision. + */ +fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat { value -> + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + if (value.isLessThanThreshold()) { + buildString { + append(CAN_BE_LOWER_SIGN) + append( + formatter.format(FIAT_FORMAT_THRESHOLD) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol), + ) + } + } else { + formatter.format(value) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + } +} + +/** + * Formats fiat amount with default precision and adds tilde sign + */ +fun BigDecimalFiatFormat.approximateAmount(): BigDecimalFormat = BigDecimalFormat { value -> + val formattedAmount = defaultAmount()(value) + + if (value.isLessThanThreshold()) { + formattedAmount + } else { + buildString { + append(TILDE_SIGN) + append(formattedAmount) + } + } +} + +/** + * Formats fiat amount with extended precision. + */ +fun BigDecimalFiatFormat.uncapped(): BigDecimalFormat = BigDecimalFormat { value -> + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + + val digits = if (value.isLessThanThreshold()) { + FIAT_MARKET_EXTENDED_DIGITS + } else { + FIAT_MARKET_DEFAULT_DIGITS + } + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = digits + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(value) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) +} + +/** + * Formats fiat price with precision calculated based on the value. + * @see getFiatPriceAmountWithScale + */ +fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value -> + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + + val (priceAmount, finalScale) = getFiatPriceAmountWithScale(value = value) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = finalScale + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(priceAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) +} + +// == Helpers == + +private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD + +private fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { + return if (value < BigDecimal.ONE) { + val leadingZeroes = value.scale() - value.precision() + val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES + + val amount = value + .setScale(scale, RoundingMode.HALF_UP) + .stripTrailingZeros() + + amount to amount.scale() + } else { + value to FIAT_MARKET_DEFAULT_DIGITS + } +} + +// == Constants == + +private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") +private const val FIAT_MARKET_DEFAULT_DIGITS = 2 +private const val FIAT_MARKET_EXTENDED_DIGITS = 6 +private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt new file mode 100644 index 0000000000..c685db7f1d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt @@ -0,0 +1,29 @@ +package com.tangem.core.ui.format.bigdecimal + +import java.math.BigDecimal + +interface BigDecimalFormatScope { + companion object { val Empty = object : BigDecimalFormatScope {} } +} + +fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope + +inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String { + return BigDecimalFormatScope.Empty.block()(this) +} + +inline fun BigDecimal?.format( + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, + block: BigDecimalFormatScope.() -> BigDecimalFormat, +): String { + if (this == null) return fallbackString + return BigDecimalFormatScope.Empty.block()(this) +} + +fun BigDecimal?.format( + format: BigDecimalFormat, + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, +): String { + if (this == null) return fallbackString + return format(this) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatConstants.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatConstants.kt new file mode 100644 index 0000000000..59835ab84c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatConstants.kt @@ -0,0 +1,20 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.utils.StringsSigns.LOWER_SIGN +import java.math.BigDecimal +import java.util.Currency +import java.util.Locale + +object BigDecimalFormatConstants { + + const val EMPTY_BALANCE_SIGN = DASH_SIGN + const val CAN_BE_LOWER_SIGN = LOWER_SIGN + val FORMAT_THRESHOLD = BigDecimal("0.01") + + const val CURRENCY_SPACE = '\u00a0' + + val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001") + + val usdCurrency: Currency by lazy { Currency.getInstance(Locale.US) } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt new file mode 100644 index 0000000000..b7019d1d3c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt @@ -0,0 +1,39 @@ +package com.tangem.core.ui.format.bigdecimal + +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Locale + +class BigDecimalPercentFormat( + val withoutSign: Boolean = true, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormat { + override fun invoke(value: BigDecimal): String = default()(value) +} + +// == Initializers == + +fun BigDecimalFormatScope.percent( + withoutSign: Boolean = true, + locale: Locale = Locale.getDefault(), +): BigDecimalPercentFormat { + return BigDecimalPercentFormat( + withoutSign = withoutSign, + locale = locale, + ) +} + +// == Formatters == + +private fun BigDecimalPercentFormat.default(): BigDecimalFormat = BigDecimalFormat { value -> + val formatter = NumberFormat.getPercentInstance(locale).apply { + maximumFractionDigits = 2 + minimumFractionDigits = 2 + roundingMode = RoundingMode.HALF_UP + } + + val valueToFormat = if (withoutSign) value.abs() else value + + formatter.format(valueToFormat) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/Utils.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/Utils.kt new file mode 100644 index 0000000000..94509b276f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/Utils.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.format.bigdecimal + +import java.util.Currency + +fun getJavaCurrencyByCode(code: String): Currency { + return runCatching { Currency.getInstance(code) } + .getOrElse { e -> + // Currency code is not valid ISO 4217 code + if (e is IllegalArgumentException) { + BigDecimalFormatConstants.usdCurrency + } else { + throw e + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 4c44809cfa..fd6a353bb1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -1,20 +1,17 @@ package com.tangem.core.ui.utils -import android.icu.text.CompactDecimalFormat import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.LOWER_SIGN import com.tangem.utils.StringsSigns.TILDE_SIGN -import com.tangem.utils.extensions.isNotWhitespace -import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode -import java.text.DecimalFormat import java.text.NumberFormat import java.util.Currency import java.util.Locale @Suppress("LargeClass") +@Deprecated("Use BigDecimal.format") object BigDecimalFormatter { const val EMPTY_BALANCE_SIGN = DASH_SIGN @@ -30,10 +27,7 @@ object BigDecimalFormatter { private val usdCurrency = Currency.getInstance("USD") - @Deprecated( - "Use formatCryptoAmount2", - replaceWith = ReplaceWith("formatCryptoAmount2"), - ) + @Deprecated("Use BigDecimal.format") fun formatCryptoAmount( cryptoAmount: BigDecimal?, cryptoCurrency: String, @@ -58,30 +52,7 @@ object BigDecimalFormatter { } } - // Migrate to this method from formatCryptoAmount ([REDACTED_TASK_KEY]) - fun formatCryptoAmount2( - cryptoAmount: BigDecimal?, - cryptoCurrency: String, - decimals: Int, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = usdCurrency - maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(cryptoAmount) - .replaceFiatSymbolWithCrypto( - fiatCurrencySymbol = usdCurrency.symbol, - cryptoCurrencySymbol = cryptoCurrency, - ) - } - + @Deprecated("Use BigDecimal.format") fun formatCryptoAmountShorted( cryptoAmount: BigDecimal?, cryptoCurrency: String, @@ -115,6 +86,7 @@ object BigDecimalFormatter { } } + @Deprecated("Use BigDecimal.format") fun formatCryptoAmountUncapped( cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency, @@ -138,6 +110,7 @@ object BigDecimalFormatter { } } + @Deprecated("Use BigDecimal.format") fun formatCryptoFeeAmount( cryptoAmount: BigDecimal?, cryptoCurrency: String, @@ -177,6 +150,7 @@ object BigDecimalFormatter { } } + @Deprecated("Use BigDecimal.format") fun formatCryptoAmount( cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency, @@ -185,6 +159,7 @@ object BigDecimalFormatter { return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals, locale) } + @Deprecated("Use BigDecimal.format") fun formatFiatAmount( fiatAmount: BigDecimal?, fiatCurrencyCode: String, @@ -226,6 +201,7 @@ object BigDecimalFormatter { } } + @Deprecated("Use BigDecimal.format") fun formatFiatAmountUncapped( fiatAmount: BigDecimal?, fiatCurrencyCode: String, @@ -251,6 +227,7 @@ object BigDecimalFormatter { .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } + @Deprecated("Use BigDecimal.format") fun formatFiatPriceUncapped( fiatAmount: BigDecimal?, fiatCurrencyCode: String, @@ -273,6 +250,7 @@ object BigDecimalFormatter { .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } + @Deprecated("Use BigDecimal.format") fun getFiatPriceUncappedWithScale(value: BigDecimal): Pair { return if (value < BigDecimal.ONE) { val leadingZeroes = value.scale() - value.precision() @@ -288,26 +266,7 @@ object BigDecimalFormatter { } } - fun formatFiatEditableAmount( - fiatAmount: String?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - - val formatterCurrency = getCurrency(fiatCurrencyCode) - val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - } - val formatter = requireNotNull(numberFormatter as? DecimalFormat) { - Timber.e("NumberFormat is null") - return EMPTY_BALANCE_SIGN - } - return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}" - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - } - + @Deprecated("Use BigDecimal.format") fun formatPercent( percent: BigDecimal, useAbsoluteValue: Boolean, @@ -341,230 +300,6 @@ object BigDecimalFormatter { } } - /** - * Adds a proper currency sign for the provided formatted [amount] - * ex. '10.0k" -> "$10.0k", "string" -> "$string" - */ - private fun addCurrencySymbolToStringAmount( - amount: String, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - val sampleAmount = BigDecimal.TEN - val currency = getCurrency(fiatCurrencyCode) - - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - maximumFractionDigits = 0 - minimumFractionDigits = 0 - this.currency = currency - } - - val formatted = formatter.format(sampleAmount) - .replace(currency.getSymbol(locale), fiatCurrencySymbol) - .replace(sampleAmount.toString(), amount) - - return formatted - } - - /** - * Adds a proper currency sign for the provided formatted [amount] - * ex. '10.0k" -> "ETH 10.0k", "string" -> "ETH string" - */ - private fun addCryptoCurrencySymbolToStringAmount( - amount: String, - cryptoCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - val sampleAmount = BigDecimal.TEN - - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - maximumFractionDigits = 0 - minimumFractionDigits = 0 - currency = usdCurrency - } - - val formatted = formatter.format(sampleAmount) - .replace(sampleAmount.toString(), amount) - - return formatted.replaceFiatSymbolWithCrypto( - fiatCurrencySymbol = usdCurrency.symbol, - cryptoCurrencySymbol = cryptoCurrencySymbol, - ) - } - - /** - * "123456.6" -> "$123.457K" - * "12345.6" -> "$123.046K" - * Negative amount is not supported - * @param threeDigitsMethod if true, will format the amount always with 3 significant digits - * @param scale the number of digits to the right of the decimal point - */ - @Suppress("MagicNumber") - fun formatCompactFiatAmount( - amount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - threeDigitsMethod: Boolean = false, - scale: Int = 0, - locale: Locale = Locale.getDefault(), - ): String { - if (amount == null) return EMPTY_BALANCE_SIGN - - if (amount < BigDecimal.ONE) { - return formatFiatPriceUncapped( - fiatAmount = amount, - fiatCurrencyCode = fiatCurrencyCode, - fiatCurrencySymbol = fiatCurrencySymbol, - locale = locale, - ) - } - - val rawAmount = formatCompactAmount( - amount = amount, - locale = locale, - threeDigitsMethod = threeDigitsMethod, - scale = scale, - ) - - return addCurrencySymbolToStringAmount( - amount = rawAmount, - fiatCurrencyCode = fiatCurrencyCode, - fiatCurrencySymbol = fiatCurrencySymbol, - locale = locale, - ) - } - - /** - * "123456.6" -> "ETH 123.457K" - * "12345.6" -> "123.046K ETH" - * Negative amount is not supported - * @param threeDigitsMethod if true, will format the amount always with 3 significant digits - * @param scale the number of digits to the right of the decimal point - */ - fun formatCompactCryptoAmount( - amount: BigDecimal?, - cryptoCurrencySymbol: String, - threeDigitsMethod: Boolean = false, - decimals: Int = 0, - locale: Locale = Locale.getDefault(), - ): String { - if (amount == null) return EMPTY_BALANCE_SIGN - - if (amount < BigDecimal.ONE) { - return formatCryptoAmount2( - cryptoAmount = amount, - cryptoCurrency = cryptoCurrencySymbol, - decimals = decimals, - locale = locale, - ) - } - - val rawAmount = formatCompactAmount( - amount = amount, - locale = locale, - threeDigitsMethod = threeDigitsMethod, - scale = decimals, - ) - - return addCryptoCurrencySymbolToStringAmount( - amount = rawAmount, - cryptoCurrencySymbol = cryptoCurrencySymbol, - locale = locale, - ) - } - - /** - * "123456.6" -> "123.457K" - * "12345.6" -> "123.046K" - * Negative amount is not supported - * @param threeDigitsMethod if true, will format the amount always with 3 significant digits - * @param scale the number of digits to the right of the decimal point - */ - @Suppress("MagicNumber") - fun formatCompactAmount( - amount: BigDecimal, - locale: Locale = Locale.getDefault(), - threeDigitsMethod: Boolean = false, - scale: Int = 0, - ): String { - if (threeDigitsMethod) { - val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP) - val digitsCount = scaledAmount.toString().count() - val digitsToFormat = 6 - when (digitsCount % 3) { - 0 -> 0 - 1 -> 2 - else -> 1 - } - - val formatter = CompactDecimalFormat.getInstance( - locale, - CompactDecimalFormat.CompactStyle.SHORT, - ).apply { - minimumSignificantDigits = 4 - maximumSignificantDigits = digitsToFormat - } - - return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP)) - } else { - val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP) - val digitsCount = scaledAmount.toString().count() - val digitsToFormat = 5 - when (digitsCount % 3) { - 0 -> 0 - 1 -> 2 - else -> 1 - } - - val formatter = CompactDecimalFormat.getInstance( - locale, - CompactDecimalFormat.CompactStyle.SHORT, - ).apply { - minimumSignificantDigits = 2 - maximumSignificantDigits = digitsToFormat - } - - return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP)) - } - } - - // Replaces fiat currency symbol with crypto currency symbol - // with respect to the position of the symbol and whitespace - private fun String.replaceFiatSymbolWithCrypto(fiatCurrencySymbol: String, cryptoCurrencySymbol: String): String { - val str = this - if (str.isEmpty()) return str - - return buildString { - when { - str.endsWith(fiatCurrencySymbol) -> { - val withoutSymbol = str.dropLast(fiatCurrencySymbol.length) - val last = withoutSymbol.lastOrNull() ?: return cryptoCurrencySymbol - - append(withoutSymbol) - - if (last.isNotWhitespace()) { - append("\u2009") - } - - append(cryptoCurrencySymbol) - } - str.startsWith(fiatCurrencySymbol) -> { - append(cryptoCurrencySymbol) - - val withoutSymbol = str.drop(fiatCurrencySymbol.length) - val first = withoutSymbol.firstOrNull() - ?: return cryptoCurrencySymbol - - if (first.isNotWhitespace()) { - append("\u2009") - } - - append(withoutSymbol) - } - else -> append(str) - } - } - } - private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD private fun BigDecimal.checkCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt new file mode 100644 index 0000000000..cc6fa5e40e --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt @@ -0,0 +1,373 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.google.common.truth.Truth +import org.junit.Test +import java.math.BigDecimal +import java.util.Locale + +internal class BigDecimalCryptoFormatTest { + + private val testLocale = Locale.US + private val testLocale2 = Locale.GERMANY + private val symbol = "BTC" + + // === defaultAmount() === + + @Test + fun `defaultAmount (usually used as a user balance)`() { + val testValue = BigDecimal("0.123456789999") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 8, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12345679".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `defaultAmount (usually used as a user balance) alter locale`() { + val testValue = BigDecimal("0.123456789999") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 8, + locale = testLocale2, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0,12345679".addSymbolWithSpaceRight(symbol)) + } + + @Test + fun `defaultAmount decimals more than 8`() { + val testValue = BigDecimal("0.123456789999") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12345679".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `defaultAmount decimals more than 8 (short value)`() { + val testValue = BigDecimal("0.12345") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12345".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `defaultAmount decimals minimal (short value)`() { + val testValue = BigDecimal("0.12345") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 2, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `defaultAmount less than 2 decimals`() { + val testValue = BigDecimal("0.12345") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 0, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `defaultAmount grouping`() { + val testValue = BigDecimal("12345678.11") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 0, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("12,345,678.11".addSymbolWithSpaceLeft(symbol)) + } + + // === shorted() === + + @Test + fun `shorted amount smoke`() { + val testValue = BigDecimal("50000.126123") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 8, + locale = testLocale, + ).shorted() + } + + Truth.assertThat(formatted) + .isEqualTo("50,000.13".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `shorted amount decimals less than 2 grouping`() { + val testValue = BigDecimal("50000.126123") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 1, + locale = testLocale, + ).shorted() + } + + Truth.assertThat(formatted) + .isEqualTo("50,000.13".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `shorted amount less than threshold`() { + val testValue = BigDecimal("0.0034567899") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 4, + locale = testLocale, + ).shorted() + } + + Truth.assertThat(formatted) + .isEqualTo("0.0034".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `shorted amount less than threshold, more decimals`() { + val testValue = BigDecimal("0.00345678") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 8, + locale = testLocale, + ).shorted() + } + + Truth.assertThat(formatted) + .isEqualTo("0.003456".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `shorted amount diff locale half up`() { + val testValue = BigDecimal("50000.126123") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 8, + locale = testLocale2, + ).shorted() + } + + Truth.assertThat(formatted) + .isEqualTo("50.000,13".addSymbolWithSpaceRight(symbol)) + } + + // === uncapped() === + + @Test + fun `uncapped amount`() { + val testValue = BigDecimal("50000.123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("50,000.1234123412".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `uncapped amount diff locale`() { + val testValue = BigDecimal("50000.123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale2, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("50.000,1234123412".addSymbolWithSpaceRight(symbol)) + } + + @Test + fun `uncapped amount half up`() { + val testValue = BigDecimal("50000.12341234125") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale2, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("50.000,1234123413".addSymbolWithSpaceRight(symbol)) + } + + @Test + fun `uncapped amount min decimals`() { + val testValue = BigDecimal("50000.12341234125") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 1, + locale = testLocale2, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("50.000,12".addSymbolWithSpaceRight(symbol)) + } + + // === fee === + + @Test + fun `fee amount`() { + val testValue = BigDecimal("0.000123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).fee() + } + + Truth.assertThat(formatted) + .isEqualTo("0.000123".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `fee amount diff locale`() { + val testValue = BigDecimal("0.000123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale2, + ).fee() + } + + Truth.assertThat(formatted) + .isEqualTo("0,000123".addSymbolWithSpaceRight(symbol)) + } + + @Test + fun `fee amount canBeLower true`() { + val testValue = BigDecimal("0.000123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).fee(canBeLower = true) + } + + Truth.assertThat(formatted) + .isEqualTo("<" + CURRENCY_SPACE_FOR_TESTS + "0.000123".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `fee amount canBeLower true (diff locale)`() { + val testValue = BigDecimal("0.000123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale2, + ).fee(canBeLower = true) + } + + Truth.assertThat(formatted) + .isEqualTo("<" + "0,000123".addSymbolWithSpaceRight(symbol)) + } + + @Test + fun `fee amount lee than threshold`() { + val testValue = BigDecimal("0.0000001234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).fee() + } + + Truth.assertThat(formatted) + .isEqualTo("<" + CURRENCY_SPACE_FOR_TESTS + "0.000001".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `fee amount min decimals half up`() { + val testValue = BigDecimal("0.125412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 1, + locale = testLocale, + ).fee() + } + + Truth.assertThat(formatted) + .isEqualTo("0.13".addSymbolWithSpaceLeft(symbol)) + } +} \ No newline at end of file diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt new file mode 100644 index 0000000000..d5c2cb310e --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt @@ -0,0 +1,297 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.google.common.truth.Truth +import org.junit.Test +import java.math.BigDecimal +import java.util.Locale + +internal class BigDecimalFiatFormatTest { + + val testLocale = Locale.US + val testLocale2 = Locale.GERMANY + + val usdCurrencyCode = "USD" + val usdSymbol = "$" + + private fun String.addUsdSymbolLeft() = usdSymbol + this + + // === defaultAmount() === + + @Test + fun `defaultAmount smoke`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("1,234.12".addUsdSymbolLeft()) + } + + @Test + fun `defaultAmount half up`() { + val testValue = BigDecimal("1234.125") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("1,234.13".addUsdSymbolLeft()) + } + + @Test + fun `defaultAmount diff locale`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale2, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("1.234,12".addSymbolWithSpaceRight(usdSymbol)) + } + + @Test + fun `defaultAmount less threshold`() { + val testValue = BigDecimal("0.002234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("<" + "0.01".addUsdSymbolLeft()) + } + + @Test + fun `defaultAmount less threshold diff locale`() { + val testValue = BigDecimal("0.002234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale2, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("<" + "0,01".addSymbolWithSpaceRight(usdSymbol)) + } + + // === approximateAmount() === + + @Test + fun `approximateAmount smoke`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).approximateAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("~" + "1,234.12".addUsdSymbolLeft()) + } + + @Test + fun `approximateAmount half up`() { + val testValue = BigDecimal("1234.125") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).approximateAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("~" + "1,234.13".addUsdSymbolLeft()) + } + + @Test + fun `approximateAmount diff locale`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale2, + ).approximateAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("~" + "1.234,12".addSymbolWithSpaceRight(usdSymbol)) + } + + @Test + fun `approximateAmount less threshold`() { + val testValue = BigDecimal("0.002234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).approximateAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("<" + "0.01".addUsdSymbolLeft()) + } + + // === uncapped() === + + @Test + fun `uncapped smoke`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("1,234.12".addUsdSymbolLeft()) + } + + @Test + fun `uncapped less threshold`() { + val testValue = BigDecimal("0.00121") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("0.00121".addUsdSymbolLeft()) + } + + @Test + fun `uncapped decimals overflow`() { + val testValue = BigDecimal("0.00123412341234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("0.001234".addUsdSymbolLeft()) + } + + // === price() === + + @Test + fun `price smoke`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("1,234.12".addUsdSymbolLeft()) + } + + @Test + fun `price diff locale`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale2, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("1.234,12".addSymbolWithSpaceRight(usdSymbol)) + } + + @Test + fun `price less threshold`() { + val testValue = BigDecimal("0.99987") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("0.9999".addUsdSymbolLeft()) + } + + @Test + fun `price less threshold more decimals strip zeros`() { + val testValue = BigDecimal("0.0000123000") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("0.0000123".addUsdSymbolLeft()) + } + + @Test + fun `price less threshold too much decimals strip zeros`() { + val testValue = BigDecimal("0.000000000000000000001230001234000") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("0.00000000000000000000123".addUsdSymbolLeft()) + } +} \ No newline at end of file diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatTest.kt new file mode 100644 index 0000000000..63061a9116 --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatTest.kt @@ -0,0 +1,29 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.google.common.truth.Truth +import org.junit.Test +import java.math.BigDecimal + +internal class BigDecimalFormatTest { + + @Test + fun smoke() { + val value = BigDecimal("1234") + val bgformat = BigDecimalFormat { bg -> + bg.toPlainString() + "!" + } + val expected = "1234!" + + Truth.assertThat( + value.format(bgformat), + ).isEqualTo(expected) + + Truth.assertThat( + value.format { bgformat }, + ).isEqualTo(expected) + + Truth.assertThat( + null.format(fallbackString = "!") { bgformat }, + ).isEqualTo("!") + } +} \ No newline at end of file diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormatTest.kt new file mode 100644 index 0000000000..3d3be8c25f --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormatTest.kt @@ -0,0 +1,70 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.google.common.truth.Truth +import org.junit.Test +import java.math.BigDecimal +import java.util.Locale + +internal class BigDecimalPercentFormatTest { + + val testLocale = Locale.US + val testLocale2 = Locale.GERMANY + + @Test + fun smoke() { + val value = BigDecimal("00.34") + + val formatted = value.format { + percent(locale = testLocale) + } + + Truth.assertThat(formatted).isEqualTo("34.00%") + } + + @Test + fun negative() { + val value = BigDecimal("00.34").negate() + + val formatted = value.format { + percent(locale = testLocale) + } + + Truth.assertThat(formatted).isEqualTo("34.00%") + } + + @Test + fun `negative with sign`() { + val value = BigDecimal("00.34").negate() + + val formatted = value.format { + percent( + withoutSign = false, + locale = testLocale, + ) + } + + Truth.assertThat(formatted).isEqualTo("-34.00%") + } + + @Test + fun `default more decimals half up`() { + val value = BigDecimal("00.345678").negate() + + val formatted = value.format { + percent(locale = testLocale) + } + + Truth.assertThat(formatted).isEqualTo("34.57%") + } + + @Test + fun `default diff locale`() { + val value = BigDecimal("00.345678").negate() + + val formatted = value.format { + percent(locale = testLocale2) + } + + Truth.assertThat(formatted).isEqualTo("34,57".addSymbolWithSpaceRight("%")) + } +} \ No newline at end of file diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/TestUtils.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/TestUtils.kt new file mode 100644 index 0000000000..436873335d --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/TestUtils.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.format.bigdecimal + +internal const val CURRENCY_SPACE_FOR_TESTS = '\u00a0' + +internal fun String.addSymbolWithSpaceRight(symbol: String): String = "$this$CURRENCY_SPACE_FOR_TESTS$symbol" + +internal fun String.addSymbolWithSpaceLeft(symbol: String): String = "$symbol$CURRENCY_SPACE_FOR_TESTS$this" \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt index fb93bd86a2..65273ee0bb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt @@ -3,7 +3,10 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.compact +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.rawCompact import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo @@ -142,14 +145,17 @@ internal class InsightsConverter( private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String { val value = if (isFiatValue) { - val currency = appCurrency() - BigDecimalFormatter.formatCompactFiatAmount( - amount = this.abs(), - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ) + this.abs().format { + val currency = appCurrency() + fiat( + fiatCurrencyCode = currency.code, + fiatCurrencySymbol = currency.symbol, + ).compact() + } } else { - BigDecimalFormatter.formatCompactAmount(amount = this.abs()) + this.abs().format { + rawCompact() + } } return when { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt index 48b3217b3d..c91039839b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt @@ -2,7 +2,10 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.compact +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent @@ -129,18 +132,21 @@ internal class MetricsConverter( if (this == null) return StringsSigns.DASH_SIGN return if (crypto) { - BigDecimalFormatter.formatCompactCryptoAmount( - amount = this, - cryptoCurrencySymbol = tokenSymbol, - ) + format { + crypto( + symbol = tokenSymbol, + decimals = 2, + ).compact() + } } else { val currency = appCurrency() - BigDecimalFormatter.formatCompactFiatAmount( - amount = this, - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ) + format { + fiat( + fiatCurrencyCode = currency.code, + fiatCurrencySymbol = currency.symbol, + ).compact() + } } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index b59acbc16d..f4d62e02de 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -5,6 +5,9 @@ import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.sorted import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.format.bigdecimal.compact +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket @@ -71,12 +74,14 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getMarketCap(): String? { val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null - return BigDecimalFormatter.formatCompactFiatAmount( - amount = value, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - threeDigitsMethod = true, - ) + return value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).compact( + threeDigitsMethod = true, + ) + } } private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { From 29915a82242dc1681c11cfd17bafc756e12ccd43 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Oct 2024 20:22:54 +0500 Subject: [PATCH 02/18] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../data/staking/DefaultStakingRepository.kt | 18 +- .../domain/staking/model/stakekit/Yield.kt | 7 +- .../action/StakingActionCommonType.kt | 14 +- .../analytics/utils/StakingAnalyticSender.kt | 19 +- .../state/StakingStateController.kt | 9 +- .../presentation/state/StakingStateRouter.kt | 28 +- .../impl/presentation/state/StakingUiState.kt | 24 +- .../impl/presentation/state/ValidatorState.kt | 34 --- .../helpers/StakingFeeTransactionLoader.kt | 16 +- .../state/helpers/StakingTransactionSender.kt | 21 +- .../ConfirmationStatePreviewData.kt | 49 ---- .../previewdata/ValidatorStatePreviewData.kt | 60 +++++ .../state/stub/StakingClickIntentsStub.kt | 14 +- .../ActionTypeActiveStakeTransformer.kt | 33 --- .../AddStakingNotificationsTransformer.kt | 21 +- .../SetActionToExecuteTransformer.kt | 25 -- .../SetButtonsStateTransformer.kt | 33 +-- .../SetConfirmationStateAssentTransformer.kt | 10 - ...etConfirmationStateCompletedTransformer.kt | 7 +- .../SetConfirmationStateEmptyTransformer.kt | 16 ++ ...tConfirmationStateInProgressTransformer.kt | 1 - .../SetConfirmationStateInitTransformer.kt | 70 +++++ .../SetConfirmationStateLoadingTransformer.kt | 29 +- ...ConfirmationStateResetAssentTransformer.kt | 1 - .../SetInitialDataStateTransformer.kt | 25 +- .../state/transformers/SetTitleTransformer.kt | 10 +- .../AmountRequirementStateTransformer.kt | 2 +- ...firmationStateAssentApprovalTransformer.kt | 1 - .../ShowApprovalBottomSheetTransformer.kt | 3 +- .../ValidatorSelectChangeTransformer.kt | 40 ++- .../state/utils/StakingPendingActionUtils.kt | 39 ++- .../ui/StakingConfirmationContent.kt | 21 +- .../impl/presentation/ui/StakingScreen.kt | 26 +- .../ui/StakingValidatorListContent.kt | 16 +- .../presentation/ui/block/ValidatorBlock.kt | 39 +-- .../viewmodel/StakingClickIntents.kt | 14 +- .../viewmodel/StakingViewModel.kt | 254 +++++++++--------- .../com/tangem/lib/crypto/BlockchainUtils.kt | 5 + 39 files changed, 531 insertions(+), 524 deletions(-) delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ValidatorStatePreviewData.kt delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateEmptyTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 46365c5cc0..a0beef5668 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -735,6 +735,7 @@ Staking is currently unavailable due to network conditions. Please try again later. Staking in the %1$s network with a new validator will automatically transfer all previously staked funds to this validator Reinvests your earned rewards in your staked amount, increasing potential earnings. + Restake lets you move your funds from one validator to another without the need to unstake Unlock your money to withdraw it from staking process. Unlocking takes %s. Your funds will be available for use after the 21-day unbonding period. Reward will be withdrawn along with your unstaking funds. Your funds will be available for use after the %s unbonding period. diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index c3161be225..729477a5cc 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -151,7 +151,7 @@ internal class DefaultStakingRepository( val yield = getYield(cryptoCurrencyId, symbol) StakingEntryInfo( - apr = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr), + apr = requireNotNull(yield.preferredValidators.maxByOrNull { it.apr.orZero() }?.apr), rewardSchedule = yield.metadata.rewardSchedule, tokenSymbol = yield.token.symbol, ) @@ -207,23 +207,21 @@ internal class DefaultStakingRepository( ): StakingAction { return withContext(dispatchers.io) { val response = when (params.actionCommonType) { - StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction( + StakingActionCommonType.Enter -> stakeKitApi.createEnterAction( createActionRequestBody( userWalletId, network, params, ), ) - StakingActionCommonType.EXIT -> stakeKitApi.createExitAction( + StakingActionCommonType.Exit -> stakeKitApi.createExitAction( createActionRequestBody( userWalletId, network, params, ), ) - StakingActionCommonType.PENDING_OTHER, - StakingActionCommonType.PENDING_REWARDS, - -> stakeKitApi.createPendingAction( + is StakingActionCommonType.Pending -> stakeKitApi.createPendingAction( createPendingActionRequestBody(params), ) } @@ -239,23 +237,21 @@ internal class DefaultStakingRepository( ): StakingGasEstimate { return withContext(dispatchers.io) { val gasEstimateDTO = when (params.actionCommonType) { - StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter( + StakingActionCommonType.Enter -> stakeKitApi.estimateGasOnEnter( createActionRequestBody( userWalletId, network, params, ), ) - StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit( + StakingActionCommonType.Exit -> stakeKitApi.estimateGasOnExit( createActionRequestBody( userWalletId, network, params, ), ) - StakingActionCommonType.PENDING_REWARDS, - StakingActionCommonType.PENDING_OTHER, - -> stakeKitApi.estimateGasOnPending( + is StakingActionCommonType.Pending -> stakeKitApi.estimateGasOnPending( createPendingActionRequestBody(params), ) } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index a2a0a6b7fe..9f201fb81e 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -18,6 +18,11 @@ data class Yield( val isAvailable: Boolean, ) { + val preferredValidators: List + get() = validators.filter { it.preferred } + + fun getCurrentToken(rawCurrencyId: String?) = tokens.firstOrNull { rawCurrencyId == it.coinGeckoId } ?: token + @Serializable data class Status( val enter: Boolean, @@ -130,8 +135,6 @@ data class Yield( APR, // simple rate UNKNOWN, } - - fun getCurrentToken(rawCurrencyId: String?) = tokens.firstOrNull { rawCurrencyId == it.coinGeckoId } ?: token } @Serializable diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt index 5a59ba211c..0e2d7abe32 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt @@ -1,8 +1,12 @@ package com.tangem.domain.staking.model.stakekit.action -enum class StakingActionCommonType { - ENTER, - EXIT, - PENDING_REWARDS, - PENDING_OTHER, +sealed class StakingActionCommonType { + + data object Enter : StakingActionCommonType() + data object Exit : StakingActionCommonType() + sealed class Pending : StakingActionCommonType() { + data object Restake : Pending() + data object Rewards : Pending() + data object Other : Pending() + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index 0b3f4fcd35..7fcb116702 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -33,10 +33,10 @@ internal class StakingAnalyticSender( fun confirmationScreen(value: StakingUiState) { val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorState = value.validatorState as? StakingStates.ValidatorState.Data val validatorName = validatorState?.chosenValidator?.name ?: return - if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) return + if (confirmationState?.innerState == InnerConfirmationStakingState.COMPLETED) return analyticsEventHandler.send( StakingAnalyticsEvents.ConfirmationScreenOpened( @@ -55,6 +55,7 @@ internal class StakingAnalyticSender( StakingStep.Amount -> StakeScreenSource.Amount StakingStep.Confirmation -> StakeScreenSource.Confirmation StakingStep.Validators, + StakingStep.RestakeValidator, StakingStep.RewardsValidators, -> StakeScreenSource.Validators }, @@ -78,8 +79,7 @@ internal class StakingAnalyticSender( } fun sendTransactionStakingAnalytics(value: StakingUiState) { - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorState = value.validatorState as? StakingStates.ValidatorState.Data val validatorName = validatorState?.chosenValidator?.name ?: return analyticsEventHandler.send( @@ -102,8 +102,7 @@ internal class StakingAnalyticSender( } fun sendTransactionStakingClickedAnalytics(value: StakingUiState) { - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorState = value.validatorState as? StakingStates.ValidatorState.Data val validatorName = validatorState?.chosenValidator?.name ?: return analyticsEventHandler.send( @@ -119,11 +118,9 @@ internal class StakingAnalyticSender( val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data return when (value.actionType) { - StakingActionCommonType.ENTER -> StakingActionType.STAKE - StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE - StakingActionCommonType.PENDING_REWARDS, - StakingActionCommonType.PENDING_OTHER, - -> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN + StakingActionCommonType.Enter -> StakingActionType.STAKE + StakingActionCommonType.Exit -> StakingActionType.UNSTAKE + is StakingActionCommonType.Pending -> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 0b04c60e11..dd8919a82c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -45,6 +45,12 @@ internal class StakingStateController @Inject constructor( mutableUiState.update(function = titleTransformer::transform) } + fun updateAll(vararg transformer: Transformer) { + transformer.forEach { mutableUiState.update(function = it::transform) } + mutableUiState.update(function = buttonsTransformer::transform) + mutableUiState.update(function = titleTransformer::transform) + } + fun clear() { mutableUiState.update { getInitialState() } mutableUiState.update(function = buttonsTransformer::transform) @@ -72,12 +78,13 @@ internal class StakingStateController @Inject constructor( currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = AmountState.Empty(), + validatorState = StakingStates.ValidatorState.Empty(), rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), confirmationState = StakingStates.ConfirmationState.Empty(), isBalanceHidden = false, event = consumedEvent(), bottomSheetConfig = null, - actionType = StakingActionCommonType.ENTER, + actionType = StakingActionCommonType.Enter, buttonsState = NavigationButtonsState.Empty, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 69cca72194..e0e42070ac 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -23,12 +23,14 @@ internal class StakingStateRouter( fun onNextClick() { when (stateController.value.currentStep) { StakingStep.InitialInfo -> when (stateController.value.actionType) { - StakingActionCommonType.ENTER -> showAmount() - StakingActionCommonType.PENDING_OTHER, - StakingActionCommonType.EXIT, + StakingActionCommonType.Enter -> showAmount() + StakingActionCommonType.Pending.Other, + StakingActionCommonType.Exit, + StakingActionCommonType.Pending.Rewards, -> showConfirmation() - StakingActionCommonType.PENDING_REWARDS -> showRewardsValidators() + StakingActionCommonType.Pending.Restake -> showRestakeValidators() } + StakingStep.RestakeValidator, StakingStep.RewardsValidators, StakingStep.Validators, StakingStep.Amount, @@ -41,25 +43,31 @@ internal class StakingStateRouter( val uiState = stateController.uiState.value when (uiState.currentStep) { StakingStep.InitialInfo -> onBackClick() - StakingStep.Amount -> showInitial() + StakingStep.RestakeValidator, + StakingStep.RewardsValidators, + StakingStep.Amount, + -> showInitial() StakingStep.Confirmation -> { - if (uiState.actionType != StakingActionCommonType.ENTER) { + if (uiState.actionType != StakingActionCommonType.Enter) { showInitial() } else { showAmount() } } StakingStep.Validators -> showConfirmation() - StakingStep.RewardsValidators -> showInitial() } } + fun showValidators() { + stateController.update { it.copy(currentStep = StakingStep.Validators) } + } + private fun showInitial() { analyticSender.initialInfoScreen(stateController.value) stateController.update { it.copy(currentStep = StakingStep.InitialInfo) } } - private fun showRewardsValidators() { + fun showRewardsValidators() { analyticsEventsHandler.send( StakingAnalyticsEvents.RewardScreenOpened(stateController.value.cryptoCurrencySymbol), ) @@ -73,8 +81,8 @@ internal class StakingStateRouter( stateController.update { it.copy(currentStep = StakingStep.Amount) } } - fun showValidators() { - stateController.update { it.copy(currentStep = StakingStep.Validators) } + private fun showRestakeValidators() { + stateController.update { it.copy(currentStep = StakingStep.RestakeValidator) } } private fun showConfirmation() { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 4bc8b84bda..56aa176637 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -11,6 +11,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.domain.staking.model.PendingTransaction import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEvent @@ -34,6 +35,7 @@ internal data class StakingUiState( val amountState: AmountState, val rewardsValidatorsState: StakingStates.RewardsValidatorsState, val confirmationState: StakingStates.ConfirmationState, + val validatorState: StakingStates.ValidatorState, val isBalanceHidden: Boolean, val bottomSheetConfig: TangemBottomSheetConfig?, val actionType: StakingActionCommonType, @@ -45,10 +47,12 @@ internal data class StakingUiState( initialInfoState: StakingStates.InitialInfoState = this.initialInfoState, amountState: AmountState = this.amountState, confirmationState: StakingStates.ConfirmationState = this.confirmationState, + validatorState: StakingStates.ValidatorState = this.validatorState, ): StakingUiState = copy( initialInfoState = initialInfoState, amountState = amountState, confirmationState = confirmationState, + validatorState = validatorState, ) } @@ -85,13 +89,30 @@ internal sealed class StakingStates { ) : RewardsValidatorsState() } + sealed class ValidatorState : StakingStates() { + abstract val isClickable: Boolean + + data class Data( + override val isPrimaryButtonEnabled: Boolean, + override val isClickable: Boolean, + val isVisibleOnConfirmation: Boolean, + val chosenValidator: Yield.Validator, + val activeValidator: Yield.Validator?, + val availableValidators: List, + ) : ValidatorState() + + data class Empty( + override val isClickable: Boolean = false, + override val isPrimaryButtonEnabled: Boolean = false, + ) : ValidatorState() + } + /** Confirmation state */ sealed class ConfirmationState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, val innerState: InnerConfirmationStakingState, val feeState: FeeState, - val validatorState: ValidatorState, val pendingAction: PendingAction?, val pendingActions: ImmutableList?, val notifications: ImmutableList, @@ -112,6 +133,7 @@ enum class StakingStep { InitialInfo, RewardsValidators, Amount, + RestakeValidator, Confirmation, Validators, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt deleted file mode 100644 index 7871149443..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state - -import androidx.compose.runtime.Immutable -import com.tangem.domain.staking.model.stakekit.Yield - -@Immutable -internal sealed class ValidatorState { - - abstract val isClickable: Boolean - - data class Content( - override val isClickable: Boolean, - val chosenValidator: Yield.Validator, - val availableValidators: List, - ) : ValidatorState() - - data object Loading : ValidatorState() { - override val isClickable: Boolean - get() = false - } - - data object Error : ValidatorState() { - override val isClickable: Boolean - get() = false - } - - fun copySealed(isClickable: Boolean): ValidatorState { - return if (this is Content) { - copy(isClickable = isClickable) - } else { - this - } - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt index 33559d6211..2daa181826 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -22,8 +22,7 @@ import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.ValidatorState -import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw +import com.tangem.features.staking.impl.presentation.state.utils.isComposePendingActions import com.tangem.utils.extensions.orZero import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -48,8 +47,6 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( ) { suspend fun getFee( - pendingAction: PendingAction?, - pendingActions: ImmutableList?, onStakingFee: (Fee) -> Unit, onStakingFeeError: (StakingError) -> Unit, onApprovalFee: (TransactionFee) -> Unit, @@ -57,17 +54,20 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( ) { val state = stateController.value val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data - ?: error("No confirmation state") - val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("Illegal state") + val validatorState = state.validatorState as? StakingStates.ValidatorState.Data ?: error("No validator provided") val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided") + val pendingAction = confirmationState.pendingAction + val pendingActions = confirmationState.pendingActions + val validatorAddress = validatorState.chosenValidator.address val approval = stakingApproval as? StakingApproval.Needed - if (approval != null && state.actionType == StakingActionCommonType.ENTER) { + if (approval != null && state.actionType == StakingActionCommonType.Enter) { val allowance = getAllowanceUseCase( userWalletId = userWallet.walletId, cryptoCurrency = cryptoCurrencyStatus.currency, @@ -114,7 +114,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( val sourceAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("No available address") - val gasEstimate = if (isSolanaWithdraw(cryptoCurrencyStatus.currency.network.id.value, pendingActions)) { + val gasEstimate = if (isComposePendingActions(cryptoCurrencyStatus.currency.network.id.value, pendingActions)) { val result = coroutineScope { pendingActions?.map { action -> async { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt index f334c448f4..90fc4e9295 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -21,9 +21,12 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingStateController +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount -import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw +import com.tangem.features.staking.impl.presentation.state.utils.isComposePendingActions import com.tangem.utils.extensions.orZero import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -99,7 +102,7 @@ internal class StakingTransactionSender @AssistedInject constructor( } private fun composePendingTransaction(confirmationState: StakingStates.ConfirmationState.Data): PendingTransaction { - return confirmationState.possiblePendingTransaction ?: composeStakeTransaction(confirmationState) + return confirmationState.possiblePendingTransaction ?: composeStakeTransaction() } private suspend fun getStakingTransactions( @@ -107,11 +110,11 @@ internal class StakingTransactionSender @AssistedInject constructor( confirmationState: StakingStates.ConfirmationState.Data, onConstructError: (StakingError) -> Unit, ) = coroutineScope { - val isAllWithdrawAction = isSolanaWithdraw( + val isComposePendingActions = isComposePendingActions( cryptoCurrencyStatus.currency.network.id.value, confirmationState.pendingActions, ) - if (isAllWithdrawAction) { + if (isComposePendingActions) { confirmationState.pendingActions?.map { action -> async { getStakingTransaction( @@ -172,7 +175,7 @@ internal class StakingTransactionSender @AssistedInject constructor( action: PendingAction? = confirmationState.pendingAction, onConstructError: (StakingError) -> Unit, ): List { - val validatorState = confirmationState.validatorState as? ValidatorState.Content + val validatorState = state.validatorState as? StakingStates.ValidatorState.Data ?: error("No validator provided") val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") @@ -278,7 +281,7 @@ internal class StakingTransactionSender @AssistedInject constructor( private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal { val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") val feeValue = fee.amount.value ?: error("No fee value") - val isEnterAction = stateController.value.actionType == StakingActionCommonType.ENTER + val isEnterAction = stateController.value.actionType == StakingActionCommonType.Enter return checkAndCalculateSubtractedAmount( isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction, @@ -289,7 +292,7 @@ internal class StakingTransactionSender @AssistedInject constructor( ) } - private fun composeStakeTransaction(confirmationState: StakingStates.ConfirmationState.Data): PendingTransaction { + private fun composeStakeTransaction(): PendingTransaction { val state = stateController.value val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data @@ -301,7 +304,7 @@ internal class StakingTransactionSender @AssistedInject constructor( type = BalanceType.STAKED, amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO, rawCurrencyId = cryptoCurrencyStatus.currency.id.rawCurrencyId, - validator = (confirmationState.validatorState as? ValidatorState.Content)?.chosenValidator, + validator = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenValidator, balancesId = yieldBalance?.getBalancesUniqueId() ?: 0, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index d9c7263d8e..016a6a0625 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -7,8 +7,6 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import kotlinx.collections.immutable.persistentListOf @@ -16,48 +14,6 @@ import java.math.BigDecimal internal object ConfirmationStatePreviewData { - private val validatorList = listOf( - Yield.Validator( - address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951", - status = ValidatorStatus.ACTIVE, - name = "Luganodes", - image = "https://assets.stakek.it/validators/luganodes.png", - apr = BigDecimal("0.054823398040640445"), - commission = 0.1, - stakedBalance = "355544384.45009977", - website = "https://luganodes.com/", - votingPower = 0.09778360195377911, - preferred = true, - isStrategicPartner = false, - ), - Yield.Validator( - address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32", - status = ValidatorStatus.ACTIVE, - name = "InfStones", - image = "https://assets.stakek.it/validators/infstones.png", - apr = BigDecimal("0.057786472172836965"), - commission = 0.05, - stakedBalance = "12495684.05643019", - website = "https://infstones.com/", - votingPower = 0.0034366257754399774, - preferred = true, - isStrategicPartner = false, - ), - Yield.Validator( - address = "0xd14a87025109013b0a2354a775cb335f926af65a", - status = ValidatorStatus.ACTIVE, - name = "Kiln", - image = "https://assets.stakek.it/validators/kiln.png", - apr = BigDecimal("0.057786472172836965"), - commission = 0.05, - stakedBalance = "85400369.96393165", - website = "https://infstones.com/", - votingPower = 0.023487238579718264, - preferred = true, - isStrategicPartner = true, - ), - ) - private val fee = Fee.Common( amount = Amount( currencySymbol = "MATIC", @@ -77,11 +33,6 @@ internal object ConfirmationStatePreviewData { isFeeApproximate = false, isFeeConvertibleToFiat = true, ), - validatorState = ValidatorState.Content( - isClickable = true, - chosenValidator = validatorList[0], - availableValidators = validatorList, - ), footerText = stringReference("You stake \$715.11 and will be receiving ~\$35 monthly"), notifications = persistentListOf( StakingNotification.Info.EarnRewards( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ValidatorStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ValidatorStatePreviewData.kt new file mode 100644 index 0000000000..225057c19c --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ValidatorStatePreviewData.kt @@ -0,0 +1,60 @@ +package com.tangem.features.staking.impl.presentation.state.previewdata + +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus +import com.tangem.features.staking.impl.presentation.state.StakingStates +import java.math.BigDecimal + +internal object ValidatorStatePreviewData { + + private val validatorList = listOf( + Yield.Validator( + address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951", + status = ValidatorStatus.ACTIVE, + name = "Luganodes", + image = "https://assets.stakek.it/validators/luganodes.png", + apr = BigDecimal("0.054823398040640445"), + commission = 0.1, + stakedBalance = "355544384.45009977", + website = "https://luganodes.com/", + votingPower = 0.09778360195377911, + preferred = true, + isStrategicPartner = false, + ), + Yield.Validator( + address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32", + status = ValidatorStatus.ACTIVE, + name = "InfStones", + image = "https://assets.stakek.it/validators/infstones.png", + apr = BigDecimal("0.057786472172836965"), + commission = 0.05, + stakedBalance = "12495684.05643019", + website = "https://infstones.com/", + votingPower = 0.0034366257754399774, + preferred = true, + isStrategicPartner = true, + ), + Yield.Validator( + address = "0xd14a87025109013b0a2354a775cb335f926af65a", + status = ValidatorStatus.ACTIVE, + name = "Kiln", + image = "https://assets.stakek.it/validators/kiln.png", + apr = BigDecimal("0.057786472172836965"), + commission = 0.05, + stakedBalance = "85400369.96393165", + website = "https://infstones.com/", + votingPower = 0.023487238579718264, + preferred = true, + isStrategicPartner = false, + ), + ) + + val validatorState = StakingStates.ValidatorState.Data( + availableValidators = validatorList, + chosenValidator = validatorList.first(), + isPrimaryButtonEnabled = true, + activeValidator = null, + isClickable = true, + isVisibleOnConfirmation = true, + ) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 716cab0e93..9da7141f5a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -2,14 +2,11 @@ package com.tangem.features.staking.impl.presentation.state.stub import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents -import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @Suppress("TooManyFunctions") @@ -17,13 +14,7 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} - override fun onNextClick( - actionTypeToOverwrite: StakingActionCommonType?, - pendingAction: PendingAction?, - pendingActions: ImmutableList?, - balanceState: BalanceState?, - ) { - } + override fun onNextClick(balanceState: BalanceState?) {} override fun onActionClick() {} @@ -67,8 +58,7 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onActiveStake(activeStake: BalanceState) {} - override fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) { - } + override fun getFee() {} override fun onAmountReduceByClick( reduceAmountBy: BigDecimal, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt deleted file mode 100644 index 31e1f97f96..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.transformers - -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network -import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.lib.crypto.BlockchainUtils.isTron -import com.tangem.utils.transformer.Transformer - -internal class ActionTypeActiveStakeTransformer( - private val cryptoCurrencyStatus: CryptoCurrencyStatus, - private val activeStake: BalanceState, -) : Transformer { - - override fun transform(prevState: StakingUiState): StakingUiState { - val isTronStakedBalance = isTronStakedBalance( - networkId = cryptoCurrencyStatus.currency.network.id, - activeStake = activeStake, - ) - val actionType = if (activeStake.pendingActions.isEmpty() || isTronStakedBalance) { - StakingActionCommonType.EXIT - } else { - StakingActionCommonType.PENDING_OTHER - } - - return prevState.copy(actionType = actionType) - } - - private fun isTronStakedBalance(networkId: Network.ID, activeStake: BalanceState): Boolean { - return isTron(networkId.value) && activeStake.type == BalanceType.STAKED - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index c8066c1dc2..5c3bfbd6e0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -65,7 +65,7 @@ internal class AddStakingNotificationsTransformer( val feeValue = feeState?.fee?.amount?.value.orZero() val reduceAmountBy = confirmationState.reduceAmountBy.orZero() - val isEnterAction = prevState.actionType == StakingActionCommonType.ENTER + val isEnterAction = prevState.actionType == StakingActionCommonType.Enter val isFeeCoverage = checkFeeCoverage( amountValue = amountValue, feeValue = feeValue, @@ -93,12 +93,7 @@ internal class AddStakingNotificationsTransformer( prevState = prevState, feeError = feeError, sendingAmount = sendingAmount, - onReload = { - prevState.clickIntents.getFee( - confirmationState.pendingAction, - confirmationState.pendingActions, - ) - }, + onReload = prevState.clickIntents::getFee, feeValue = feeValue, ) // warnings @@ -228,7 +223,7 @@ internal class AddStakingNotificationsTransformer( val showNotification = sendingAmount + feeAmount > balance if (showNotification) { - val notification = if (actionType == StakingActionCommonType.ENTER) { + val notification = if (actionType == StakingActionCommonType.Enter) { NotificationUM.Error.TotalExceedsBalance } else { with(cryptoCurrencyStatus.currency) { @@ -249,9 +244,9 @@ internal class AddStakingNotificationsTransformer( private fun MutableList.addInfoNotifications(prevState: StakingUiState) { when (prevState.actionType) { - StakingActionCommonType.EXIT -> addExitInfoNotifications() - StakingActionCommonType.ENTER -> addEnterInfoNotifications() - else -> addPendingInfoNotifications(prevState) + StakingActionCommonType.Enter -> addEnterInfoNotifications() + StakingActionCommonType.Exit -> addExitInfoNotifications() + is StakingActionCommonType.Pending -> addPendingInfoNotifications(prevState) } } @@ -312,6 +307,10 @@ internal class AddStakingNotificationsTransformer( resourceReference(R.string.staking_revote) to resourceReference(R.string.staking_notifications_revote_tron_text) } + StakingActionType.RESTAKE -> { + resourceReference(R.string.staking_restake) to + resourceReference(R.string.staking_notification_restake_text) + } else -> null to null } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt deleted file mode 100644 index e3adab9f50..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.transformers - -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList - -internal class SetActionToExecuteTransformer( - private val actionTypeToOverwrite: StakingActionCommonType, - private val pendingAction: PendingAction?, - private val pendingActions: ImmutableList?, -) : Transformer { - override fun transform(prevState: StakingUiState): StakingUiState { - val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data - return prevState.copy( - actionType = actionTypeToOverwrite, - confirmationState = confirmationState?.copy( - pendingAction = pendingAction, - pendingActions = pendingActions, - ) ?: StakingStates.ConfirmationState.Empty(), - ) - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 52251d7408..c2761a0713 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -109,6 +109,7 @@ internal class SetButtonsStateTransformer( StakingStep.Confirmation -> getConfirmationButtonText() StakingStep.Validators -> resourceReference(R.string.common_continue) StakingStep.Amount, + StakingStep.RestakeValidator, StakingStep.RewardsValidators, -> resourceReference(R.string.common_next) } @@ -120,17 +121,15 @@ internal class SetButtonsStateTransformer( resourceReference(R.string.common_close) } else { when (actionType) { - StakingActionCommonType.ENTER -> { + StakingActionCommonType.Enter -> { if (confirmationState.isApprovalNeeded) { resourceReference(R.string.give_permission_title) } else { resourceReference(R.string.common_stake) } } - StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake) - StakingActionCommonType.PENDING_OTHER, - StakingActionCommonType.PENDING_REWARDS, - -> confirmationState.pendingAction?.type.getPendingActionTitle() + StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake) + is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle() } } } else { @@ -140,18 +139,11 @@ internal class SetButtonsStateTransformer( private fun StakingUiState.onPrimaryClick() { when (currentStep) { - StakingStep.InitialInfo -> clickIntents.onEnterClick() - StakingStep.Validators -> { - if (confirmationState is StakingStates.ConfirmationState.Data) { - clickIntents.onNextClick( - pendingActions = confirmationState.pendingActions, - pendingAction = confirmationState.pendingAction, - ) - } else { - clickIntents.onNextClick() - } - } - StakingStep.Amount -> clickIntents.onNextClick() + StakingStep.InitialInfo -> clickIntents.onNextClick() + StakingStep.Validators, + StakingStep.RestakeValidator, + -> clickIntents.onNextClick() + StakingStep.Amount -> clickIntents.onEnterClick() StakingStep.Confirmation -> onConfirmationClick() StakingStep.RewardsValidators -> Unit } @@ -162,7 +154,7 @@ internal class SetButtonsStateTransformer( if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { clickIntents.onNextClick() } else { - val isEnterAction = actionType == StakingActionCommonType.ENTER + val isEnterAction = actionType == StakingActionCommonType.Enter val isApproveNeeded = confirmationState.isApprovalNeeded if (isEnterAction && isApproveNeeded) { @@ -179,6 +171,7 @@ internal class SetButtonsStateTransformer( private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) { StakingStep.InitialInfo, StakingStep.RewardsValidators, + StakingStep.RestakeValidator, StakingStep.Confirmation, StakingStep.Validators, -> false @@ -192,7 +185,9 @@ internal class SetButtonsStateTransformer( StakingStep.Amount -> amountState.isPrimaryButtonEnabled StakingStep.Confirmation -> confirmationState.isPrimaryButtonEnabled StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled - StakingStep.Validators -> true + StakingStep.RestakeValidator, + StakingStep.Validators, + -> true } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 196784785f..85443b4096 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -2,22 +2,17 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.FeeState -import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val fee: Fee, - private val action: PendingAction?, - private val actions: ImmutableList?, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -30,7 +25,6 @@ internal class SetConfirmationStateAssentTransformer( if (this is StakingStates.ConfirmationState.Data) { val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true return copy( - innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Content( fee = fee, rate = feeCryptoCurrencyStatus?.value?.fiatRate, @@ -38,11 +32,7 @@ internal class SetConfirmationStateAssentTransformer( appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), - validatorState = validatorState.copySealed(isClickable = true), - pendingAction = action, - pendingActions = actions, isPrimaryButtonEnabled = true, - isApprovalNeeded = false, ) } else { return this diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index db27e0c2d9..4ecbf95344 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -16,11 +16,10 @@ internal class SetConfirmationStateCompletedTransformer( } private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState { - if (this is StakingStates.ConfirmationState.Data) { - return copy( + return if (this is StakingStates.ConfirmationState.Data) { + copy( isPrimaryButtonEnabled = true, innerState = InnerConfirmationStakingState.COMPLETED, - validatorState = validatorState.copySealed(isClickable = false), footerText = TextReference.EMPTY, notifications = persistentListOf(), transactionDoneState = TransactionDoneState.Content( @@ -29,7 +28,7 @@ internal class SetConfirmationStateCompletedTransformer( ), ) } else { - return this + this } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateEmptyTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateEmptyTransformer.kt new file mode 100644 index 0000000000..84cd59a951 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateEmptyTransformer.kt @@ -0,0 +1,16 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal object SetConfirmationStateEmptyTransformer : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + actionType = StakingActionCommonType.Enter, + validatorState = StakingStates.ValidatorState.Empty(), + confirmationState = StakingStates.ConfirmationState.Empty(), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt index 24bdd43c84..9043cc8ae9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt @@ -19,7 +19,6 @@ internal class SetConfirmationStateInProgressTransformer : Transformer? = null, + private val pendingAction: PendingAction? = pendingActions?.firstOrNull(), +) : Transformer { + + private val networkId + get() = cryptoCurrencyStatus.currency.network.id.value + + private val isComposePendingActions + get() = isComposePendingActions(networkId, pendingActions) + + private val isTronStakedBalance + get() = isTronStakedBalance(networkId, pendingAction) + + private val isExit: Boolean + get() = pendingAction == null && pendingActions?.isEmpty() == true || isTronStakedBalance + + override fun transform(prevState: StakingUiState): StakingUiState { + val actionType = when { + isEnter -> StakingActionCommonType.Enter + isExit -> StakingActionCommonType.Exit + else -> when (pendingAction?.type) { + StakingActionType.STAKE -> StakingActionCommonType.Enter + StakingActionType.UNSTAKE -> StakingActionCommonType.Exit + StakingActionType.CLAIM_REWARDS, + StakingActionType.RESTAKE_REWARDS, + -> StakingActionCommonType.Pending.Rewards + StakingActionType.VOTE_LOCKED, + StakingActionType.RESTAKE, + -> StakingActionCommonType.Pending.Restake + else -> StakingActionCommonType.Pending.Other + } + } + + return prevState.copy( + actionType = actionType, + confirmationState = StakingStates.ConfirmationState.Data( + isPrimaryButtonEnabled = false, + innerState = InnerConfirmationStakingState.ASSENT, + feeState = FeeState.Loading, + notifications = persistentListOf(), + footerText = TextReference.EMPTY, + transactionDoneState = TransactionDoneState.Empty, + isApprovalNeeded = stakingApproval is StakingApproval.Needed, + reduceAmountBy = null, + pendingAction = pendingAction, + pendingActions = pendingActions.takeIf { isComposePendingActions }, + possiblePendingTransaction = null, + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index e32fc821f9..dae491e1b8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -10,10 +10,11 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.utils.getRewardSchedule import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.persistentListOf internal class SetConfirmationStateLoadingTransformer( private val yield: Yield, @@ -22,39 +23,21 @@ internal class SetConfirmationStateLoadingTransformer( ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { - val filteredValidators = yield.validators.filter { - it.preferred - } val possibleConfirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data - val possibleValidatorState = possibleConfirmationState?.validatorState as? ValidatorState.Content - val chosenValidator = possibleValidatorState?.chosenValidator ?: filteredValidators[0] return prevState.copy( - confirmationState = StakingStates.ConfirmationState.Data( + confirmationState = possibleConfirmationState?.copy( isPrimaryButtonEnabled = false, - innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Loading, - validatorState = ValidatorState.Content( - isClickable = true, - chosenValidator = chosenValidator, - availableValidators = filteredValidators, - ), - notifications = persistentListOf(), footerText = getFooter(prevState), - transactionDoneState = TransactionDoneState.Empty, - pendingAction = possibleConfirmationState?.pendingAction, - pendingActions = possibleConfirmationState?.pendingActions, - isApprovalNeeded = false, - reduceAmountBy = null, - possiblePendingTransaction = null, - ), + ) ?: prevState.confirmationState, ) } private fun getFooter(state: StakingUiState): TextReference { val amountState = state.amountState as? AmountState.Data - val isEnterAction = state.actionType == StakingActionCommonType.ENTER + val isEnterAction = state.actionType == StakingActionCommonType.Enter val amountDecimal = amountState?.amountTextField?.fiatAmount?.value val amountValue = BigDecimalFormatter.formatFiatAmount( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt index ee866db72c..1c1c7103dc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt @@ -13,7 +13,6 @@ internal object SetConfirmationStateResetAssentTransformer : Transformer, private val userWalletProvider: Provider, @@ -76,7 +74,7 @@ internal class SetInitialDataStateTransformer( currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), amountState = createInitialAmountState(), - confirmationState = createInitialConfirmationState(), + confirmationState = StakingStates.ConfirmationState.Empty(), rewardsValidatorsState = rewardsValidatorStateConverter.convert(Unit), bottomSheetConfig = null, ) @@ -87,7 +85,7 @@ internal class SetInitialDataStateTransformer( return StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = !cryptoCurrencyStatusProvider().value.amount.isNullOrZero(), showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty, - aprRange = getAprRange(yield.validators), + aprRange = getAprRange(yield.preferredValidators), infoItems = getInfoItems(), onInfoClick = clickIntents::onInfoClick, yieldBalance = yieldBalance, @@ -113,7 +111,7 @@ internal class SetInitialDataStateTransformer( } private fun createAnnualPercentageRateItem(): RoundedListWithDividersItemData { - val validators = yield.validators + val validators = yield.preferredValidators return RoundedListWithDividersItemData( id = R.string.staking_details_annual_percentage_rate, startText = TextReference.Res(R.string.staking_details_annual_percentage_rate), @@ -214,23 +212,6 @@ internal class SetInitialDataStateTransformer( return amountStateConverter.convert("") } - private fun createInitialConfirmationState(): StakingStates.ConfirmationState { - return StakingStates.ConfirmationState.Data( - isPrimaryButtonEnabled = false, - innerState = InnerConfirmationStakingState.ASSENT, - feeState = FeeState.Loading, - validatorState = ValidatorState.Loading, - notifications = persistentListOf(), - footerText = TextReference.EMPTY, - transactionDoneState = TransactionDoneState.Empty, - pendingAction = null, - pendingActions = null, - isApprovalNeeded = isApprovalNeeded, - reduceAmountBy = null, - possiblePendingTransaction = null, - ) - } - private fun getAprRange(validators: List): TextReference { val aprValues = validators.mapNotNull { it.apr } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt index 578af70403..a29c1a8a85 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -20,7 +20,9 @@ internal object SetTitleTransformer : Transformer { val title = when (currentStep) { StakingStep.Amount -> resourceReference(R.string.send_amount_label) - StakingStep.Validators -> resourceReference(R.string.staking_validators) + StakingStep.RestakeValidator, + StakingStep.Validators, + -> resourceReference(R.string.staking_validators) StakingStep.RewardsValidators -> resourceReference(R.string.common_claim_rewards) StakingStep.InitialInfo -> resourceReference( R.string.staking_title_stake, @@ -29,11 +31,11 @@ internal object SetTitleTransformer : Transformer { StakingStep.Confirmation -> { when (actionType) { - StakingActionCommonType.ENTER -> resourceReference( + StakingActionCommonType.Enter -> resourceReference( R.string.staking_title_stake, wrappedList(prevState.cryptoCurrencyName), ) - StakingActionCommonType.EXIT -> resourceReference( + StakingActionCommonType.Exit -> resourceReference( R.string.staking_title_unstake, wrappedList(prevState.cryptoCurrencyName), ) @@ -51,7 +53,7 @@ internal object SetTitleTransformer : Transformer { } } - val subtitle = if (currentStep == StakingStep.Confirmation && actionType == StakingActionCommonType.ENTER) { + val subtitle = if (currentStep == StakingStep.Confirmation && actionType == StakingActionCommonType.Enter) { stringReference(prevState.walletName) } else { null diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index d669d5a7e9..284f9b239b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -102,7 +102,7 @@ internal class AmountRequirementStateTransformer( private fun isIntegerOnlyError(amountState: AmountState.Data, actionType: StakingActionCommonType): Boolean { val cryptoAmountValue = amountState.amountTextField.cryptoAmount.value ?: return false - val isEnter = actionType == StakingActionCommonType.ENTER + val isEnter = actionType == StakingActionCommonType.Enter val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) val isIntegerOnly = cryptoAmountValue.isZero() || cryptoAmountValue.remainder(BigDecimal.ONE).isZero() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt index 939c6877dc..6f331795e4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt @@ -35,7 +35,6 @@ internal class SetConfirmationStateAssentApprovalTransformer( appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), - validatorState = validatorState.copySealed(isClickable = true), isPrimaryButtonEnabled = true, isApprovalNeeded = true, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt index b5631913ad..ca32d6c1be 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -12,7 +12,6 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.ValidatorState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer @@ -28,7 +27,7 @@ internal class ShowApprovalBottomSheetTransformer( val amountState = prevState.amountState as? AmountState.Data ?: return prevState val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState - val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: return prevState + val validatorState = prevState.validatorState as? StakingStates.ValidatorState.Data ?: return prevState val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState val fee = feeState.fee ?: return prevState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index e6ba4e32db..0f79a3ab26 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -1,29 +1,45 @@ package com.tangem.features.staking.impl.presentation.state.transformers.validator import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.ValidatorState import com.tangem.utils.transformer.Transformer internal class ValidatorSelectChangeTransformer( + private val yield: Yield, private val selectedValidator: Yield.Validator?, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { - val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState - selectedValidator ?: return prevState - val validatorState = (confirmationState.validatorState as? ValidatorState.Content)?.copy( - chosenValidator = selectedValidator, - ) ?: ValidatorState.Content( - isClickable = false, - availableValidators = emptyList(), - chosenValidator = selectedValidator, - ) + val validatorState = prevState.validatorState as? StakingStates.ValidatorState.Data + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + + val isRestake = prevState.actionType == StakingActionCommonType.Pending.Restake + val isEnter = prevState.actionType == StakingActionCommonType.Enter + val isFromInfoScreen = prevState.currentStep == StakingStep.InitialInfo + val isVoteLocked = confirmationState?.pendingAction?.type == StakingActionType.VOTE_LOCKED + + val activeValidator = selectedValidator.takeIf { isFromInfoScreen && isRestake } + ?: validatorState?.activeValidator + val filteredValidators = yield.preferredValidators.filterNot { it == activeValidator } + + val selectedValidator = if (isRestake && isFromInfoScreen) { + filteredValidators.firstOrNull() + } else { + selectedValidator + } return prevState.copy( - confirmationState = confirmationState.copy( - validatorState = validatorState, + validatorState = StakingStates.ValidatorState.Data( + chosenValidator = selectedValidator ?: yield.preferredValidators.first(), + availableValidators = filteredValidators, + isPrimaryButtonEnabled = true, + isClickable = true, + activeValidator = activeValidator, + isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt index efda617b73..ae8f6d4cb2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -5,8 +5,12 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.lib.crypto.BlockchainUtils.isBSC import com.tangem.lib.crypto.BlockchainUtils.isSolana +import com.tangem.lib.crypto.BlockchainUtils.isTron import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList @Suppress("CyclomaticComplexMethod") internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (this) { @@ -29,8 +33,35 @@ internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (t null -> TextReference.EMPTY } -internal fun isSolanaWithdraw(networkId: String, pendingActions: ImmutableList?): Boolean { - val isSolana = isSolana(networkId) - val isWithdraw = pendingActions?.all { it.type == StakingActionType.WITHDRAW } == true - return isSolana && isWithdraw && !pendingActions.isNullOrEmpty() +internal fun isSingleAction(networkId: String, activeStake: BalanceState): Boolean { + val isSingleAction = activeStake.pendingActions.size <= 1 // Either single or none pending actions + val isComposePendingActions = isComposePendingActions(networkId, activeStake.pendingActions) + val isBscRestake = isBSC(networkId) && activeStake.pendingActions.any { + it.type == StakingActionType.RESTAKE + } + + return isSingleAction && !isBscRestake || isComposePendingActions +} + +internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState) = if (isBSC(networkId)) { + activeStake.pendingActions.plus( + PendingAction( + type = StakingActionType.UNSTAKE, + passthrough = "", + args = null, + ), + ).toPersistentList() +} else { + activeStake.pendingActions +} + +internal fun isTronStakedBalance(networkId: String, pendingAction: PendingAction?): Boolean { + return isTron(networkId) && pendingAction?.type == StakingActionType.REVOKE +} + +internal fun isComposePendingActions(networkId: String, pendingActions: ImmutableList?): Boolean { + return when { + isSolana(networkId) -> pendingActions?.any { it.type == StakingActionType.WITHDRAW } == true + else -> false + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 255fd36faa..ad11c31650 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -19,13 +19,13 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData +import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock @@ -36,12 +36,12 @@ import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickInten internal fun StakingConfirmationContent( amountState: AmountState, state: StakingStates.ConfirmationState, + validatorState: StakingStates.ValidatorState, clickIntents: StakingClickIntents, type: StakingActionCommonType, ) { if (state !is StakingStates.ConfirmationState.Data) return - val isEnterAction = type == StakingActionCommonType.ENTER - val showValidatorBlock = isEnterAction || state.pendingAction?.type == StakingActionType.VOTE_LOCKED + val isEnterAction = type == StakingActionCommonType.Enter val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED val isTransactionInProgress = state.notifications.any { it is StakingNotification.Warning.TransactionInProgress } Column( @@ -67,13 +67,11 @@ internal fun StakingConfirmationContent( isEditingDisabled = !isEnterAction && state.innerState != InnerConfirmationStakingState.COMPLETED, onClick = clickIntents::onPrevClick, ) - if (showValidatorBlock) { - ValidatorBlock( - validatorState = state.validatorState, - isClickable = !isTransactionInProgress, - onClick = clickIntents::openValidators, - ) - } + ValidatorBlock( + validatorState = validatorState, + isClickable = !isTransactionInProgress, + onClick = clickIntents::openValidators, + ) StakingFeeBlock(feeState = state.feeState, isTransactionSent = isTransactionSent) NotificationsBlock(notifications = state.notifications) } @@ -88,8 +86,9 @@ private fun Preview_StakingConfirmationContent() { StakingConfirmationContent( amountState = AmountStatePreviewData.amountState, state = ConfirmationStatePreviewData.assentStakingState, + validatorState = ValidatorStatePreviewData.validatorState, clickIntents = StakingClickIntentsStub, - type = StakingActionCommonType.ENTER, + type = StakingActionCommonType.Enter, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 5c2e5ceb51..21be700364 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -3,7 +3,6 @@ package com.tangem.features.staking.impl.presentation.ui import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween import androidx.compose.animation.togetherWith import androidx.compose.foundation.background @@ -89,15 +88,12 @@ private fun StakingAppBar(uiState: StakingUiState) { val (backIcon, click) = when (uiState.currentStep) { StakingStep.Amount, StakingStep.Confirmation, - -> { - R.drawable.ic_close_24 to uiState.clickIntents::onBackClick - } + -> R.drawable.ic_close_24 to uiState.clickIntents::onBackClick StakingStep.Validators, StakingStep.RewardsValidators, + StakingStep.RestakeValidator, StakingStep.InitialInfo, - -> { - R.drawable.ic_back_24 to uiState.clickIntents::onPrevClick - } + -> R.drawable.ic_back_24 to uiState.clickIntents::onPrevClick } AppBarWithBackButtonAndIcon( text = uiState.title.resolveReference(), @@ -109,7 +105,6 @@ private fun StakingAppBar(uiState: StakingUiState) { ) } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) { val currentScreen = uiState.currentStep @@ -172,17 +167,16 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M StakingStep.Confirmation -> StakingConfirmationContent( amountState = uiState.amountState, state = uiState.confirmationState, + validatorState = uiState.validatorState, clickIntents = uiState.clickIntents, type = uiState.actionType, ) - StakingStep.Validators -> { - val confirmState = uiState.confirmationState - if (confirmState !is StakingStates.ConfirmationState.Data) return@AnimatedContent - StakingValidatorListContent( - state = confirmState.validatorState, - clickIntents = uiState.clickIntents, - ) - } + StakingStep.RestakeValidator, + StakingStep.Validators, + -> StakingValidatorListContent( + state = uiState.validatorState, + clickIntents = uiState.clickIntents, + ) } } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index 9ddda86f32..692f5b3050 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -28,8 +28,8 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.ValidatorState -import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.extensions.orZero @@ -39,7 +39,7 @@ import com.tangem.utils.extensions.orZero */ @Composable internal fun StakingValidatorListContent( - state: ValidatorState, + state: StakingStates.ValidatorState, clickIntents: StakingClickIntents, modifier: Modifier = Modifier, ) { @@ -51,7 +51,7 @@ internal fun StakingValidatorListContent( .background(TangemTheme.colors.background.secondary) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - if (state is ValidatorState.Content) { + if (state is StakingStates.ValidatorState.Data) { val validators = state.availableValidators items( count = validators.size, @@ -131,7 +131,7 @@ private fun RowScope.ValidatorLabel(isStrategicPartner: Boolean) { @Composable private fun StakingValidatorListContent_Preview( @PreviewParameter(StakingValidatorListContentPreviewProvider::class) - data: ValidatorState, + data: StakingStates.ValidatorState, ) { TangemThemePreview { StakingValidatorListContent( @@ -141,8 +141,8 @@ private fun StakingValidatorListContent_Preview( } } -private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf(ConfirmationStatePreviewData.assentStakingState.validatorState) +private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(ValidatorStatePreviewData.validatorState) } // endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index 192d1e5397..2da1751f34 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -15,39 +15,40 @@ import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder import com.tangem.utils.extensions.orZero @Composable -internal fun ValidatorBlock(validatorState: ValidatorState, isClickable: Boolean, onClick: () -> Unit) { +internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClickable: Boolean, onClick: () -> Unit) { + val state = validatorState as? StakingStates.ValidatorState.Data ?: return + if (!state.isVisibleOnConfirmation) return + Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) .clickable( - enabled = validatorState.isClickable && isClickable, + enabled = state.isClickable && isClickable, interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onClick, ), ) { - if (validatorState is ValidatorState.Content) { - InputRowImageInfo( - title = resourceReference(R.string.staking_validator), - subtitle = stringReference(validatorState.chosenValidator.name), - infoTitle = annotatedReference { - append(resourceReference(R.string.staking_details_apr).resolveReference()) - appendSpace() - appendColored( - text = BigDecimalFormatter.formatPercent(validatorState.chosenValidator.apr.orZero(), true), - color = TangemTheme.colors.text.accent, - ) - }, - imageUrl = validatorState.chosenValidator.image.orEmpty(), - onImageError = { ValidatorImagePlaceholder() }, - ) - } + InputRowImageInfo( + title = resourceReference(R.string.staking_validator), + subtitle = stringReference(state.chosenValidator.name), + infoTitle = annotatedReference { + append(resourceReference(R.string.staking_details_apr).resolveReference()) + appendSpace() + appendColored( + text = BigDecimalFormatter.formatPercent(state.chosenValidator.apr.orZero(), true), + color = TangemTheme.colors.text.accent, + ) + }, + imageUrl = state.chosenValidator.image.orEmpty(), + onImageError = { ValidatorImagePlaceholder() }, + ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 8e55492aa8..7e23d53fc6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -3,13 +3,10 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType -import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @Suppress("TooManyFunctions") @@ -17,12 +14,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() - fun onNextClick( - actionTypeToOverwrite: StakingActionCommonType? = null, - pendingAction: PendingAction? = null, - pendingActions: ImmutableList? = null, - balanceState: BalanceState? = null, - ) + fun onNextClick(balanceState: BalanceState? = null) fun onActionClick() @@ -36,9 +28,9 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onEnterClick() - fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) + fun getFee() - override fun onAmountNext() = onNextClick(actionTypeToOverwrite = null) + override fun onAmountNext() = onNextClick() fun openValidators() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index a6a90596ef..8e3d7ac5ee 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -63,12 +63,14 @@ import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.* import com.tangem.features.staking.impl.presentation.state.transformers.approval.* import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer -import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw +import com.tangem.features.staking.impl.presentation.state.utils.isSingleAction +import com.tangem.features.staking.impl.presentation.state.utils.withStubUnstakeAction import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -185,6 +187,7 @@ internal class StakingViewModel @Inject constructor( private var stakingApproval: StakingApproval = StakingApproval.Empty private var isAmountSubtractAvailable: Boolean = false + private var isAnyTokenStaked: Boolean = false private val allowanceTaskScheduler = SingleTaskScheduler() private val transactionsInProgress: CopyOnWriteArrayList = CopyOnWriteArrayList() @@ -210,70 +213,16 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } - override fun onNextClick( - actionTypeToOverwrite: StakingActionCommonType?, - pendingAction: PendingAction?, - pendingActions: ImmutableList?, - balanceState: BalanceState?, - ) { - if (actionTypeToOverwrite != null) { - stateController.update(SetActionToExecuteTransformer(actionTypeToOverwrite, pendingAction, pendingActions)) - } + override fun onNextClick(balanceState: BalanceState?) { stakingStateRouter.onNextClick() - when { - isInitState() -> { - stateController.update(SetConfirmationStateResetAssentTransformer) - stateController.update( - SetConfirmationStateLoadingTransformer( - yield = yield, - appCurrency = appCurrency, - cryptoCurrency = cryptoCurrencyStatus.currency, - ), - ) - if (balanceState != null) { - stateController.update( - SetPossiblePendingTransactionTransformer(yield, balanceState, cryptoCurrencyStatus), - ) - } - - stateController.update( - transformer = SetInitialDataStateTransformer( - clickIntents = this@StakingViewModel, - yield = yield, - isAnyTokenStaked = true, - isApprovalNeeded = stakingApproval is StakingApproval.Needed, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - userWalletProvider = Provider { userWallet }, - appCurrencyProvider = Provider { appCurrency }, - balancesToShowProvider = Provider { balancesToShow }, - ), - ) - onRefreshSwipe(isRefreshing = false) - } - isAssentState() -> { - getFee(pendingAction, pendingActions) - if (balanceState != null) { - stateController.update( - SetPossiblePendingTransactionTransformer(yield, balanceState, cryptoCurrencyStatus), - ) - } - val amountState = value.amountState as? AmountState.Data - if (amountState?.amountTextField?.isWarning == true) { - stateController.update( - AmountRoundToIntegerTransformer( - cryptoCurrencyStatus = cryptoCurrencyStatus, - ), - ) - } - } + if (balanceState != null && (isInitState() || isAssentState())) { + stateController.update( + SetPossiblePendingTransactionTransformer(yield, balanceState, cryptoCurrencyStatus), + ) } } - override fun onActionClick() { - handleOnNextConfirmationClick() - } - - override fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) { + override fun getFee() { stateController.update( SetConfirmationStateLoadingTransformer( yield = yield, @@ -283,16 +232,12 @@ internal class StakingViewModel @Inject constructor( ) viewModelScope.launch { feeLoader.getFee( - pendingAction = pendingAction, - pendingActions = pendingActions, onStakingFee = { gasEstimate -> stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = gasEstimate, - action = pendingAction, - actions = pendingActions, ), ) updateNotifications() @@ -326,7 +271,7 @@ internal class StakingViewModel @Inject constructor( }.saveIn(feeJobHolder) } - private fun handleOnNextConfirmationClick() { + override fun onActionClick() { if (isAssentState()) { viewModelScope.launch { stakingAnalyticSender.sendTransactionStakingClickedAnalytics(value) @@ -412,28 +357,23 @@ internal class StakingViewModel @Inject constructor( } override fun onEnterClick() { - onAmountValueChange("") // reset amount state - - // TODO refactor in [REDACTED_JIRA] - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - val filteredValidator = yield.validators.filter { it.preferred } - if (filteredValidator.isEmpty()) { + if (yield.preferredValidators.isEmpty()) { stateController.updateEvent( StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators), ) } else { - stateController.update { - value.copy( - confirmationState = confirmationState?.copy( - validatorState = ValidatorState.Content( - isClickable = true, - chosenValidator = filteredValidator[0], - availableValidators = filteredValidator, - ), - ) as StakingStates.ConfirmationState, - ) - } - onNextClick(StakingActionCommonType.ENTER) + stateController.updateAll( + SetConfirmationStateInitTransformer( + isEnter = true, + cryptoCurrencyStatus = cryptoCurrencyStatus, + stakingApproval = stakingApproval, + ), + ValidatorSelectChangeTransformer( + selectedValidator = null, + yield = yield, + ), + ) + onNextClick() } } @@ -474,7 +414,12 @@ internal class StakingViewModel @Inject constructor( validator.name, ), ) - stateController.update(ValidatorSelectChangeTransformer(validator)) + stateController.update( + ValidatorSelectChangeTransformer( + selectedValidator = validator, + yield = yield, + ), + ) } override fun openRewardsValidators() { @@ -493,50 +438,38 @@ internal class StakingViewModel @Inject constructor( token = value.cryptoCurrencySymbol, ), ) - onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_REWARDS) + stateController.update { + value.copy(actionType = StakingActionCommonType.Pending.Rewards) + } + stakingStateRouter.showRewardsValidators() } } override fun onActiveStake(activeStake: BalanceState) { - val isAllWithdrawActions = isSolanaWithdraw( - cryptoCurrencyStatus.currency.network.id.value, - activeStake.pendingActions, - ) - val isMultiplePendingActions = activeStake.pendingActions.size > 1 - if (isMultiplePendingActions && !isAllWithdrawActions) { + val networkId = cryptoCurrencyStatus.currency.network.id.value + if (isSingleAction(networkId, activeStake)) { + prepareForConfirmation( + pendingActions = activeStake.pendingActions, + validator = activeStake.validator, + amountValue = activeStake.cryptoValue, + ) + onNextClick(balanceState = activeStake) + } else { stateController.update( ShowActionSelectorBottomSheetTransformer( - pendingActions = activeStake.pendingActions, + pendingActions = withStubUnstakeAction(networkId, activeStake), onActionSelect = { action -> - stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) - stateController.update( - AmountChangeStateTransformer( - cryptoCurrencyStatus, - activeStake.cryptoValue, - yield, - ), - ) - onNextClick( - actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER, + prepareForConfirmation( pendingAction = action, - balanceState = activeStake, + validator = activeStake.validator, + amountValue = activeStake.cryptoValue, ) stateController.update(DismissBottomSheetStateTransformer) + onNextClick(balanceState = activeStake) }, onDismiss = { stateController.update(DismissBottomSheetStateTransformer) }, ), ) - } else { - stateController.update(ActionTypeActiveStakeTransformer(cryptoCurrencyStatus, activeStake)) - stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) - - onNextClick( - actionTypeToOverwrite = null, - pendingAction = activeStake.pendingActions.firstOrNull(), - pendingActions = activeStake.pendingActions.takeIf { isAllWithdrawActions }, - balanceState = activeStake, - ) } } @@ -632,7 +565,7 @@ internal class StakingViewModel @Inject constructor( stakingAnalyticSender.sendTransactionApprovalAnalytics(tokenCryptoCurrency) stateController.update(SetApprovalInProgressTransformer) stateController.update(DismissBottomSheetStateTransformer) - awaitForAllowance(confirmationState.pendingAction) + awaitForAllowance() }, ) }.saveIn(approvalJobHolder) @@ -718,7 +651,7 @@ internal class StakingViewModel @Inject constructor( stateController.update(DismissStakingNotificationsStateTransformer(notification)) } - private fun awaitForAllowance(pendingAction: PendingAction?) { + private fun awaitForAllowance() { val approval = stakingApproval as? StakingApproval.Needed ?: return allowanceTaskScheduler.scheduleTask( scope = viewModelScope, @@ -737,7 +670,7 @@ internal class StakingViewModel @Inject constructor( val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided") if (allowance >= amount) { - getFee(pendingAction = pendingAction, pendingActions = null) + getFee() allowanceTaskScheduler.cancelTask() } }, @@ -775,7 +708,7 @@ internal class StakingViewModel @Inject constructor( val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") } val amountState = uiState.value.amountState as? AmountState.Data val confirmationState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data - val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorState = uiState.value.validatorState as? StakingStates.ValidatorState.Data val feeState = confirmationState?.feeState as? FeeState.Content val validator = validatorState?.chosenValidator @@ -817,6 +750,10 @@ internal class StakingViewModel @Inject constructor( stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { StakingApproval.Empty } } + private suspend fun setupIsAnyTokenStaked() { + isAnyTokenStaked = isAnyTokenStakedUseCase(userWalletId).getOrNull() ?: false + } + private fun subscribeOnCurrencyStatusUpdates() { getUserWalletUseCase(userWalletId).fold( ifRight = { wallet -> @@ -849,23 +786,11 @@ internal class StakingViewModel @Inject constructor( feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() cryptoCurrencyStatus = status - val isAnyTokenStaked = isAnyTokenStakedUseCase(userWalletId).getOrNull() ?: false setupApprovalNeeded() + setupIsAnyTokenStaked() checkIfSubtractAvailable() - - stateController.update( - transformer = SetInitialDataStateTransformer( - clickIntents = this@StakingViewModel, - yield = yield, - isAnyTokenStaked = isAnyTokenStaked, - isApprovalNeeded = stakingApproval is StakingApproval.Needed, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - userWalletProvider = Provider { userWallet }, - appCurrencyProvider = Provider { appCurrency }, - balancesToShowProvider = Provider { balancesToShow }, - ), - ) + subscribeOnStepChanges() }, ifLeft = { error -> Timber.e(error.toString()) @@ -900,6 +825,69 @@ internal class StakingViewModel @Inject constructor( .launchIn(viewModelScope) } + private fun subscribeOnStepChanges() { + uiState + .distinctUntilChangedBy { it.currentStep } + .onEach { + when { + isInitState() -> { + stateController.updateAll( + SetInitialDataStateTransformer( + clickIntents = this@StakingViewModel, + yield = yield, + isAnyTokenStaked = isAnyTokenStaked, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider { appCurrency }, + balancesToShowProvider = Provider { balancesToShow }, + ), + SetConfirmationStateEmptyTransformer, + ) + onRefreshSwipe(isRefreshing = false) + } + isAssentState() -> { + getFee() + val amountState = value.amountState as? AmountState.Data + if (amountState?.amountTextField?.isWarning == true) { + stateController.update( + AmountRoundToIntegerTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + ), + ) + } + } + } + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + + private fun prepareForConfirmation( + pendingActions: ImmutableList = persistentListOf(), + pendingAction: PendingAction? = pendingActions.firstOrNull(), + validator: Yield.Validator?, + amountValue: String, + ) { + stateController.updateAll( + SetConfirmationStateInitTransformer( + isEnter = false, + cryptoCurrencyStatus = cryptoCurrencyStatus, + stakingApproval = stakingApproval, + pendingActions = pendingActions, + pendingAction = pendingAction, + ), + ValidatorSelectChangeTransformer( + selectedValidator = validator, + yield = yield, + ), + AmountChangeStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + value = amountValue, + yield = yield, + ), + ) + } + private fun isAssentState(): Boolean { return value.currentStep == StakingStep.Confirmation && (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 1c340dabc3..fe67dcf443 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -88,6 +88,11 @@ object BlockchainUtils { return blockchain == Blockchain.Cosmos || blockchain == Blockchain.CosmosTestnet } + fun isBSC(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.BSC || blockchain == Blockchain.BSCTestnet + } + data class BlockchainInfo( val blockchainId: String, val name: String, From e77c29223021b2a75be0248bfdec76e0cd8e07b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Oct 2024 18:50:21 +0300 Subject: [PATCH 03/18] Updated on 2026-08-14 --- .../models/response/model/YieldDTO.kt | 62 ++++++++--------- .../com/tangem/utils/converter/Converter.kt | 15 ++++ .../data/staking/DefaultStakingRepository.kt | 9 +-- .../data/staking/converters/YieldConverter.kt | 69 ++++++++++--------- 4 files changed, 87 insertions(+), 68 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index 330df3c666..951d60ddf7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -9,31 +9,31 @@ data class YieldDTO( @Json(name = "id") val id: String, @Json(name = "token") - val token: TokenDTO, + val token: TokenDTO?, @Json(name = "tokens") - val tokens: List, + val tokens: List?, @Json(name = "args") - val args: ArgsDTO, + val args: ArgsDTO?, @Json(name = "status") - val status: StatusDTO, + val status: StatusDTO?, @Json(name = "apy") - val apy: BigDecimal, + val apy: BigDecimal?, @Json(name = "rewardRate") - val rewardRate: Double, + val rewardRate: Double?, @Json(name = "rewardType") - val rewardType: RewardTypeDTO, + val rewardType: RewardTypeDTO?, @Json(name = "metadata") - val metadata: MetadataDTO, + val metadata: MetadataDTO?, @Json(name = "validators") - val validators: List, + val validators: List?, @Json(name = "isAvailable") - val isAvailable: Boolean, + val isAvailable: Boolean?, ) { @JsonClass(generateAdapter = true) data class StatusDTO( @Json(name = "enter") - val enter: Boolean, + val enter: Boolean?, @Json(name = "exit") val exit: Boolean?, ) @@ -41,21 +41,21 @@ data class YieldDTO( @JsonClass(generateAdapter = true) data class ArgsDTO( @Json(name = "enter") - val enter: Enter, + val enter: Enter?, @Json(name = "exit") val exit: Enter?, ) { @JsonClass(generateAdapter = true) data class Enter( @Json(name = "addresses") - val addresses: Addresses, + val addresses: Addresses?, @Json(name = "args") - val args: Map, + val args: Map?, ) { @JsonClass(generateAdapter = true) data class Addresses( @Json(name = "address") - val address: AddressArgumentDTO, + val address: AddressArgumentDTO?, @Json(name = "additionalAddresses") val additionalAddresses: Map? = null, ) @@ -65,7 +65,7 @@ data class YieldDTO( @JsonClass(generateAdapter = true) data class ValidatorDTO( @Json(name = "address") - val address: String, + val address: String?, @Json(name = "status") val status: ValidatorStatusDTO, @Json(name = "name") @@ -106,51 +106,51 @@ data class YieldDTO( @JsonClass(generateAdapter = true) data class MetadataDTO( @Json(name = "name") - val name: String, + val name: String?, @Json(name = "logoURI") - val logoUri: String, + val logoUri: String?, @Json(name = "description") - val description: String, + val description: String?, @Json(name = "documentation") val documentation: String?, @Json(name = "gasFeeToken") - val gasFeeTokenDTO: TokenDTO, + val gasFeeTokenDTO: TokenDTO?, @Json(name = "token") - val tokenDTO: TokenDTO, + val tokenDTO: TokenDTO?, @Json(name = "tokens") - val tokensDTO: List, + val tokensDTO: List?, @Json(name = "type") - val type: String, + val type: String?, @Json(name = "rewardSchedule") - val rewardSchedule: RewardScheduleDTO, + val rewardSchedule: RewardScheduleDTO?, @Json(name = "cooldownPeriod") val cooldownPeriod: PeriodDTO?, @Json(name = "warmupPeriod") - val warmupPeriod: PeriodDTO, + val warmupPeriod: PeriodDTO?, @Json(name = "rewardClaiming") - val rewardClaiming: RewardClaimingDTO, + val rewardClaiming: RewardClaimingDTO?, @Json(name = "defaultValidator") val defaultValidator: String?, @Json(name = "minimumStake") val minimumStake: Int?, @Json(name = "supportsMultipleValidators") - val supportsMultipleValidators: Boolean, + val supportsMultipleValidators: Boolean?, @Json(name = "revshare") - val revshare: EnabledDTO, + val revshare: EnabledDTO?, @Json(name = "fee") - val fee: EnabledDTO, + val fee: EnabledDTO?, ) { @JsonClass(generateAdapter = true) data class PeriodDTO( @Json(name = "days") - val days: Int, + val days: Int?, ) @JsonClass(generateAdapter = true) data class EnabledDTO( @Json(name = "enabled") - val enabled: Boolean, + val enabled: Boolean?, ) enum class RewardScheduleDTO { diff --git a/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt b/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt index b818478a45..603ebf1fcd 100644 --- a/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt +++ b/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt @@ -11,4 +11,19 @@ interface Converter { fun convertSet(input: Collection): Set { return input.mapTo(hashSetOf(), ::convert) } + + fun convertListIgnoreErrors(input: Collection, onError: ((Throwable) -> Unit)? = null): List { + return input.mapNotNull { + try { + convert(it) + } catch (throwable: Throwable) { + onError?.invoke(throwable) + null + } + } + } + + fun T?.asMandatory(name: String): T { + return this ?: error("$name must not be null") + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 729477a5cc..71bd21a01e 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -126,7 +126,7 @@ internal class DefaultStakingRepository( val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false) .getOrThrow() - stakingYieldsStore.store(stakingTokensWithYields.data.filter { it.isAvailable }) + stakingYieldsStore.store(stakingTokensWithYields.data.filter { it.isAvailable ?: false }) }, ) } @@ -579,9 +579,10 @@ internal class DefaultStakingRepository( } private fun getEnabledYields(): List { - return stakingYieldsStore - .get() - .map { yieldConverter.convert(it) } + return yieldConverter.convertListIgnoreErrors( + input = stakingYieldsStore.get(), + onError = { Timber.e("Error converting enabled yields list: $it") }, + ) } private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody { diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index 8489a8772d..7ed9eb23e3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -17,37 +17,37 @@ class YieldConverter( override fun convert(value: YieldDTO): Yield { return Yield( - id = value.id, - token = tokenConverter.convert(value.token), - tokens = value.tokens.map { tokenConverter.convert(it) }, - args = convertArgs(value.args), - status = convertStatus(value.status), - apy = value.apy, - rewardRate = value.rewardRate, - rewardType = convertRewardType(value.rewardType), - metadata = convertMetadata(value.metadata), - validators = value.validators + id = value.id.asMandatory("id"), + token = tokenConverter.convert(value.token.asMandatory("token")), + tokens = value.tokens.asMandatory("tokens").map { tokenConverter.convert(it) }, + args = convertArgs(value.args.asMandatory("args")), + status = convertStatus(value.status.asMandatory("status")), + apy = value.apy.asMandatory("apy"), + rewardRate = value.rewardRate.asMandatory("rewardRate"), + rewardType = convertRewardType(value.rewardType.asMandatory("rewardType")), + metadata = convertMetadata(value.metadata.asMandatory("metadata")), + validators = value.validators.asMandatory("validators") .asSequence() .filter { it.status == ValidatorStatusDTO.ACTIVE } .map { convertValidator(it) } .sortedByDescending { it.isStrategicPartner } .sortedByDescending { it.apr } .toImmutableList(), - isAvailable = value.isAvailable, + isAvailable = value.isAvailable.asMandatory("isAvailable"), ) } private fun convertArgs(argsDTO: YieldDTO.ArgsDTO): Yield.Args { return Yield.Args( - enter = convertEnter(argsDTO.enter), + enter = convertEnter(argsDTO.enter.asMandatory("enter")), exit = argsDTO.exit?.let { convertEnter(it) }, ) } private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter { return Yield.Args.Enter( - addresses = convertAddresses(enterDTO.addresses), - args = enterDTO.args + addresses = convertAddresses(enterDTO.addresses.asMandatory("addresses")), + args = enterDTO.args.asMandatory("args") .mapKeys { convertArgType(it.key) } .mapValues { convertAddressArgument(it.value) }, ) @@ -55,7 +55,7 @@ class YieldConverter( private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses { return Yield.Args.Enter.Addresses( - address = convertAddressArgument(addressesDTO.address), + address = convertAddressArgument(addressesDTO.address.asMandatory("address")), additionalAddresses = addressesDTO.additionalAddresses ?.mapKeys { convertArgType(it.key) } ?.mapValues { convertAddressArgument(it.value) }, @@ -73,48 +73,51 @@ class YieldConverter( private fun convertStatus(statusDTO: YieldDTO.StatusDTO): Yield.Status { return Yield.Status( - enter = statusDTO.enter, + enter = statusDTO.enter.asMandatory("enter"), exit = statusDTO.exit, ) } private fun convertMetadata(metadataDTO: YieldDTO.MetadataDTO): Yield.Metadata { return Yield.Metadata( - name = metadataDTO.name, - logoUri = metadataDTO.logoUri, - description = metadataDTO.description, + name = metadataDTO.name.asMandatory("name"), + logoUri = metadataDTO.logoUri.asMandatory("logoUri"), + description = metadataDTO.description.asMandatory("description"), documentation = metadataDTO.documentation, - gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO), - token = tokenConverter.convert(metadataDTO.tokenDTO), - tokens = metadataDTO.tokensDTO.map { tokenConverter.convert(it) }, - type = metadataDTO.type, - rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule), + gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")), + token = tokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")), + tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map { tokenConverter.convert(it) }, + type = metadataDTO.type.asMandatory("type"), + rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule.asMandatory("rewardSchedule")), cooldownPeriod = metadataDTO.cooldownPeriod?.let { convertPeriod(it) }, - warmupPeriod = convertPeriod(metadataDTO.warmupPeriod), - rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming), + warmupPeriod = convertPeriod(metadataDTO.warmupPeriod.asMandatory("warmupPeriod")), + rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming.asMandatory("rewardClaiming")), defaultValidator = metadataDTO.defaultValidator, minimumStake = metadataDTO.minimumStake, - supportsMultipleValidators = metadataDTO.supportsMultipleValidators, - revshare = convertEnabled(metadataDTO.revshare), - fee = convertEnabled(metadataDTO.fee), + supportsMultipleValidators = metadataDTO.supportsMultipleValidators.asMandatory( + "supportsMultipleValidators", + ), + revshare = convertEnabled(metadataDTO.revshare.asMandatory("revshare")), + fee = convertEnabled(metadataDTO.fee.asMandatory("fee")), ) } private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period { return Yield.Metadata.Period( - days = periodDTO.days, + days = periodDTO.days.asMandatory("days"), ) } private fun convertEnabled(enabledDTO: YieldDTO.MetadataDTO.EnabledDTO): Yield.Metadata.Enabled { return Yield.Metadata.Enabled( - enabled = enabledDTO.enabled, + enabled = enabledDTO.enabled.asMandatory("enabled"), ) } private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator { + val address = validatorDTO.address.asMandatory("address") return Yield.Validator( - address = validatorDTO.address, + address = address, status = convertValidatorStatus(validatorDTO.status), name = validatorDTO.name, image = validatorDTO.image, @@ -124,7 +127,7 @@ class YieldConverter( stakedBalance = validatorDTO.stakedBalance, votingPower = validatorDTO.votingPower, preferred = validatorDTO.preferred, - isStrategicPartner = isStrategicPartner(validatorDTO.address), + isStrategicPartner = isStrategicPartner(address), ) } From 42806a4d7c530cb7e1e38db5a9fee217a1d4c3e9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Oct 2024 11:59:34 +0300 Subject: [PATCH 04/18] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 22 +++++++++++++++++++ .../com/tangem/tests/DetailsScreenTest.kt | 3 --- .../kotlin/com/tangem/tests/StoriesTest.kt | 4 +++- .../DefaultBlockchainSDKFactory.kt | 6 +---- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 4b3278bc9f..3ff0cba81e 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -1,16 +1,22 @@ package com.tangem.common import android.Manifest +import android.content.Context +import android.util.Log import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import com.kaspersky.components.composesupport.config.withComposeSupport import com.kaspersky.kaspresso.kaspresso.Kaspresso import com.kaspersky.kaspresso.testcases.api.testcase.TestCase +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.tap.MainActivity import com.tangem.tap.domain.sdk.TangemSdkManager import dagger.hilt.android.testing.HiltAndroidRule +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.rules.RuleChain import org.junit.runner.RunWith @@ -24,6 +30,9 @@ abstract class BaseTestCase : TestCase( @Inject lateinit var tangemSdkManager: TangemSdkManager + @Inject + lateinit var appPreferencesStore: AppPreferencesStore + @get:Rule open val composeTestRule = createAndroidComposeRule() @@ -48,9 +57,22 @@ abstract class BaseTestCase : TestCase( hiltRule.inject() Intents.init() additionalBeforeSection() + + runBlocking { + delay(INIT_DELAY) + } + }.after { + runBlocking { + appPreferencesStore.editData { prefs -> prefs.clear() } + } + additionalAfterSection() Intents.release() } + companion object { + private const val INIT_DELAY = 1000L + } + } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt index 8c4bd6d3c9..dd86a046cc 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt @@ -143,9 +143,6 @@ class DetailsScreenTest : BaseTestCase() { } } ComposeScreen.onComposeScreen(composeTestRule) { - step("Assert Link more cards button does not exist") { - linkMoreCardsButton.assertIsNotDisplayed() - } step("Assert Card Settings button is visible") { cardSettingsButton.assertIsDisplayed() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt index 9eb88b0075..eab4389f6b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -1,6 +1,7 @@ package com.tangem.tests import android.content.Intent.ACTION_VIEW +import androidx.test.espresso.intent.matcher.UriMatchers import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion import com.tangem.screens.DisclaimerTestScreen @@ -9,6 +10,7 @@ import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.kakao.intent.KIntent +import org.hamcrest.Matchers import org.junit.Test @HiltAndroidTest @@ -29,7 +31,7 @@ class StoriesTest : BaseTestCase() { step("Assert: browser opened") { val expectedIntent = KIntent { hasAction(ACTION_VIEW) - hasData(NEW_BUY_WALLET_URL) + hasData { toString().startsWith(NEW_BUY_WALLET_URL) } } expectedIntent.intended() device.uiDevice.pressBack() diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt index 23379f7e5b..9e5dbd53c8 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -39,10 +39,6 @@ internal class DefaultBlockchainSDKFactory( private val mainScope = CoroutineScope(dispatchers.main) private val walletManagerFactory: Flow = createWalletManagerFactory() - // TODO: [REDACTED_JIRA] - // private val walletManagerFactory: Flow by lazy(LazyThreadSafetyMode.NONE) { - // createWalletManagerFactory() - // } override suspend fun init() { coroutineScope { @@ -59,7 +55,7 @@ internal class DefaultBlockchainSDKFactory( // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] transform = walletManagerFactoryCreator::create, ) - .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) + .stateIn(scope = mainScope, started = SharingStarted.Lazily, initialValue = null) } private fun CoroutineScope.updateBlockchainProviderTypes() { From c6a5780867f9618fe30a7316789d622c6ee0b224 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Oct 2024 12:18:25 +0500 Subject: [PATCH 05/18] Updated on 2026-08-14 --- app/src/main/assets/testnet_tokens.json | 10 ++++++++++ .../core/ui/extensions/BlockchainIcons.kt | 5 +++++ .../ui/src/main/res/drawable/ic_casper_22.xml | 9 +++++++++ .../src/main/res/drawable/img_casper_22.xml | 19 +++++++++++++++++++ .../tangem/blockchainsdk/utils/Blockchain.kt | 10 ++++++---- 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_casper_22.xml create mode 100644 core/ui/src/main/res/drawable/img_casper_22.xml diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index 3badefc007..30fa60f3c4 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -748,6 +748,16 @@ "networkId": "core/test" } ] + }, + { + "id": "casper-network", + "name": "Casper", + "symbol": "CSPR", + "networks": [ + { + "networkId": "casper-network/test" + } + ] } ] } diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 9c460e02a1..1270dd2741 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -79,6 +79,7 @@ fun getActiveIconRes(blockchainId: String): Int { "energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22 "energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22 "core", "core/test" -> R.drawable.img_core_22 + "casper-network", "casper-network/test" -> R.drawable.img_casper_22 else -> R.drawable.ic_alert_24 } } @@ -159,6 +160,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22 "energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22 "core", "core/test" -> R.drawable.img_core_22 + "casper-network", "casper-network/test" -> R.drawable.img_casper_22 else -> R.drawable.ic_alert_24 } } @@ -236,6 +238,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22 "energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22 "core", "core/test" -> R.drawable.img_core_22 + "casper-network" -> R.drawable.img_casper_22 else -> R.drawable.ic_alert_24 } } @@ -316,6 +319,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "energy-web-chain", "energy-web-chain/test" -> R.drawable.ic_energy_web_22 "energy-web-x", "energy-web-x/test" -> R.drawable.ic_energy_web_22 "core", "core/test" -> R.drawable.ic_core_22 + "casper-network", "casper-network/test" -> R.drawable.ic_casper_22 else -> R.drawable.ic_alert_24 } } @@ -396,6 +400,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "energy-web-chain", "energy-web-chain/test" -> R.drawable.ic_energy_web_22 "energy-web-x", "energy-web-x/test" -> R.drawable.ic_energy_web_22 "core", "core/test" -> R.drawable.ic_core_22 + "casper-network", "casper-network/test" -> R.drawable.ic_casper_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_casper_22.xml b/core/ui/src/main/res/drawable/ic_casper_22.xml new file mode 100644 index 0000000000..e2abfc22ea --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_casper_22.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_casper_22.xml b/core/ui/src/main/res/drawable/img_casper_22.xml new file mode 100644 index 0000000000..ade71437f7 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_casper_22.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index c8e9fca637..3132bfa610 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -136,6 +136,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "add_later_test" -> Blockchain.CasperTestnet "core" -> Blockchain.Core "core/test" -> Blockchain.CoreTestnet + "casper-network" -> Blockchain.Casper + "casper-network/test" -> Blockchain.CasperTestnet else -> null } } @@ -269,8 +271,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.EnergyWebChainTestnet -> "energy-web-chain/test" Blockchain.EnergyWebX -> "energy-web-x" Blockchain.EnergyWebXTestnet -> "energy-web-x/test" - Blockchain.Casper -> "add_later" - Blockchain.CasperTestnet -> "add_later_test" + Blockchain.Casper -> "casper-network" + Blockchain.CasperTestnet -> "casper-network/test" Blockchain.Core -> "core" Blockchain.CoreTestnet -> "core/test" } @@ -360,8 +362,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Sui, Blockchain.SuiTestnet -> "sui" Blockchain.EnergyWebChain, Blockchain.EnergyWebChainTestnet -> "energy-web-token" Blockchain.EnergyWebX, Blockchain.EnergyWebXTestnet -> "energy-web-token" - Blockchain.Casper -> "add_later" - Blockchain.CasperTestnet -> "add_later_test" + Blockchain.Casper, Blockchain.CasperTestnet -> "casper-network" Blockchain.Core, Blockchain.CoreTestnet -> "coredaoorg" } } @@ -386,6 +387,7 @@ fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? { Blockchain.Near, Blockchain.NearTestnet -> 0.00182.toBigDecimal() Blockchain.Aptos, Blockchain.AptosTestnet, Blockchain.Filecoin, + Blockchain.Casper, Blockchain.CasperTestnet, -> BigDecimal.ZERO else -> null } From ca2c82d15eb0a34b68cd94350c9dfb29a701d642 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Oct 2024 18:22:06 +0500 Subject: [PATCH 06/18] Updated on 2026-08-14 --- .../datasource/api/onramp/MockedOnrampApi.kt | 178 ++++++++++++++++++ .../com/tangem/datasource/di/NetworkModule.kt | 33 ++-- 2 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/onramp/MockedOnrampApi.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/MockedOnrampApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/MockedOnrampApi.kt new file mode 100644 index 0000000000..a960766de8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/MockedOnrampApi.kt @@ -0,0 +1,178 @@ +package com.tangem.datasource.api.onramp + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO +import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest +import com.tangem.datasource.api.onramp.models.response.OnrampDataResponse +import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse +import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse +import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO +import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO +import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO +import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO + +internal class MockedOnrampApi : OnrampApi { + override suspend fun getCurrencies(): ApiResponse> = ApiResponse.Success( + COUNTRIES.map(OnrampCountryDTO::defaultCurrency), + ) + + override suspend fun getCountries(): ApiResponse> = ApiResponse.Success(COUNTRIES + RUSSIA) + + override suspend fun getCountryByIp(): ApiResponse = ApiResponse.Success(RUSSIA) + + override suspend fun getPaymentMethods(): ApiResponse> = ApiResponse.Success( + listOf( + PaymentMethodDTO(id = "google", name = "Google Play", image = ""), + PaymentMethodDTO(id = "apple", name = "Apple Pay", image = ""), + PaymentMethodDTO(id = "card", name = "Card", image = ""), + ), + ) + + override suspend fun getPairs(body: OnrampPairsRequest): ApiResponse> = ApiResponse.Success( + listOf( + OnrampPairDTO( + fromCurrencyCode = "USD", + to = OnrampDestinationDTO(contractAddress = "0xcontract_address", network = "ethereum"), + providers = listOf(), + ), + ), + ) + + override suspend fun getQuote( + fromCurrencyCode: String, + toContractAddress: String, + toNetwork: String, + paymentMethod: String, + countryCode: String, + fromAmount: String, + toDecimals: Int, + providerId: String, + ): ApiResponse { + TODO("Not yet implemented") + } + + override suspend fun getData( + fromCurrencyCode: String, + toContractAddress: String, + toNetwork: String, + paymentMethod: String, + countryCode: String, + fromAmount: String, + toDecimals: Int, + providerId: String, + toAddress: String, + redirectUrl: String, + language: String?, + theme: String?, + requestId: String, + ): ApiResponse { + TODO("Not yet implemented") + } + + override suspend fun getStatus(txId: String): ApiResponse { + TODO("Not yet implemented") + } + + private companion object { + private val RUSSIA = OnrampCountryDTO( + name = "Russia", + code = "RU", + image = "https://hatscripts.github.io/circle-flags/flags/ru.svg", + alpha3 = "RUS", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "Russian ruble", + code = "RUB", + image = "https://hatscripts.github.io/circle-flags/flags/ru.svg", + precision = 2, + ), + onrampAvailable = false, + ) + private val COUNTRIES = listOf( + OnrampCountryDTO( + name = "United States of America", + code = "USA", + image = "https://hatscripts.github.io/circle-flags/flags/us.svg", + alpha3 = "USA", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "US Dollar", + code = "USD", + image = "https://hatscripts.github.io/circle-flags/flags/us.svg", + precision = 2, + ), + onrampAvailable = true, + ), + OnrampCountryDTO( + name = "Europe Union", + code = "EU", + image = "https://hatscripts.github.io/circle-flags/flags/eu.svg", + alpha3 = "EUR", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "Euro", + code = "EUR", + image = "https://hatscripts.github.io/circle-flags/flags/eu.svg", + precision = 2, + ), + onrampAvailable = true, + ), + OnrampCountryDTO( + name = "Great Britain", + code = "GB", + image = "https://hatscripts.github.io/circle-flags/flags/gb.svg", + alpha3 = "GB", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "British Pound Sterling", + code = "GBP", + image = "https://hatscripts.github.io/circle-flags/flags/gb.svg", + precision = 2, + ), + onrampAvailable = true, + ), + OnrampCountryDTO( + name = "CANADA", + code = "CA", + image = "https://hatscripts.github.io/circle-flags/flags/ca.svg", + alpha3 = "CA", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "Canadian Dollar", + code = "CAD", + image = "https://hatscripts.github.io/circle-flags/flags/ca.svg", + precision = 2, + ), + onrampAvailable = true, + ), + OnrampCountryDTO( + name = "Hon Kong", + code = "HK", + image = "https://hatscripts.github.io/circle-flags/flags/hk.svg", + alpha3 = "HK", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "Hon Kong Dollar", + code = "HKD", + image = "https://hatscripts.github.io/circle-flags/flags/hk.svg", + precision = 2, + ), + onrampAvailable = true, + ), + OnrampCountryDTO( + name = "Australia", + code = "AU", + image = "https://hatscripts.github.io/circle-flags/flags/au.svg", + alpha3 = "AU", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "Australian Dollar", + code = "AUD", + image = "https://hatscripts.github.io/circle-flags/flags/au.svg", + precision = 2, + ), + onrampAvailable = true, + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 8b38ca983a..720e14ac0b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -11,6 +11,7 @@ import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.markets.TangemTechMarketsApi +import com.tangem.datasource.api.onramp.MockedOnrampApi import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -98,22 +99,24 @@ internal object NetworkModule { @Provides @Singleton fun provideOnrampApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, + // @NetworkMoshi moshi: Moshi, + // @ApplicationContext context: Context, + // apiConfigsManager: ApiConfigsManager, + // appLogsStore: AppLogsStore, ): OnrampApi { - return createApi( - id = ApiConfig.ID.Express, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ) - }, - ) + // TODO: Remove when backend will be ready - [REDACTED_TASK_KEY] + return MockedOnrampApi() + // return createApi( + // id = ApiConfig.ID.Express, + // moshi = moshi, + // context = context, + // apiConfigsManager = apiConfigsManager, + // clientBuilder = { + // addInterceptor( + // NetworkLogsSaveInterceptor(appLogsStore), + // ) + // }, + // ) } @Provides From 095587921d505a5e4565efecb4b0a9a3959a542a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Oct 2024 12:33:12 +0300 Subject: [PATCH 07/18] Updated on 2026-08-14 --- .../TangemBottomSheetScaffold.kt | 81 +++++++++------- .../com/tangem/core/ui/extensions/Shadow.kt | 95 +++++++++++++++++++ .../markets/portfolio/impl/ui/MyPortfolio.kt | 26 ++++- .../presentation/wallet/ui/WalletScreen.kt | 1 - 4 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/extensions/Shadow.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt index aa68f71190..ccab322580 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt @@ -2,30 +2,57 @@ package com.tangem.core.ui.components.sheetscaffold +import android.graphics.Bitmap +import android.graphics.BlurMaskFilter +import android.renderscript.Allocation +import android.renderscript.Element +import android.renderscript.RenderScript +import android.renderscript.ScriptIntrinsicBlur import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.gestures.DraggableAnchors import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.contentColorFor import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.composed +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.* import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMaxOfOrNull +import androidx.compose.ui.zIndex +import androidx.core.graphics.withSave +import androidx.core.graphics.withTranslation import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue.* +import com.tangem.core.ui.extensions.softLayerShadow import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.toPx import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.pow import kotlin.math.roundToInt /** @@ -51,13 +78,6 @@ import kotlin.math.roundToInt * [Dp.Unspecified] for a sheet that spans the entire screen width. * @param sheetShape the shape of the bottom sheet * @param sheetContainerColor the background color of the bottom sheet - * @param sheetContentColor the preferred content color provided by the bottom sheet to its - * children. Defaults to the matching content color for [sheetContainerColor], or if that is not a - * color from the theme, this will keep the same content color set above the bottom sheet. - * @param sheetTonalElevation when [sheetContainerColor] is [ColorScheme.surface], a translucent - * primary color overlay is applied on top of the container. A higher tonal elevation value will - * result in a darker color in light theme and lighter color in dark theme. See also: [Surface]. - * @param sheetShadowElevation the shadow elevation of the bottom sheet * @param sheetSwipeEnabled whether the sheet swiping is enabled and should react to the user's * input * @param topBar top app bar of the screen, typically a [SmallTopAppBar] @@ -81,10 +101,7 @@ fun TangemBottomSheetScaffold( sheetPeekHeight: Dp, sheetMaxWidth: Dp = 640.dp, sheetShape: Shape = TangemTheme.shapes.bottomSheetLarge, - sheetContainerColor: Color = Color.White, // FIXME - sheetContentColor: Color = contentColorFor(sheetContainerColor), - sheetTonalElevation: Dp = 0.dp, - sheetShadowElevation: Dp = 1.dp, + sheetContainerColor: Color = Color.White, sheetSwipeEnabled: Boolean = true, topBar: @Composable (() -> Unit)? = null, snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, @@ -109,9 +126,6 @@ fun TangemBottomSheetScaffold( sheetSwipeEnabled = sheetSwipeEnabled, shape = sheetShape, containerColor = sheetContainerColor, - contentColor = sheetContentColor, - tonalElevation = sheetTonalElevation, - shadowElevation = sheetShadowElevation, content = sheetContent, ) }, @@ -178,9 +192,6 @@ private fun StandardBottomSheet( sheetSwipeEnabled: Boolean, shape: Shape, containerColor: Color, - contentColor: Color, - tonalElevation: Dp, - shadowElevation: Dp, content: @Composable ColumnScope.() -> Unit, ) { val scope = rememberCoroutineScope() @@ -202,7 +213,7 @@ private fun StandardBottomSheet( Modifier } - Surface( + Column( modifier = Modifier .widthIn(max = sheetMaxWidth) .fillMaxWidth() @@ -251,16 +262,20 @@ private fun StandardBottomSheet( state = state.anchoredDraggableState, orientation = orientation, enabled = sheetSwipeEnabled, - ), - shape = shape, - color = containerColor, - contentColor = contentColor, - tonalElevation = tonalElevation, - shadowElevation = shadowElevation, + ) + .softLayerShadow( + radius = 8.dp, + color = Color.Black.copy( + alpha = if (isSystemInDarkTheme()) .16f else .08f + ), + shape = shape, + offset = DpOffset(x = 0.dp, y = (-4).dp), + isAlphaContentClip = true + ) + .background(containerColor, shape) + .clip(shape), ) { - Column(Modifier.fillMaxWidth()) { - content() - } + content() } } @@ -293,7 +308,7 @@ private fun BottomSheetScaffoldLayout( ), ) { (topBarMeasurables, bodyMeasurables, bottomSheetMeasurables, snackbarHostMeasurables), - constraints, + constraints, -> val layoutWidth = constraints.maxWidth val layoutHeight = constraints.maxHeight @@ -321,7 +336,7 @@ private fun BottomSheetScaffoldLayout( PartiallyExpanded -> sheetOffset().roundToInt() - snackbarHeight Expanded, Hidden, - -> layoutHeight - snackbarHeight + -> layoutHeight - snackbarHeight } // Placement order is important for elevation diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Shadow.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Shadow.kt new file mode 100644 index 0000000000..bc1ce31389 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/Shadow.kt @@ -0,0 +1,95 @@ +package com.tangem.core.ui.extensions + +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +fun Modifier.softLayerShadow( + radius: Dp = 8.dp, + color: Color = Color.Black.copy(alpha = .23f), + shape: Shape = RectangleShape, + spread: Dp = 0.dp, + offset: DpOffset = DpOffset(x = 0.dp, y = 2.dp), + isAlphaContentClip: Boolean = false, +): Modifier = this.drawWithCache { + val radiusPx = radius.toPx() + require(radiusPx > 0.0F) + val paint = Paint().apply { + this.color = color + + asFrameworkPaint().apply { + isDither = true + isAntiAlias = true + + setShadowLayer( + radiusPx, + offset.x.toPx(), + offset.y.toPx(), + color.toArgb(), + ) + } + } + val shapeOutline = shape.createOutline( + size = size, + layoutDirection = LayoutDirection.Rtl, + density = this, + ) + val shapePath = Path().apply { + addOutline(outline = shapeOutline) + } + + val drawShadowBlock: DrawScope.() -> Unit = { + drawIntoCanvas { canvas -> + canvas.withSave { + if (spread.value != 0.0F) { + canvas.scale( + sx = spreadScale( + spread = spread.toPx(), + size = size.width, + ), + sy = spreadScale( + spread = spread.toPx(), + size = size.height, + ), + pivotX = center.x, + pivotY = center.y, + ) + } + + canvas.drawOutline( + outline = shapeOutline, + paint = paint, + ) + } + } + } + + onDrawBehind { + if (isAlphaContentClip) { + clipShadowByPath( + path = shapePath, + block = drawShadowBlock, + ) + } else { + drawShadowBlock() + } + } +} + +@Suppress("UnnecessaryParentheses") +private fun spreadScale(spread: Float, size: Float): Float = 1.0F + ((spread / size) * 2.0F) + +private fun DrawScope.clipShadowByPath(path: Path, block: DrawScope.() -> Unit) { + clipPath( + path = path, + clipOp = ClipOp.Difference, + block = block, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index daaa385754..abd9b2ccdc 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -8,12 +8,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.key +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SmallButtonShimmer import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.information.InformationBlock @@ -76,10 +78,26 @@ private fun Title() { private fun AddButton(state: AddButtonState, onClick: () -> Unit) { when (state) { AddButtonState.Loading -> { - SmallButtonShimmer( - modifier = Modifier.size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18), - shape = RoundedCornerShape(TangemTheme.dimens.radius3), - ) + Box { + SmallButtonShimmer( + modifier = Modifier.width(width = TangemTheme.dimens.size63), + shape = RoundedCornerShape(TangemTheme.dimens.radius3), + withIcon = true, + ) + + Box( + Modifier + .matchParentSize() + .background(TangemTheme.colors.background.action), + ) + + RectangleShimmer( + modifier = Modifier + .align(Alignment.Center) + .size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18), + radius = TangemTheme.dimens.radius3, + ) + } } AddButtonState.Available, AddButtonState.Unavailable, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index f9f733cd80..1d289a12df 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -391,7 +391,6 @@ private inline fun BaseScaffoldWithMarkets( sheetContainerColor = backgroundColor.value, scaffoldState = scaffoldState, sheetPeekHeight = peekHeight, - sheetShadowElevation = 8.dp, sheetShape = TangemTheme.shapes.bottomSheetLarge, sheetContent = { // hide bottom sheet when back pressed From 3183f5b955e9930253cb14093b78859d29cedb9d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Oct 2024 19:44:53 +0500 Subject: [PATCH 08/18] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../com/tangem/tap/ApplicationEntryPoint.kt | 3 ++ .../java/com/tangem/tap/TangemApplication.kt | 5 +++ .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../configs/feature_toggles_config.json | 4 ++ features/onramp/api/.gitignore | 1 + features/onramp/api/build.gradle.kts | 16 ++++++++ .../features/onramp/OnrampFeatureToggles.kt | 6 +++ .../onramp/component/OnrampComponent.kt | 9 +++++ features/onramp/impl/.gitignore | 1 + features/onramp/impl/build.gradle.kts | 38 +++++++++++++++++++ .../onramp/DefaultOnrampFeatureToggles.kt | 11 ++++++ .../features/onramp/di/FeatureModule.kt | 21 ++++++++++ settings.gradle.kts | 3 ++ 14 files changed, 122 insertions(+) create mode 100644 features/onramp/api/.gitignore create mode 100644 features/onramp/api/build.gradle.kts create mode 100644 features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt create mode 100644 features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt create mode 100644 features/onramp/impl/.gitignore create mode 100644 features/onramp/impl/build.gradle.kts create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/FeatureModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0412c6eacf..d9d1aaef00 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -143,6 +143,8 @@ dependencies { implementation(projects.features.walletSettings.impl) implementation(projects.features.markets.api) implementation(projects.features.markets.impl) + implementation(projects.features.onramp.api) + implementation(projects.features.onramp.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 7d37eacaf5..a425711dd5 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -31,6 +31,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository @@ -119,4 +120,6 @@ interface ApplicationEntryPoint { fun getHomeFeatureToggles(): HomeFeatureToggles fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase + + fun getOnrampFeatureToggles(): OnrampFeatureToggles } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 0b5a37f14d..530337b2cd 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -45,6 +45,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler @@ -189,6 +190,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val getUserCountryUseCase: GetUserCountryUseCase get() = entryPoint.getGetUserCountryCodeUseCase() + + private val onrampFeatureToggles: OnrampFeatureToggles + get() = entryPoint.getOnrampFeatureToggles() // endregion override fun onCreate() { @@ -272,6 +276,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { transactionSignerFactory = transactionSignerFactory, homeFeatureToggles = homeFeatureToggles, getUserCountryUseCase = getUserCountryUseCase, + onrampFeatureToggles = onrampFeatureToggles, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 56c13b9a41..8d6b257f74 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -30,6 +30,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -87,4 +88,5 @@ data class DaggerGraphState( val transactionSignerFactory: TransactionSignerFactory? = null, val homeFeatureToggles: HomeFeatureToggles? = null, val getUserCountryUseCase: GetUserCountryUseCase? = null, + val onrampFeatureToggles: OnrampFeatureToggles? = null, ) : StateType \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 0aa128c712..97119cabb4 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -22,5 +22,9 @@ { "name": "MIGRATE_USER_COUNTRY_CODE_ENABLED", "version": "5.17.0" + }, + { + "name": "ONRAMP_ENABLED", + "version": "undefined" } ] diff --git a/features/onramp/api/.gitignore b/features/onramp/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/onramp/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/onramp/api/build.gradle.kts b/features/onramp/api/build.gradle.kts new file mode 100644 index 0000000000..dfd87a2e88 --- /dev/null +++ b/features/onramp/api/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("kotlin-parcelize") + id("configuration") +} + +android { + namespace = "com.tangem.features.onramp.api" +} + +dependencies { + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) +} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt new file mode 100644 index 0000000000..7d46cb6ad1 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.features.onramp + +interface OnrampFeatureToggles { + + val isFeatureEnabled: Boolean +} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt new file mode 100644 index 0000000000..b5cd6141fb --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt @@ -0,0 +1,9 @@ +package com.tangem.features.onramp.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface OnrampComponent : ComposableContentComponent { + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/.gitignore b/features/onramp/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/onramp/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts new file mode 100644 index 0000000000..37ef2d093d --- /dev/null +++ b/features/onramp/impl/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.onramp.impl" +} + +dependencies { + /** Project - API */ + implementation(projects.features.onramp.api) + + /** Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.featuretoggles) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /* AndroidX */ + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.compose) + + /* Compose */ + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.shimmer) + implementation(deps.compose.coil) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt new file mode 100644 index 0000000000..15d7dd6713 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onramp + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultOnrampFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : OnrampFeatureToggles { + + override val isFeatureEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("ONRAMP_ENABLED") +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/FeatureModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/FeatureModule.kt new file mode 100644 index 0000000000..2c39321176 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/FeatureModule.kt @@ -0,0 +1,21 @@ +package com.tangem.features.onramp.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.onramp.DefaultOnrampFeatureToggles +import com.tangem.features.onramp.OnrampFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object FeatureModule { + + @Provides + @Singleton + fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): OnrampFeatureToggles { + return DefaultOnrampFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index e8e6fe3fcf..f3b70b2d8b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -198,6 +198,9 @@ include(":features:wallet-settings:impl") include(":features:markets:api") include(":features:markets:impl") + +include(":features:onramp:api") +include(":features:onramp:impl") // endregion Feature modules // region Domain modules From 7ca9946f476d616b430e12cd54106e6557356175 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Oct 2024 12:20:15 +0300 Subject: [PATCH 09/18] Updated on 2026-08-14 --- .../converters/AmountStateConverter.kt | 7 +- .../common/ui/amountScreen/ui/AmountBlock.kt | 11 +- .../common/ui/amountScreen/ui/AmountField.kt | 8 +- .../common/ui/notifications/NotificationUM.kt | 16 +- .../ui/notifications/NotificationsFactory.kt | 26 +-- .../ui/tokens/TokenItemStateConverter.kt | 10 +- .../inputrow/InputRowImageSelector.kt | 5 +- .../bigdecimal/BigDecimalCryptoFormat.kt | 33 +++- .../bigdecimal/BigDecimalSimpleFormat.kt | 34 ++++ .../core/ui/utils/BigDecimalFormatter.kt | 159 ------------------ .../bigdecimal/BigDecimalCryptoFormatTest.kt | 34 ++++ .../utils/CryptoCurrencyFormatExtensions.kt | 43 ----- .../usecase/SendTransactionUseCase.kt | 5 +- .../impl/model/MarketsTokenDetailsModel.kt | 16 +- .../impl/model/formatter/Formatters.kt | 9 +- .../block/impl/model/TokenMarketBlockModel.kt | 7 +- .../converters/MarketsTokenItemConverter.kt | 6 +- .../SendRecipientHistoryListConverter.kt | 8 +- .../presentation/ui/SendNavigationButtons.kt | 17 +- .../ui/fee/SendSpeedSelectorItem.kt | 15 +- .../impl/presentation/ui/send/FeeBlock.kt | 15 +- .../state/converters/BalanceItemConverter.kt | 7 +- .../RewardsValidatorStateConverter.kt | 9 +- .../converters/YieldBalancesConverter.kt | 7 +- .../SetInitialDataStateTransformer.kt | 26 +-- .../AmountRequirementStateTransformer.kt | 11 +- .../ShowApprovalBottomSheetTransformer.kt | 10 +- .../ui/StakingClaimRewardsValidatorContent.kt | 8 +- .../ui/StakingInitialInfoContent.kt | 8 +- .../ui/StakingValidatorListContent.kt | 5 +- .../presentation/ui/block/StakingFeeBlock.kt | 15 +- .../presentation/ui/block/ValidatorBlock.kt | 5 +- .../feature/swap/domain/models/SwapAmount.kt | 5 - .../swap/domain/models/ui/AmountFormatter.kt | 11 +- .../feature/swap/domain/SwapInteractorImpl.kt | 2 +- .../swap/converters/TokensDataConverter.kt | 10 +- .../feature/swap/di/SwapSingletonModule.kt | 18 ++ .../swap/di/impl/DefaultAmountFormatter.kt | 18 ++ .../tangem/feature/swap/ui/StateBuilder.kt | 24 ++- .../tangem/feature/swap/utils/SwapUtils.kt | 7 + .../feature/swap/viewmodels/SwapViewModel.kt | 2 +- ...TokenDetailsBalanceSelectStateConverter.kt | 4 +- .../TokenDetailsLoadedBalanceConverter.kt | 17 +- .../TokenDetailsNotificationConverter.kt | 24 +-- ...enDetailsSwapTransactionsStateConverter.kt | 12 +- ...tailsTxHistoryTransactionStateConverter.kt | 5 +- .../domain/WalletAdditionalInfoFactory.kt | 11 +- .../SetBalancesAndLimitsTransformer.kt | 18 +- .../BalancesAndLimitsBottomSheetConverter.kt | 9 +- .../SingleWalletMarketPriceConverter.kt | 4 +- .../converter/TxHistoryItemStateConverter.kt | 5 +- .../VisaTxDetailsBottomSheetConverter.kt | 8 +- .../VisaTxHistoryItemStateConverter.kt | 8 +- 53 files changed, 355 insertions(+), 462 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalSimpleFormat.kt delete mode 100644 core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapSingletonModule.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/di/impl/DefaultAmountFormatter.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index 410a23b468..3445c4859f 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -3,13 +3,14 @@ package com.tangem.common.ui.amountScreen.converters import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter -import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -49,7 +50,7 @@ class AmountStateConverter( val appCurrency = appCurrencyProvider() val status = cryptoCurrencyStatusProvider() val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol) - val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals) + val crypto = status.value.amount.format { crypto(status.currency) } val noFeeRate = status.value.fiatRate.isNullOrZero() return AmountState.Data( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index b2903b20cc..bcac8bb21b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -19,16 +19,20 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.currency.icon.CurrencyIcon -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.format.bigdecimal.anyDecimals +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter +import java.math.BigDecimal @Composable fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit) { if (amountState !is AmountState.Data) return val amount = amountState.amountTextField - val cryptoAmount = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) + val cryptoAmount = formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) val fiatAmount = BigDecimalFormatter.formatFiatAmount( fiatAmount = amount.fiatAmount.value, fiatCurrencySymbol = amount.fiatAmount.currencySymbol, @@ -77,6 +81,9 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis } } +fun formatWithSymbol(amount: String, symbol: String) = + BigDecimal.ZERO.format { crypto(symbol, 0).anyDecimals() }.replace("0", amount) + // region Preview @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index d8347d67af..c74f183aa6 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -24,6 +24,8 @@ import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.rememberDecimalFormat @@ -92,11 +94,7 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri ), ) { val text = if (amountField.isFiatValue) { - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = secondaryAmount.value, - cryptoCurrency = secondaryAmount.currencySymbol, - decimals = secondaryAmount.decimals, - ) + secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) } } else { BigDecimalFormatter.formatFiatAmount( fiatAmount = secondaryAmount.value, diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index accc615136..8ca24539b5 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -6,7 +6,9 @@ import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.shorted import java.math.BigDecimal sealed class NotificationUM(val config: NotificationConfig) { @@ -267,8 +269,8 @@ sealed class NotificationUM(val config: NotificationConfig) { subtitle = resourceReference( R.string.koinos_insufficient_mana_to_send_koin_description, formatArgs = wrappedList( - BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()), - BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()), + mana.format { crypto("", Blockchain.Koinos.decimals()).shorted() }, + maxMana.format { crypto("", Blockchain.Koinos.decimals()).shorted() }, ), ), ) @@ -286,11 +288,9 @@ sealed class NotificationUM(val config: NotificationConfig) { subtitle = resourceReference( R.string.koinos_mana_exceeds_koin_balance_description, formatArgs = wrappedList( - BigDecimalFormatter.formatCryptoAmount( - availableKoinForTransfer, - Blockchain.Koinos.currency, - Blockchain.Koinos.decimals(), - ), + availableKoinForTransfer.format { + crypto(Blockchain.Koinos.currency, Blockchain.Koinos.decimals()) + }, ), ), buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 36c15ab3c6..51df42310b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -6,7 +6,9 @@ import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency @@ -65,10 +67,9 @@ object NotificationsFactory { if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) { add( NotificationUM.Error.ReserveAmount( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = sendingAmount, - cryptoCurrency = cryptoCurrency, - ), + sendingAmount.format { + crypto(cryptoCurrency) + }, ), ) } @@ -87,10 +88,7 @@ object NotificationsFactory { NotificationUM.Error.TransactionLimitError( cryptoCurrency = cryptoCurrency.name, utxoLimit = utxoLimit.maxLimit.toPlainString(), - amountLimit = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = utxoLimit.maxAmount, - cryptoCurrency = cryptoCurrency, - ), + amountLimit = utxoLimit.maxAmount.format { crypto(cryptoCurrency) }, onConfirmClick = { onReduceClick( utxoLimit.maxAmount, @@ -124,10 +122,7 @@ object NotificationsFactory { if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) { add( NotificationUM.Error.ExistentialDeposit( - deposit = BigDecimalFormatter.formatCryptoAmountUncapped( - cryptoAmount = existentialDeposit, - cryptoCurrency = cryptoCurrency, - ), + deposit = existentialDeposit.format { crypto(cryptoCurrency).uncapped() }, onConfirmClick = { onReduceClick( existentialDeposit, @@ -155,10 +150,7 @@ object NotificationsFactory { if (isFeeCoverage) { add( NotificationUM.Warning.FeeCoverageNotification( - cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped( - cryptoAmount = cryptoDiff, - cryptoCurrency = cryptoCurrency, - ), + cryptoAmount = cryptoDiff.format { crypto(cryptoCurrency).uncapped() }, fiatAmount = getFiatString( value = cryptoDiff, rate = fiatRate, diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 964b1a0929..913b07dd84 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -4,6 +4,9 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -80,7 +83,7 @@ class TokenItemStateConverter( private fun CryptoCurrencyStatus.getFormattedAmount(): String { val amount = value.amount?.plus(getStakedBalance()) ?: return DASH_SIGN - return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + return amount.format { crypto(currency) } } private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { @@ -167,10 +170,7 @@ class TokenItemStateConverter( return if (fiatRate != null && priceChange != null) { TokenItemState.SubtitleState.CryptoPriceContent( price = fiatRate.getFormattedCryptoPrice(appCurrency), - priceChangePercent = BigDecimalFormatter.formatPercent( - percent = priceChange, - useAbsoluteValue = true, - ), + priceChangePercent = priceChange.format { percent() }, type = priceChange.getPriceChangeType(), ) } else { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt index a9921a83b5..e94c8d5191 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt @@ -24,9 +24,10 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.annotatedReference import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import java.math.BigDecimal /** @@ -98,7 +99,7 @@ private fun InputRowImageSelectorPreview( append(" ") withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) { append( - BigDecimalFormatter.formatPercent(BigDecimal.ZERO, true), + BigDecimal.ZERO.format { percent() }, ) } }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt index 6973261be0..eea3130209 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt @@ -120,6 +120,26 @@ fun BigDecimalCryptoFormat.uncapped() = BigDecimalFormat { value -> ) } +/** + * Format for displaying crypto amounts with a fixed number of decimals. + */ +fun BigDecimalCryptoFormat.anyDecimals(maxDecimals: Int = decimals, minDecimals: Int = decimals) = + BigDecimalFormat { value -> + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = maxDecimals + minimumFractionDigits = minDecimals + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) + } + /** * Format for displaying fees. * If the fee is less than the threshold, it will be displayed as a fixed value "<0.000001 BTC", " { val withoutSymbol = str.dropLast(fiatCurrencySymbol.length) + + if (cryptoCurrencySymbol.isBlank()) { + return withoutSymbol + } + val last = withoutSymbol.lastOrNull() ?: return cryptoCurrencySymbol append(withoutSymbol) @@ -200,12 +225,16 @@ internal fun String.replaceFiatSymbolWithCrypto( append(CURRENCY_SPACE) } - append(cryptoCurrencySymbol) - val withoutSymbol = str.drop(fiatCurrencySymbol.length) val first = withoutSymbol.firstOrNull() ?: return cryptoCurrencySymbol + if (cryptoCurrencySymbol.isBlank()) { + return withoutSymbol + } + + append(cryptoCurrencySymbol) + if (first.isNotWhitespace()) { append(CURRENCY_SPACE) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalSimpleFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalSimpleFormat.kt new file mode 100644 index 0000000000..f6c5067d65 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalSimpleFormat.kt @@ -0,0 +1,34 @@ +package com.tangem.core.ui.format.bigdecimal + +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Locale + +open class BigDecimalSimpleFormat( + val decimals: Int, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormat { + + override fun invoke(value: BigDecimal): String = default()(value) +} + +// == Initializers == + +fun BigDecimalFormatScope.simple(decimals: Int, locale: Locale = Locale.getDefault()) = BigDecimalSimpleFormat( + decimals = decimals, + locale = locale, +) + +// == Formatters == + +fun BigDecimalSimpleFormat.default() = BigDecimalFormat { value -> + val formatter = NumberFormat.getInstance(locale).apply { + maximumFractionDigits = decimals + minimumFractionDigits = 0 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(value) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index fd6a353bb1..744a2d9bf3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -1,6 +1,5 @@ package com.tangem.core.ui.utils -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.LOWER_SIGN import com.tangem.utils.StringsSigns.TILDE_SIGN @@ -16,10 +15,8 @@ object BigDecimalFormatter { const val EMPTY_BALANCE_SIGN = DASH_SIGN private const val CAN_BE_LOWER_SIGN = LOWER_SIGN - private val FORMAT_THRESHOLD = BigDecimal("0.01") private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") - private val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001") private const val FIAT_MARKET_DEFAULT_DIGITS = 2 private const val FIAT_MARKET_EXTENDED_DIGITS = 6 @@ -27,138 +24,6 @@ object BigDecimalFormatter { private val usdCurrency = Currency.getInstance("USD") - @Deprecated("Use BigDecimal.format") - fun formatCryptoAmount( - cryptoAmount: BigDecimal?, - cryptoCurrency: String, - decimals: Int, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(cryptoAmount).let { - if (cryptoCurrency.isEmpty()) { - it - } else { - it + "\u2009$cryptoCurrency" - } - } - } - - @Deprecated("Use BigDecimal.format") - fun formatCryptoAmountShorted( - cryptoAmount: BigDecimal?, - cryptoCurrency: String, - decimals: Int, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = if (cryptoAmount.isMoreThanThreshold()) { - NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = 2 - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - } else { - NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6) - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.DOWN - } - } - - return formatter.format(cryptoAmount).let { - if (cryptoCurrency.isEmpty()) { - it - } else { - it + "\u2009$cryptoCurrency" - } - } - } - - @Deprecated("Use BigDecimal.format") - fun formatCryptoAmountUncapped( - cryptoAmount: BigDecimal?, - cryptoCurrency: CryptoCurrency, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = cryptoCurrency.decimals - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(cryptoAmount).let { - if (cryptoCurrency.symbol.isEmpty()) { - it - } else { - it + "\u2009${cryptoCurrency.symbol}" - } - } - } - - @Deprecated("Use BigDecimal.format") - fun formatCryptoFeeAmount( - cryptoAmount: BigDecimal?, - cryptoCurrency: String, - decimals: Int, - canBeLower: Boolean = false, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6) - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - - val amountFormatted = if (cryptoAmount.checkCryptoThreshold()) { - buildString { - append(CAN_BE_LOWER_SIGN) - append( - formatter.format(CRYPTO_FEE_FORMAT_THRESHOLD), - ) - } - } else { - buildString { - if (canBeLower) { - append(CAN_BE_LOWER_SIGN) - } - append(formatter.format(cryptoAmount)) - } - } - - return if (cryptoCurrency.isEmpty()) { - amountFormatted - } else { - amountFormatted + "\u2009$cryptoCurrency" - } - } - - @Deprecated("Use BigDecimal.format") - fun formatCryptoAmount( - cryptoAmount: BigDecimal?, - cryptoCurrency: CryptoCurrency, - locale: Locale = Locale.getDefault(), - ): String { - return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals, locale) - } - @Deprecated("Use BigDecimal.format") fun formatFiatAmount( fiatAmount: BigDecimal?, @@ -266,28 +131,6 @@ object BigDecimalFormatter { } } - @Deprecated("Use BigDecimal.format") - fun formatPercent( - percent: BigDecimal, - useAbsoluteValue: Boolean, - locale: Locale = Locale.getDefault(), - maxFractionDigits: Int = 2, - minFractionDigits: Int = 2, - ): String { - val formatter = NumberFormat.getPercentInstance(locale).apply { - maximumFractionDigits = maxFractionDigits - minimumFractionDigits = minFractionDigits - roundingMode = RoundingMode.HALF_UP - } - val value = if (useAbsoluteValue) percent.abs() else percent - - return formatter.format(value) - } - - fun formatWithSymbol(amount: String, symbol: String) = "$amount\u2009$symbol" - - private fun BigDecimal.isMoreThanThreshold() = this > FORMAT_THRESHOLD - private fun getCurrency(code: String): Currency { return runCatching { Currency.getInstance(code) } .getOrElse { e -> @@ -301,6 +144,4 @@ object BigDecimalFormatter { } private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD - - private fun BigDecimal.checkCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD } \ No newline at end of file diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt index cc6fa5e40e..a073607b8b 100644 --- a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt @@ -370,4 +370,38 @@ internal class BigDecimalCryptoFormatTest { Truth.assertThat(formatted) .isEqualTo("0.13".addSymbolWithSpaceLeft(symbol)) } + + // === anyDecimals() === + + @Test + fun `anyDecimals smoke`() { + val testValue = BigDecimal("0.123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 5, + locale = testLocale, + ).anyDecimals() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12341".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `anyDecimals zero`() { + val testValue = BigDecimal("0.123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 0, + locale = testLocale, + ).anyDecimals() + } + + Truth.assertThat(formatted) + .isEqualTo("0".addSymbolWithSpaceLeft(symbol)) + } } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt b/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt deleted file mode 100644 index ce1a6d2470..0000000000 --- a/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.utils - -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.DecimalFormat -import java.text.NumberFormat -import java.util.Locale - -// todo determine where to place this extensions -fun BigDecimal.toFormattedString( - decimals: Int, - roundingMode: RoundingMode = RoundingMode.DOWN, - locale: Locale = Locale.getDefault(), -): String { - val formatter = NumberFormat.getInstance(locale) as? DecimalFormat - val df = formatter?.apply { - maximumFractionDigits = decimals - minimumFractionDigits = 0 - isGroupingUsed = true - this.roundingMode = roundingMode - } - return df?.format(this) ?: this.toPlainString() -} - -@Suppress("MagicNumber") -fun BigDecimal.toFormattedCurrencyString( - decimals: Int, - currency: String? = null, - roundingMode: RoundingMode = RoundingMode.DOWN, - limitNumberOfDecimals: Boolean = true, -): String { - val decimalsForRounding = if (limitNumberOfDecimals) { - if (decimals > 8) 8 else decimals - } else { - decimals - } - val formattedAmount = this.toFormattedString( - decimals = decimalsForRounding, - roundingMode = roundingMode, - ) - val formattedCurrency = currency?.let { " $it" } ?: "" - return "$formattedAmount$formattedCurrency" -} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 3fb6ba5bc1..3d3f3063c6 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -11,6 +11,8 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.network.ResultChecker import com.tangem.common.core.TangemSdkError import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.simple import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemTwins @@ -24,7 +26,6 @@ import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_C import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.sdk.extensions.localizedDescriptionRes -import com.tangem.utils.toFormattedString class SendTransactionUseCase( private val demoConfig: DemoConfig, @@ -120,7 +121,7 @@ class SendTransactionUseCase( is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error) is BlockchainSdkError.CreateAccountUnderfunded -> { val minAmount = error.minReserve - val minValue = minAmount.value?.toFormattedString(minAmount.decimals).orEmpty() + val minValue = minAmount.value?.format { simple(minAmount.decimals) }.orEmpty() SendTransactionError.CreateAccountUnderfunded(minValue) } else -> { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index b956de8357..3300e07216 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -17,6 +17,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -172,12 +174,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( fiatCurrencySymbol = currentAppCurrency.value.symbol, ), dateTimeText = resourceReference(R.string.common_today), - priceChangePercentText = params.token.tokenQuotes.h24Percent?.let { - BigDecimalFormatter.formatPercent( - percent = it, - useAbsoluteValue = true, - ) - }, + priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() }, priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), iconUrl = params.token.imageUrl, chartState = MarketsTokenDetailsUM.ChartState( @@ -472,12 +469,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval) - val percentText = percent?.let { - BigDecimalFormatter.formatPercent( - percent = it, - useAbsoluteValue = true, - ) - } ?: "" + val percentText = percent?.format { percent() } ?: "" state.update { stateToUpdate -> stateToUpdate.copy( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt index 4c185c04e3..890606701f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt @@ -2,6 +2,8 @@ package com.tangem.features.markets.details.impl.model.formatter import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PriceChangeInterval @@ -28,12 +30,7 @@ internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInte PriceChangeInterval.ALL_TIME -> allTimeChangePercent } - return percent?.let { - BigDecimalFormatter.formatPercent( - percent = it, - useAbsoluteValue = true, - ) - } ?: "" + return percent?.format { percent() } ?: "" } internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index d0ca34298a..35346aeb94 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -11,6 +11,8 @@ 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.components.marketprice.PriceChangeType +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -91,10 +93,7 @@ internal class TokenMarketBlockModel @Inject constructor( // TODO get currency from quotes use case [REDACTED_TASK_KEY] fiatCurrencySymbol = currentAppCurrency.value.symbol, ), - h24Percent = BigDecimalFormatter.formatPercent( - percent = res.priceChange, - useAbsoluteValue = true, - ), + h24Percent = res.priceChange.format { percent() }, priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index f4d62e02de..41fc02dedd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.format.bigdecimal.compact import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket @@ -149,9 +150,6 @@ internal class MarketsTokenItemConverter( TrendInterval.M1 -> tokenQuotesShort.monthChangePercent } - return BigDecimalFormatter.formatPercent( - percent = percent, - useAbsoluteValue = true, - ) + return percent.format { percent() } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt index 8a8576775e..946bcbb17c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt @@ -4,6 +4,8 @@ import com.tangem.common.extensions.isZero import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat @@ -17,7 +19,6 @@ import com.tangem.features.send.impl.presentation.state.recipient.utils.RECENT_K import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.toFormattedCurrencyString import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -76,10 +77,7 @@ internal class SendRecipientHistoryListConverter( } private fun TxHistoryItem.getAmount(cryptoCurrency: CryptoCurrency): String { - return amount.toFormattedCurrencyString( - currency = cryptoCurrency.symbol, - decimals = cryptoCurrency.decimals, - ) + return amount.format { crypto(cryptoCurrency) } } private fun TxHistoryItem.extractTimestamp(): TextReference { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 864a75afa0..4c59863b0c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -32,6 +32,9 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.presentation.state.SendStates @@ -228,12 +231,14 @@ private fun SendStates.FeeState.getFiatValue() = if (isFeeConvertibleToFiat) { ) } else { val amount = fee?.amount - BigDecimalFormatter.formatCryptoFeeAmount( - cryptoAmount = amount?.value, - cryptoCurrency = amount?.currencySymbol.orEmpty(), - decimals = amount?.decimals ?: 0, - canBeLower = isFeeApproximate, - ) + amount?.value.format { + crypto( + decimals = amount?.decimals ?: 0, + symbol = amount?.currencySymbol.orEmpty(), + ).fee( + canBeLower = isFeeApproximate, + ) + } } private fun getButtonData( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index 712f915547..6774c2572d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -16,6 +16,9 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseToBigDecimal @@ -53,12 +56,12 @@ internal fun SendSpeedSelectorItem( onSelect = onSelect, modifier = modifier, preDot = stringReference( - BigDecimalFormatter.formatCryptoFeeAmount( - cryptoAmount = amount?.value, - cryptoCurrency = amount?.currencySymbol.orEmpty(), - decimals = amount?.decimals ?: 0, - canBeLower = state.isFeeApproximate, - ), + amount?.value.format { + crypto( + symbol = amount?.currencySymbol.orEmpty(), + decimals = amount?.decimals ?: 0, + ).fee(canBeLower = state.isFeeApproximate) + }, ), postDot = if (state.isFeeConvertibleToFiat) { getFiatReference(amount?.value, state.rate, state.appCurrency) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index b396811b81..4d5830190e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -18,6 +18,9 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter @@ -62,12 +65,12 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o titleRes = title, iconRes = icon, preDot = stringReference( - BigDecimalFormatter.formatCryptoFeeAmount( - cryptoAmount = feeAmount?.value, - cryptoCurrency = feeAmount?.currencySymbol.orEmpty(), - decimals = feeAmount?.decimals ?: 0, - canBeLower = feeState.isFeeApproximate, - ), + feeAmount?.value.format { + crypto( + symbol = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + ).fee(canBeLower = feeState.isFeeApproximate) + }, ), postDot = if (feeState.isFeeConvertibleToFiat) { getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index e08e0a26d1..3268482b57 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -1,6 +1,8 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -47,10 +49,7 @@ internal class BalanceItemConverter( cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), cryptoDecimal = cryptoAmount, cryptoAmount = stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoAmount, - cryptoCurrency = cryptoCurrency, - ), + cryptoAmount.format { crypto(cryptoCurrency) }, ), fiatAmount = stringReference( BigDecimalFormatter.formatFiatAmount( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index aca5b46db7..b03ba279ed 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -1,6 +1,8 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -65,10 +67,9 @@ internal class RewardsValidatorStateConverter( val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency val cryptoAmount = stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoValue, - cryptoCurrency = cryptoCurrency, - ), + cryptoValue.format { + crypto(cryptoCurrency) + }, ) val fiatAmount = stringReference( BigDecimalFormatter.formatFiatAmount( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 53674f1c51..00bc11c717 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -1,6 +1,8 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.common.extensions.isZero +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.* @@ -41,10 +43,7 @@ internal class YieldBalancesConverter( } val (type, isActionable) = getRewardBlockType() InnerYieldBalanceState.Data( - rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoRewardsValue, - cryptoCurrency = cryptoCurrency, - ), + rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, rewardsFiat = BigDecimalFormatter.formatFiatAmount( fiatAmount = fiatRewardsValue, fiatCurrencyCode = appCurrency.code, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index b962abda5b..c3ba140e75 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -6,8 +6,10 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.Yield @@ -126,11 +128,7 @@ internal class SetInitialDataStateTransformer( id = R.string.staking_details_available, startText = TextReference.Res(R.string.staking_details_available), endText = TextReference.Str( - value = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoCurrencyStatus.value.amount, - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ), + value = cryptoCurrencyStatus.value.amount.format { crypto(cryptoCurrencyStatus.currency) }, ), isEndTextHideable = true, ) @@ -156,11 +154,7 @@ internal class SetInitialDataStateTransformer( val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum ?: return null if (!isPolkadot(cryptoCurrencyStatus.currency.network.id.value)) return null - val formattedAmount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = minimumCryptoAmount, - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ) + val formattedAmount = minimumCryptoAmount.format { crypto(cryptoCurrencyStatus.currency) } return RoundedListWithDividersItemData( id = R.string.staking_details_minimum_requirement, @@ -218,14 +212,8 @@ internal class SetInitialDataStateTransformer( val minApr = aprValues.min() val maxApr = aprValues.max() - val formattedMinApr = BigDecimalFormatter.formatPercent( - percent = minApr, - useAbsoluteValue = true, - ).remove("%") - val formattedMaxApr = BigDecimalFormatter.formatPercent( - percent = maxApr, - useAbsoluteValue = true, - ) + val formattedMinApr = minApr.format { percent() }.remove("%") + val formattedMaxApr = maxApr.format { percent() } if (maxApr - minApr < EQUALITY_THRESHOLD) { return stringReference("$formattedMinApr%") diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index 284f9b239b..88ac2c09f5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -7,7 +7,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield @@ -55,11 +56,9 @@ internal class AmountRequirementStateTransformer( isRequirementError -> resourceReference( R.string.staking_amount_requirement_error, wrappedList( - BigDecimalFormatter.formatCryptoAmount( - amountRequirements.minimum, - cryptoCurrencyStatus.currency.symbol, - cryptoCurrencyStatus.currency.decimals, - ), + amountRequirements.minimum.format { + crypto(cryptoCurrencyStatus.currency) + }, ), ) isIntegerOnlyError -> resourceReference( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt index ca32d6c1be..ea5f081cfd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -5,6 +5,8 @@ import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -33,11 +35,9 @@ internal class ShowApprovalBottomSheetTransformer( val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty() val validatorAddress = validatorState.chosenValidator.address - val feeCryptoValue = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = fee.amount.value, - cryptoCurrency = fee.amount.currencySymbol, - decimals = fee.amount.decimals, - ) + val feeCryptoValue = fee.amount.value.format { + crypto(fee.amount.currencySymbol, fee.amount.decimals) + } val feeFiatValue = BigDecimalFormatter.formatFiatAmount( fiatAmount = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value), fiatCurrencyCode = appCurrencyProvider().code, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt index fecd9f9cb6..e42c4e7c6c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt @@ -12,8 +12,9 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents @@ -41,10 +42,7 @@ internal fun StakingClaimRewardsValidatorContent( annotatedReference { appendSpace() appendColored( - text = BigDecimalFormatter.formatPercent( - percent = item.validator?.apr.orZero(), - useAbsoluteValue = true, - ), + text = item.validator?.apr.orZero().format { percent() }, color = TangemTheme.colors.text.accent, ) }, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 4a5d106633..2895d3619c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -41,11 +41,12 @@ import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.components.list.roundedListWithDividersItems import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.features.staking.impl.R @@ -311,10 +312,7 @@ private fun BalanceState.getAprText() = combinedReference( annotatedReference { appendSpace() appendColored( - text = BigDecimalFormatter.formatPercent( - percent = validator?.apr.orZero(), - useAbsoluteValue = true, - ), + text = validator?.apr.orZero().format { percent() }, color = TangemTheme.colors.text.accent, ) }, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index 692f5b3050..1bcd5c9c60 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -24,9 +24,10 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.inputrow.InputRowImageSelector import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData @@ -67,7 +68,7 @@ internal fun StakingValidatorListContent( annotatedReference { appendSpace() appendColored( - text = BigDecimalFormatter.formatPercent(item.apr.orZero(), true), + text = item.apr.orZero().format { percent() }, color = TangemTheme.colors.text.accent, ) }, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index e666de9908..8817faede7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -21,6 +21,9 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter @@ -56,12 +59,12 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, preDot = stringReference( - BigDecimalFormatter.formatCryptoFeeAmount( - cryptoAmount = feeAmount?.value, - cryptoCurrency = feeAmount?.currencySymbol.orEmpty(), - decimals = feeAmount?.decimals ?: 0, - canBeLower = feeState.isFeeApproximate, - ), + feeAmount?.value.format { + crypto( + symbol = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + ).fee(canBeLower = feeState.isFeeApproximate) + }, ), postDot = if (feeState.isFeeConvertibleToFiat) { getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index 2da1751f34..3d5cc0310a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -12,8 +12,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder @@ -43,7 +44,7 @@ internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClic append(resourceReference(R.string.staking_details_apr).resolveReference()) appendSpace() appendColored( - text = BigDecimalFormatter.formatPercent(state.chosenValidator.apr.orZero(), true), + text = state.chosenValidator.apr.orZero().format { percent() }, color = TangemTheme.colors.text.accent, ) }, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt index 4b451b4dc3..aca86a3d52 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt @@ -1,6 +1,5 @@ package com.tangem.feature.swap.domain.models -import com.tangem.utils.toFormattedString import java.math.BigDecimal import java.math.RoundingMode @@ -16,10 +15,6 @@ data class SwapAmount( } } -fun SwapAmount.formatToUIRepresentation(): String { - return value.toFormattedString(decimals = decimals) -} - fun createFromAmountWithoutOffset(amountWithoutOffset: String, decimals: Int): SwapAmount { return SwapAmount( value = requireNotNull( diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt index 01914d161b..73a7999a48 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt @@ -1,10 +1,9 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.utils.toFormattedCurrencyString import java.math.BigDecimal -class AmountFormatter { +interface AmountFormatter { /** * Use to convert crypto amount [SwapAmount] to UI representation @@ -13,9 +12,7 @@ class AmountFormatter { * @param currency currency symbol * @return formatted [String] */ - fun formatSwapAmountToUI(swapAmount: SwapAmount, currency: String): String { - return swapAmount.value.toFormattedCurrencyString(swapAmount.decimals, currency) - } + fun formatSwapAmountToUI(swapAmount: SwapAmount, currency: String): String /** * Use to convert ONLY crypto amount [BigDecimal] to UI representation @@ -25,7 +22,5 @@ class AmountFormatter { * @param currency * @return formatted [String] */ - fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String? = null): String { - return amount.toFormattedCurrencyString(decimals, currency) - } + fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String? = null): String } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index b77df21551..63b9c43970 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -71,6 +71,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val validateTransactionUseCase: ValidateTransactionUseCase, private val estimateFeeUseCase: EstimateFeeUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, + private val amountFormatter: AmountFormatter, @Assisted private val userWalletId: UserWalletId, ) : SwapInteractor { @@ -79,7 +80,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private val swapCurrencyConverter = SwapCurrencyConverter() - private val amountFormatter = AmountFormatter() private val hundredPercent = BigInteger("100") private val userWallet diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 390b8e7c7e..49a1dbc25b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -2,6 +2,8 @@ package com.tangem.feature.swap.converters import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency @@ -103,11 +105,9 @@ class TokensDataConverter( } private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String { - return BigDecimalFormatter.formatCryptoAmount( - cryptoCurrencyStatus.value.amount, - cryptoCurrencyStatus.currency.symbol, - cryptoCurrencyStatus.currency.decimals, - ) + return cryptoCurrencyStatus.value.amount.format { + crypto(cryptoCurrencyStatus.currency) + } } private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapSingletonModule.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapSingletonModule.kt new file mode 100644 index 0000000000..d265dc06e2 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapSingletonModule.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.swap.di + +import com.tangem.feature.swap.di.impl.DefaultAmountFormatter +import com.tangem.feature.swap.domain.models.ui.AmountFormatter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class SwapSingletonModule { + + @Provides + fun provideAmountFormatter(): AmountFormatter { + return DefaultAmountFormatter() + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/impl/DefaultAmountFormatter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/impl/DefaultAmountFormatter.kt new file mode 100644 index 0000000000..91d8dd1821 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/impl/DefaultAmountFormatter.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.swap.di.impl + +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.ui.AmountFormatter +import java.math.BigDecimal + +class DefaultAmountFormatter : AmountFormatter { + + override fun formatSwapAmountToUI(swapAmount: SwapAmount, currency: String): String { + return swapAmount.value.format { crypto(symbol = currency, decimals = swapAmount.decimals) } + } + + override fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String?): String { + return amount.format { crypto(symbol = currency.orEmpty(), decimals = decimals) } + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 96ae124966..6fbbf57b82 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -10,6 +10,10 @@ import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.anyDecimals +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency @@ -19,12 +23,12 @@ import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* -import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.models.states.events.SwapEvent import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.feature.swap.utils.getExpressErrorMessage import com.tangem.feature.swap.utils.getExpressErrorTitle import com.tangem.feature.swap.viewmodels.SwapProcessDataState @@ -469,10 +473,8 @@ internal class StateBuilder( domainWarning: Warning.ExistentialDepositWarning, ): SwapWarning { val fromCurrency = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency - val deposit = BigDecimalFormatter.formatCryptoAmountUncapped( - cryptoAmount = domainWarning.existentialDeposit, - cryptoCurrency = fromCurrency, - ) + val deposit = domainWarning.existentialDeposit.format { crypto(fromCurrency).uncapped() } + return SwapWarning.GeneralError( NotificationConfig( title = resourceReference(R.string.send_notification_existential_deposit_title), @@ -1546,8 +1548,12 @@ internal class StateBuilder( toTokenInfo.cryptoCurrencyStatus.currency.decimals, ) val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol - val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol - val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" + val rateString = buildString { + append(BigDecimal.ONE.format { crypto(symbol = fromCurrencySymbol, decimals = 0).anyDecimals() }) + append(" ≈ ") + append(rate.format { crypto(toTokenInfo.cryptoCurrencyStatus.currency) }) + } + // val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" val badge = if (isRecommended) { ProviderState.AdditionalBadge.Recommended } else if (isNeedBestRateBadge && isBestRate) { @@ -1628,7 +1634,7 @@ internal class StateBuilder( private fun CryptoCurrencyStatus.getFormattedAmount(isNeedSymbol: Boolean): String { val amount = value.amount ?: return DASH_SIGN val symbol = if (isNeedSymbol) currency.symbol else "" - return BigDecimalFormatter.formatCryptoAmount(amount, symbol, currency.decimals) + return amount.format { crypto(symbol, currency.decimals) } } @Suppress("UnusedPrivateMember") @@ -1646,7 +1652,7 @@ internal class StateBuilder( } private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { - return BigDecimalFormatter.formatCryptoAmount(value, token.symbol, token.decimals) + return value.format { crypto(token) } } private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt index e288546e37..44c918c906 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt @@ -3,7 +3,10 @@ package com.tangem.feature.swap.utils import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.simple import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.presentation.R internal fun getExpressErrorMessage(expressDataError: ExpressDataError): TextReference { @@ -38,4 +41,8 @@ internal fun getExpressErrorTitle(expressDataError: ExpressDataError): TextRefer is ExpressDataError.UnknownError -> resourceReference(R.string.common_error) else -> resourceReference(R.string.warning_express_refresh_required_title) } +} + +internal fun SwapAmount.formatToUIRepresentation(): String { + return value.format { simple(decimals = decimals) } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 24e23b5336..0003e45237 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -40,7 +40,6 @@ import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* -import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.SwapWarning @@ -49,6 +48,7 @@ import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder +import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index 0625d38565..4a76d6f1c9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -1,5 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -72,6 +74,6 @@ internal class TokenDetailsBalanceSelectStateConverter( val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount) - return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals) + return totalAmount.format { crypto(status.currency) } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 4ddc9c283e..ae45ac9884 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -10,6 +10,9 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.StakingAvailability @@ -215,10 +218,7 @@ internal class TokenDetailsLoadedBalanceConverter( stakingEntryInfo: StakingEntryInfo, iconState: IconState, ): StakingBlockUM.StakeAvailable { - val apr = BigDecimalFormatter.formatPercent( - percent = stakingEntryInfo.apr, - useAbsoluteValue = true, - ) + val apr = stakingEntryInfo.apr.format { percent() } return StakingBlockUM.StakeAvailable( titleText = resourceReference( id = R.string.token_details_staking_block_title, @@ -243,7 +243,7 @@ internal class TokenDetailsLoadedBalanceConverter( cryptoAmount = stakingCryptoAmount, fiatAmount = stakingFiatAmount, cryptoValue = stringReference( - BigDecimalFormatter.formatCryptoAmount(stakingCryptoAmount, symbol, decimals), + stakingCryptoAmount.format { crypto(symbol = symbol, decimals = decimals) }, ), fiatValue = stringReference( BigDecimalFormatter.formatFiatAmount( @@ -275,10 +275,7 @@ internal class TokenDetailsLoadedBalanceConverter( private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN - return BigDecimalFormatter.formatPercent( - percent = priceChange, - useAbsoluteValue = true, - ) + return priceChange.format { percent() } } private fun formatPrice(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { @@ -315,7 +312,7 @@ internal class TokenDetailsLoadedBalanceConverter( val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount) - return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals) + return totalAmount.format { crypto(status.currency) } } private fun getRewardText(status: CryptoCurrencyStatus, stakingRewardAmount: BigDecimal?): TextReference { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 0722f5a792..86aae8128f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -2,8 +2,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.shorted import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.model.warnings.HederaWarnings @@ -78,11 +80,9 @@ internal class TokenDetailsNotificationConverter( CryptoCurrencyWarning.SomeNetworksUnreachable -> NetworksUnreachable is CryptoCurrencyWarning.SomeNetworksNoAccount -> NetworksNoAccount( network = warning.amountCurrency.name, - amount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = warning.amountToCreateAccount, - cryptoCurrency = "", - decimals = warning.amountCurrency.decimals, - ), + amount = warning.amountToCreateAccount.format { + crypto(symbol = "", decimals = warning.amountCurrency.decimals) + }, symbol = warning.amountCurrency.symbol, ) is CryptoCurrencyWarning.TopUpWithoutReserve -> TopUpWithoutReserve @@ -105,11 +105,7 @@ internal class TokenDetailsNotificationConverter( ) is HederaWarnings.AssociateWarningWithFee -> HederaAssociateWarning( currency = warning.currency, - fee = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = warning.fee, - cryptoCurrency = "", - decimals = warning.feeCurrencyDecimals, - ), + fee = warning.fee.format { crypto(symbol = "", decimals = warning.feeCurrencyDecimals) }, feeCurrencySymbol = warning.feeCurrencySymbol, onAssociateClick = clickIntents::onAssociateClick, ) @@ -127,11 +123,7 @@ internal class TokenDetailsNotificationConverter( } private fun formatMana(amount: BigDecimal): String { - return BigDecimalFormatter.formatCryptoAmountShorted( - cryptoAmount = amount, - cryptoCurrency = "", - decimals = Blockchain.Koinos.decimals(), - ) + return amount.format { crypto("", Blockchain.Koinos.decimals()).shorted() } } // workaround for networks that users have misunderstanding diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 6a8a7087ed..ca04809b0a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -3,6 +3,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat @@ -80,17 +82,11 @@ internal class TokenDetailsSwapTransactionsStateConverter( activeStatus = transaction.status?.status, notification = notifications, toCryptoCurrency = toCryptoCurrency, - toCryptoAmount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = toAmount, - cryptoCurrency = toCryptoCurrency, - ), + toCryptoAmount = toAmount.format { crypto(toCryptoCurrency) }, toFiatAmount = getFiatAmount(toFiatAmount), toCurrencyIcon = iconStateConverter.convert(toCryptoCurrency), fromCryptoCurrency = fromCryptoCurrency, - fromCryptoAmount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = fromAmount, - cryptoCurrency = fromCryptoCurrency, - ), + fromCryptoAmount = fromAmount.format { crypto(fromCryptoCurrency) }, fromFiatAmount = getFiatAmount(fromFiatAmount), fromCurrencyIcon = iconStateConverter.convert(fromCryptoCurrency), showProviderLink = showProviderLink, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt index 0d0b59e987..4756f994ca 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt @@ -7,6 +7,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryItem.* @@ -16,7 +18,6 @@ import com.tangem.utils.StringsSigns.MINUS import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat -import com.tangem.utils.toFormattedCurrencyString internal class TokenDetailsTxHistoryTransactionStateConverter( private val symbol: String, @@ -129,6 +130,6 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( this.amount.isZero() -> "" else -> if (isOutgoing) MINUS else PLUS } - return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) + return prefix + amount.format { crypto(symbol = symbol, decimals = decimals) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 3de82a88d3..f258bf29b6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -4,7 +4,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.plus import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet @@ -97,13 +98,7 @@ internal object WalletAdditionalInfoFactory { WalletAdditionalInfo(hideable = false, content = TextReference.Res(R.string.common_locked)) } else { val blockchain = scanResponse.cardTypesResolver.getBlockchain() - val amount = currencyAmount?.let { - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = it, - cryptoCurrency = blockchain.currency, - decimals = blockchain.decimals(), - ) - } + val amount = currencyAmount?.format { crypto(blockchain.currency, blockchain.decimals()) } WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty())) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt index d4f7d3db34..af365a24d7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt @@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.visa.model.VisaCurrency @@ -41,11 +43,9 @@ internal class SetBalancesAndLimitsTransformer( } private fun getContentBlockState(visaCurrency: VisaCurrency) = BalancesAndLimitsBlockState.Content( - availableBalance = BigDecimalFormatter.formatCryptoAmount( - visaCurrency.limits.remainingOtp, - visaCurrency.symbol, - visaCurrency.decimals, - ), + availableBalance = visaCurrency.limits.remainingOtp.format { + crypto(visaCurrency.symbol, visaCurrency.decimals) + }, limitDays = Days.daysBetween(DateTime.now(), visaCurrency.limits.expirationDate).days.inc(), isEnabled = true, onClick = clickIntents::onBalancesAndLimitsClick, @@ -72,11 +72,9 @@ internal class SetBalancesAndLimitsTransformer( imageResId = imageResId, onRenameClick = onRenameClick, onDeleteClick = onDeleteClick, - balance = BigDecimalFormatter.formatCryptoAmount( - visaCurrency.balances.available, - visaCurrency.symbol, - visaCurrency.decimals, - ), + balance = visaCurrency.balances.available.format { + crypto(visaCurrency.symbol, visaCurrency.decimals) + }, cardCount = userWallet.getCardsCount(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt index 146e196a65..dc965b29e2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBottomSheetConfig @@ -15,11 +16,7 @@ internal class BalancesAndLimitsBottomSheetConverter( ) : Converter { override fun convert(value: VisaCurrency): BalancesAndLimitsBottomSheetConfig { - fun formatAmount(amount: BigDecimal): String = BigDecimalFormatter.formatCryptoAmount( - amount, - cryptoCurrency = value.symbol, - decimals = value.decimals, - ) + fun formatAmount(amount: BigDecimal): String = amount.format { crypto(value.symbol, value.decimals) } val otpLimit = value.limits.remainingOtp.let(::formatAmount) val noOtpLimit = value.limits.remainingNoOtp.let(::formatAmount) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt index 8d710656dc..3611aa8119 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -4,6 +4,8 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -57,7 +59,7 @@ internal class SingleWalletMarketPriceConverter( private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN - return BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true) + return priceChange.format { percent() } } private fun getPriceChangeType(status: CryptoCurrencyStatus.Value): PriceChangeType { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index 2d162a17c0..461f74999f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -6,6 +6,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryItem.* @@ -15,7 +17,6 @@ import com.tangem.utils.StringsSigns.MINUS import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat -import com.tangem.utils.toFormattedCurrencyString internal class TxHistoryItemStateConverter( private val symbol: String, @@ -129,6 +130,6 @@ internal class TxHistoryItemStateConverter( this.amount.isZero() -> "" else -> if (isOutgoing) MINUS else PLUS } - return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) + return prefix + amount.format { crypto(symbol = symbol, decimals = decimals) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt index 6e8158da13..e655973c16 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter import com.tangem.core.ui.extensions.capitalize +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency @@ -66,11 +68,7 @@ internal class VisaTxDetailsBottomSheetConverter( } private fun formatNetworkAmount(amount: BigDecimal): String { - return BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amount, - cryptoCurrency = visaCurrency.symbol, - decimals = visaCurrency.decimals, - ) + return amount.format { crypto(visaCurrency.symbol, visaCurrency.decimals) } } private fun formatFiatAmount(amount: BigDecimal, fiatCurrency: Currency): String { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt index 60ac94068a..fe50fdf537 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt @@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency @@ -25,11 +27,7 @@ internal class VisaTxHistoryItemStateConverter( return TransactionState.Content( txHash = value.id, - amount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = value.amount, - cryptoCurrency = visaCurrency.symbol, - decimals = visaCurrency.decimals, - ), + amount = value.amount.format { crypto(visaCurrency.symbol, visaCurrency.decimals) }, // Show tx fiat amount instead of tx time time = BigDecimalFormatter.formatFiatAmount( fiatAmount = value.fiatAmount, From d487f796beced2312460d6a8a8821fdd970509fa Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Oct 2024 15:44:21 +0300 Subject: [PATCH 10/18] Updated on 2026-08-14 --- app/build.gradle.kts | 3 +++ app/src/main/res/resources.properties | 1 + 2 files changed, 4 insertions(+) create mode 100644 app/src/main/res/resources.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d9d1aaef00..b42a58fe8e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,6 +22,9 @@ android { resources.excludes.add("META-INF/LICENSE.md") resources.excludes.add("META-INF/NOTICE.md") } + androidResources { + generateLocaleConfig = true + } } configurations.all { diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 0000000000..d5a3ddc92a --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US \ No newline at end of file From 7bd584c2619449384677de080ce9fba60bd14ea2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Oct 2024 10:57:07 +0400 Subject: [PATCH 11/18] Updated on 2026-08-14 --- .../network/DefaultNetworksStatusesStore.kt | 20 ++ .../local/network/NetworksStatusesStore.kt | 2 + .../data/staking/DefaultStakingRepository.kt | 55 ++--- .../repository/DefaultCurrenciesRepository.kt | 23 +-- .../repository/DefaultNetworksRepository.kt | 79 +++----- .../com/tangem/domain/core/lce/LceFlow.kt | 9 + .../com/tangem/domain/core/lce/LceRaise.kt | 8 + .../staking/repositories/StakingRepository.kt | 5 +- .../domain/tokens/model/NetworkStatus.kt | 5 + .../domain/tokens/FetchTokenListUseCase.kt | 86 +++++--- .../tokens/GetWalletTotalBalanceUseCase.kt | 5 +- .../CurrenciesStatusesLceOperations.kt | 191 ++++++++---------- .../operations/CurrencyStatusOperations.kt | 4 +- .../tokens/repository/CurrenciesRepository.kt | 2 +- .../repository/MockStakingRepository.kt | 13 +- .../domain/GetMultiWalletWarningsFactory.kt | 6 +- .../domain/MultiWalletTokenListStore.kt | 62 ++++++ .../implementors/MultiWalletContentLoader.kt | 6 +- .../MultiWalletContentLoaderFactory.kt | 6 +- .../subscribers/BasicTokenListSubscriber.kt | 4 +- .../MultiWalletTokenListSubscriber.kt | 11 +- .../SingleWalletWithTokenListSubscriber.kt | 8 +- .../wallet/viewmodels/WalletViewModel.kt | 8 +- .../viewmodels/WalletsUpdateActionResolver.kt | 4 +- .../intents/WalletCardClickIntents.kt | 4 + .../viewmodels/intents/WalletClickIntents.kt | 3 +- .../intents/WalletWarningsClickIntents.kt | 16 +- 27 files changed, 379 insertions(+), 266 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt index 88b241c3ff..e168276a5b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt @@ -5,6 +5,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace +import com.tangem.utils.extensions.replaceBy import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -27,4 +28,23 @@ internal class DefaultNetworksStatusesStore( store(key, newValues) } } + + override suspend fun storeAll(key: UserWalletId, values: Collection) { + mutex.withLock { + val currentValues = getSyncOrNull(key) ?: emptySet() + val updatedValues = currentValues.toMutableSet() + + values.forEach { newValue -> + val isReplaced = updatedValues.replaceBy(newValue) { + it.network == newValue.network + } + + if (!isReplaced) { + updatedValues.add(newValue) + } + } + + store(key, updatedValues) + } + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt index a30a1b49ab..3ab684275f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt @@ -11,4 +11,6 @@ interface NetworksStatusesStore { suspend fun getSyncOrNull(key: UserWalletId): Set? suspend fun store(key: UserWalletId, value: NetworkStatus) + + suspend fun storeAll(key: UserWalletId, values: Collection) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 71bd21a01e..8633e9d88c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,7 +2,6 @@ package com.tangem.data.staking import android.util.Base64 import arrow.core.getOrElse -import arrow.core.raise.catch import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain @@ -30,8 +29,6 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo @@ -57,6 +54,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import kotlinx.coroutines.withContext import timber.log.Timber @@ -385,15 +383,22 @@ internal class DefaultStakingRepository( refresh: Boolean, ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext + try { - isYieldBalanceFetching.update { - it + (userWalletId to true) - } cacheRegistry.invokeOnExpire( key = getYieldBalancesKey(userWalletId), skipCache = refresh, block = { - val yields = getEnabledYields() + isYieldBalanceFetching.update { + it + (userWalletId to true) + } + + val yields = getEnabledYields().ifEmpty { + Timber.i("No enabled yields for $userWalletId") + stakingBalanceStore.store(userWalletId, emptySet()) + + return@invokeOnExpire + } val availableCurrencies = cryptoCurrencies .mapNotNull { currency -> val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) @@ -410,10 +415,15 @@ internal class DefaultStakingRepository( } .map { getBalanceRequestData(it.first.value, it.second) } .ifEmpty { - cacheRegistry.invalidate(getYieldBalancesKey(userWalletId)) - error("No addresses found") + Timber.i("No yield balances available for $userWalletId") + stakingBalanceStore.store(userWalletId, emptySet()) + + return@invokeOnExpire } - val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow() + + val result = stakeKitApi + .getMultipleYieldBalances(availableCurrencies) + .getOrThrow() stakingBalanceStore.store(userWalletId, result) }, @@ -446,27 +456,22 @@ internal class DefaultStakingRepository( } }.cancellable() - override fun getMultiYieldBalanceLce( + override fun getMultiYieldBalance( userWalletId: UserWalletId, cryptoCurrencies: List, - ): LceFlow = lceFlow { + ): Flow = channelFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalanceList.Empty) } else { - launch(dispatchers.io) { - combine( - stakingBalanceStore.get(userWalletId), - isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } }, - ) { result, isFetching -> - val balances = yieldBalanceListConverter.convert(result) - send(balances, isStillLoading = isFetching) - }.collect() - } + stakingBalanceStore.get(userWalletId) + .onEach { + val balances = yieldBalanceListConverter.convert(it) + send(balances) + } + .launchIn(scope = this + dispatchers.io) + withContext(dispatchers.io) { - catch( - block = { fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) }, - catch = { raise(it) }, - ) + fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) } } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index a3b8911411..4664e120ce 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -42,6 +42,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import kotlinx.coroutines.withContext import timber.log.Timber import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency @@ -206,18 +207,14 @@ internal class DefaultCurrenciesRepository( } } - override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow> { - return lceFlow { - val userWallet = catch({ getUserWallet(userWalletId) }) { - raise(it) - } + override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { + return channelFlow { + val userWallet = getUserWallet(userWalletId) if (userWallet.isMultiCurrency) { - getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId).collect(::send) + getMultiCurrencyWalletCurrenciesUpdates(userWalletId).collect(::send) } else { - val currency = catch({ getSingleCurrencyWalletPrimaryCurrency(userWalletId) }) { - raise(it) - } + val currency = getSingleCurrencyWalletPrimaryCurrency(userWalletId) send(listOf(currency)) } } @@ -260,16 +257,14 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) - launch(dispatchers.io) { - getMultiCurrencyWalletCurrencies(userWallet) - .collectLatest(::send) - } + getMultiCurrencyWalletCurrencies(userWallet) + .onEach { send(it) } + .launchIn(scope = this + dispatchers.io) withContext(dispatchers.io) { fetchTokensIfCacheExpired(userWallet, refresh = false) } } - .cancellable() } override fun getMultiCurrencyWalletCurrenciesUpdatesLce( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 3762b4ddee..d61f12d1a3 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -54,29 +54,25 @@ internal class DefaultNetworksRepository( userWalletId: UserWalletId, networks: Set, ): Flow> = channelFlow { - launch(dispatchers.io) { - networksStatusesStore.get(userWalletId) - .collectLatest(::send) - } + networksStatusesStore.get(userWalletId) + .onEach(::send) + .launchIn(scope = this + dispatchers.io) withContext(dispatchers.io) { - fetchNetworksStatusesIfCacheExpired(userWalletId, networks, false) + fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) } } - .cancellable() override fun getNetworkStatusesUpdatesLce( userWalletId: UserWalletId, networks: Set, ): LceFlow> = lceFlow { - launch(dispatchers.io) { - combine( - networksStatusesStore.get(userWalletId), - isNetworkStatusesFetching.map { it.getOrElse(userWalletId) { false } }, - ) { statuses, isFetching -> - send(statuses, isStillLoading = isFetching) - }.collect() - } + combine( + networksStatusesStore.get(userWalletId), + isNetworkStatusesFetching.map { it.getOrElse(userWalletId) { false } }, + ) { statuses, isFetching -> + send(statuses, isStillLoading = isFetching || networks.size != statuses.size) + }.launchIn(scope = this + dispatchers.io) withContext(dispatchers.io) { catch({ fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) }) { @@ -185,25 +181,32 @@ internal class DefaultNetworksRepository( userWalletId: UserWalletId, networks: Set, refresh: Boolean, - ) { - val currencies = getCurrencies(userWalletId, networks) - val networksDeferred = networks.mapNotNull { network -> - fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh) + ) = coroutineScope { + if (refresh) { + val statusesToRefresh = networks.map { NetworkStatus(it, NetworkStatus.Loading) } + networksStatusesStore.storeAll(userWalletId, statusesToRefresh) } - if (networksDeferred.isNotEmpty()) { - try { - isNetworkStatusesFetching.update { - it + (userWalletId to true) - } + val currencies = getCurrencies(userWalletId, networks) + val networksDeferred = networks.mapNotNull { network -> + coroutineScope { + val key = getNetworksStatusesCacheKey(userWalletId, network) - networksDeferred.awaitAll() - } finally { - isNetworkStatusesFetching.update { - it - userWalletId + if (refresh || cacheRegistry.isExpired(key)) { + async { + cacheRegistry.invokeOnExpire( + key = key, + skipCache = refresh, + block = { fetchNetworkStatus(userWalletId, network, currencies) }, + ) + } + } else { + null } } } + + networksDeferred.awaitAll() } private suspend fun fetchNetworksPendingTransactions( @@ -222,31 +225,13 @@ internal class DefaultNetworksRepository( } } - private suspend fun fetchNetworkStatusIfCacheExpired( - userWalletId: UserWalletId, - network: Network, - currencies: Sequence, - refresh: Boolean, - ): Deferred? = coroutineScope { - val key = getNetworksStatusesCacheKey(userWalletId, network) - if (refresh || cacheRegistry.isExpired(key)) { - async { - cacheRegistry.invokeOnExpire( - key = key, - skipCache = refresh, - block = { fetchNetworkStatus(userWalletId, network, currencies) }, - ) - } - } else { - null - } - } - private suspend fun fetchNetworkStatus( userWalletId: UserWalletId, network: Network, currencies: Sequence, ) { + networksStatusesStore.store(userWalletId, NetworkStatus(network, NetworkStatus.Loading)) + val result = walletManagersFacade.update( userWalletId = userWalletId, network = network, diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt index 6204c49430..037775753e 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt @@ -1,5 +1,6 @@ package com.tangem.domain.core.lce +import arrow.atomic.AtomicBoolean import arrow.core.raise.Raise import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError @@ -34,6 +35,8 @@ class LceFlowScope @PublishedApi internal constructor( private val ifLoading: suspend LceFlowScope.(C?) -> Unit, ) : Raise, CoroutineScope by producerScope { + val isLoading: AtomicBoolean = AtomicBoolean(value = true) + /** * Sends a error of type [E] within the [ProducerScope] and then closes it for send. * All subsequent sends will be ignored. @@ -46,6 +49,8 @@ class LceFlowScope @PublishedApi internal constructor( * @param r Error to raise. */ override fun raise(r: E): Nothing { + isLoading.set(false) + producerScope.trySendBlocking(r.lceError()) producerScope.close() @@ -66,6 +71,8 @@ class LceFlowScope @PublishedApi internal constructor( * @param isStillLoading A flag indicating whether the content is still loading. */ suspend fun send(content: C, isStillLoading: Boolean = false) { + isLoading.set(isStillLoading) + val value = if (isStillLoading) { ifLoading(content) return @@ -89,6 +96,8 @@ class LceFlowScope @PublishedApi internal constructor( suspend fun send(value: Lce) { if (producerScope.isClosedForSend) return + isLoading.set(value.isLoading()) + producerScope.send(value) } } diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt index 1484205426..dce3342eb3 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt @@ -1,6 +1,8 @@ package com.tangem.domain.core.lce import arrow.atomic.Atomic +import arrow.core.Either +import arrow.core.identity import arrow.core.raise.Raise import arrow.core.raise.RaiseDSL import arrow.core.raise.recover @@ -97,6 +99,12 @@ class LceRaise @PublishedApi internal constructor( is Lce.Content -> content is Lce.Error -> raise(r = this) } + + @RaiseDSL + fun Either.bindEither(): C = fold( + ifLeft = { raise(it) }, + ifRight = ::identity, + ) } /** diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 6f4ba8fd32..6e17391a27 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -3,7 +3,6 @@ package com.tangem.domain.staking.repositories import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee -import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo @@ -55,10 +54,10 @@ interface StakingRepository { cryptoCurrencies: List, ): Flow - fun getMultiYieldBalanceLce( + fun getMultiYieldBalance( userWalletId: UserWalletId, cryptoCurrencies: List, - ): LceFlow + ): Flow suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt index 08a746b12e..d33b011230 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -21,6 +21,11 @@ data class NetworkStatus( */ sealed class Value + /** + * Represents the state where the network is refreshing. + */ + data object Refreshing : Value() + /** * Represents the state where the network is unreachable. * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index 394cd7c1dc..f1ef16bca8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -40,38 +40,39 @@ class FetchTokenListUseCase( * network statuses, and quotes for associated tokens. * * @param userWalletId The ID of the user's wallet. - * @param refresh Indicates whether to force a refresh of the token list data. + * @param mode The refresh mode to control the fetching process. * @return An [Either] representing success (Right) or an error (Left) in fetching the token list. */ - suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { - return either { - val currencies = fetchCurrencies(userWalletId, refresh) + suspend operator fun invoke( + userWalletId: UserWalletId, + mode: RefreshMode = RefreshMode.NONE, + ): Either = either { + val currencies = fetchCurrencies(userWalletId, refresh = mode.refreshCurrencies) - coroutineScope { - val fetchStatuses = async { - fetchNetworksStatuses( - userWalletId, - currencies.mapTo(hashSetOf()) { it.network }, - refresh, - ) - } - val fetchQuotes = async { - fetchQuotes( - currencies.mapTo(hashSetOf()) { it.id }, - refresh, - ) - } - - val yieldBalances = async { - fetchYieldBalances( - userWalletId = userWalletId, - currencies = currencies, - refresh = refresh, - ) - } - - awaitAll(fetchStatuses, fetchQuotes, yieldBalances) + coroutineScope { + val fetchStatuses = async { + fetchNetworksStatuses( + userWalletId, + currencies.mapTo(hashSetOf()) { it.network }, + refresh = mode.refreshNetworksStatuses, + ) } + val fetchQuotes = async { + fetchQuotes( + currencies.mapTo(hashSetOf()) { it.id }, + refresh = mode.refreshQuotes, + ) + } + + val yieldBalances = async { + fetchYieldBalances( + userWalletId = userWalletId, + currencies = currencies, + refresh = mode.refreshYieldBalances, + ) + } + + awaitAll(fetchStatuses, fetchQuotes, yieldBalances) } } @@ -120,4 +121,33 @@ class FetchTokenListUseCase( catch = { /* Ignore error */ }, ) } + + /** + * Represents the refresh modes available for fetching token list information. + */ + enum class RefreshMode( + internal val refreshCurrencies: Boolean, + internal val refreshNetworksStatuses: Boolean, + internal val refreshQuotes: Boolean, + internal val refreshYieldBalances: Boolean, + ) { + NONE( + refreshCurrencies = false, + refreshNetworksStatuses = false, + refreshQuotes = false, + refreshYieldBalances = false, + ), + FULL( + refreshCurrencies = true, + refreshNetworksStatuses = true, + refreshQuotes = true, + refreshYieldBalances = true, + ), + SKIP_CURRENCIES( + refreshCurrencies = false, + refreshNetworksStatuses = true, + refreshQuotes = true, + refreshYieldBalances = true, + ), + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 012dd2847a..ebf6a72cd7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -85,9 +85,6 @@ class GetWalletTotalBalanceUseCase( stakingRepository = stakingRepository, ) - return operations.getCurrenciesStatuses( - userWalletId = userWalletId, - isSingleCurrencyWalletsAllowed = true, - ) + return operations.getCurrenciesStatuses(userWalletId) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 419842f017..730dbc541b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -1,12 +1,13 @@ package com.tangem.domain.tokens.operations import arrow.core.* +import arrow.core.raise.ensureNotNull import arrow.core.raise.recover import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce -import com.tangem.domain.core.utils.lceError -import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.core.lce.lceFlow +import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository @@ -26,147 +27,126 @@ internal class CurrenciesStatusesLceOperations( private val stakingRepository: StakingRepository, ) { - fun getCurrenciesStatuses( - userWalletId: UserWalletId, - isSingleCurrencyWalletsAllowed: Boolean = false, - ): LceFlow> { + fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> { return transformToCurrenciesStatuses( userWalletId = userWalletId, - flow = if (isSingleCurrencyWalletsAllowed) { - getWalletCurrencies(userWalletId) - } else { - getMultiCurrencyWalletCurrencies(userWalletId) - }, + currenciesFlow = getWalletCurrencies(userWalletId), ) } @OptIn(ExperimentalCoroutinesApi::class) private fun transformToCurrenciesStatuses( userWalletId: UserWalletId, - flow: LceFlow>, - ): LceFlow> { - return flow.transformLatest transform@{ maybeCurrencies -> - val nonEmptyCurrencies = maybeCurrencies.fold( - ifLoading = { maybeContent -> - emit(createLoadingCurrenciesStatuses(maybeContent)) - return@transform - }, - ifContent = { content -> - val nonEmptyCurrencies = content.toNonEmptyListOrNull() + currenciesFlow: EitherFlow>, + ): LceFlow> = lceFlow { + currenciesFlow.collectLatest { maybeCurrencies -> + val nonEmptyCurrencies = maybeCurrencies.bind().toNonEmptyListOrNull() + ensureNotNull(nonEmptyCurrencies) { TokenListError.EmptyTokens } - if (nonEmptyCurrencies == null) { - emit(TokenListError.EmptyTokens.lceError()) - return@transform - } else { - nonEmptyCurrencies - } - }, - ifError = { error -> - emit(error.lceError()) - return@transform - }, - ) + // This is only 'true' when the flow here is empty, such as during initial loading + if (isLoading.get()) { + val loadingCurrencies = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeNetworkStatuses = null, + maybeQuotes = null, + maybeYieldBalances = null, + + ) + send(loadingCurrencies) + } val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + fun createCurrenciesStatuses( + maybeQuotes: Either>?, + maybeNetworkStatuses: Either>?, + maybeYieldBalances: Either?, + ): Lce> = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeQuotes = maybeQuotes, + maybeNetworkStatuses = maybeNetworkStatuses, + maybeYieldBalances = maybeYieldBalances, + ) + combine( getQuotes(currenciesIds), getNetworksStatuses(userWalletId, networks), getYieldBalances(userWalletId, nonEmptyCurrencies), - - ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> - val statuses = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeQuotes = maybeQuotes, - maybeNetworkStatuses = maybeNetworksStatuses, - maybeYieldBalances = maybeYieldBalances, - ) - emit(statuses) - }.collect() - } - } - - private fun createLoadingCurrenciesStatuses( - maybeCurrencies: List?, - ): Lce> { - val nonEmptyCurrencies = maybeCurrencies?.toNonEmptyListOrNull() - - val statuses = if (nonEmptyCurrencies == null) { - lceLoading() - } else { - createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeNetworkStatuses = null, - maybeQuotes = null, - maybeYieldBalances = null, + ::createCurrenciesStatuses, ) + .distinctUntilChanged() + .mapLatest { maybeCurrenciesStatuses -> + send(maybeCurrenciesStatuses) + } + .launchIn(scope = this) } - - return statuses } - private fun getWalletCurrencies(userWalletId: UserWalletId): LceFlow> { + private fun getWalletCurrencies(userWalletId: UserWalletId): EitherFlow> { return currenciesRepository.getWalletCurrenciesUpdates(userWalletId) - .map { maybeCurrencies -> - maybeCurrencies.mapError { TokenListError.DataError(it) } - } - } - - private fun getMultiCurrencyWalletCurrencies( - userWalletId: UserWalletId, - ): LceFlow> { - return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId) + .map, Either>> { it.right() } + .catch { emit(TokenListError.DataError(it).left()) } .distinctUntilChanged() - .map { maybeCurrencies -> - maybeCurrencies.mapError { TokenListError.DataError(it) } - } } private fun createCurrenciesStatuses( currencies: NonEmptyList, maybeQuotes: Either>?, - maybeNetworkStatuses: Lce>?, - maybeYieldBalances: Lce?, + maybeNetworkStatuses: Either>?, + maybeYieldBalances: Either?, ): Lce> = lce { - isLoading.set(maybeNetworkStatuses == null) + isLoading.set(maybeNetworkStatuses == null || maybeYieldBalances == null) var quotesRetrievingFailed = false - val networksStatuses = maybeNetworkStatuses?.bindOrNull()?.toNonEmptySetOrNull() + val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull() + val yieldBalances = maybeYieldBalances?.bindEither() val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { - quotesRetrievingFailed = true - null - }?.ifEmpty { - quotesRetrievingFailed = true null } - val yieldBalances = maybeYieldBalances?.getOrNull() + if (quotes == null) { + quotesRetrievingFailed = true + } currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val address = extractAddress(networkStatus) - val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) - val yieldBalance = if (supportedIntegration.isNullOrBlank().not()) { - (yieldBalances as? YieldBalanceList.Data)?.getBalance( - address = address, - integrationId = supportedIntegration, - ) - } else { - null - } + val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus) - createCurrencyStatus( + val currencyStatus = createCurrencyStatus( currency = currency, quote = quote, networkStatus = networkStatus, yieldBalance = yieldBalance, ignoreQuote = quotesRetrievingFailed, ) + + if (currencyStatus.value is CryptoCurrencyStatus.Loading) { + isLoading.set(true) + } + + currencyStatus } } + private fun findYieldBalanceOrNull( + yieldBalances: YieldBalanceList?, + currency: CryptoCurrency, + networkStatus: NetworkStatus?, + ): YieldBalance? { + if (yieldBalances !is YieldBalanceList.Data) return null + + val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) + + if (supportedIntegration.isNullOrBlank()) return null + + return yieldBalances.getBalance( + address = extractAddress(networkStatus), + integrationId = supportedIntegration, + ) + } + private fun createCurrencyStatus( currency: CryptoCurrency, quote: Quote?, @@ -189,28 +169,27 @@ internal class CurrenciesStatusesLceOperations( return quotesRepository.getQuotesUpdates(tokensIds) .map, Either>> { it.right() } .catch { emit(TokenListError.DataError(it).left()) } + .distinctUntilChanged() } private fun getNetworksStatuses( userWalletId: UserWalletId, networks: NonEmptySet, - ): LceFlow> { - return networksRepository.getNetworkStatusesUpdatesLce(userWalletId, networks) - .map { maybeStatuses -> - maybeStatuses.mapError { TokenListError.DataError(it) } - } + ): EitherFlow> { + return networksRepository.getNetworkStatusesUpdates(userWalletId, networks) + .map, Either>> { it.right() } + .catch { emit(TokenListError.DataError(it).left()) } + .distinctUntilChanged() } private fun getYieldBalances( userWalletId: UserWalletId, cryptoCurrencies: List, - ): LceFlow { - return stakingRepository.getMultiYieldBalanceLce( - userWalletId = userWalletId, - cryptoCurrencies = cryptoCurrencies, - ).map { maybeBalances -> - maybeBalances.mapError { TokenListError.DataError(it) } - } + ): EitherFlow { + return stakingRepository.getMultiYieldBalance(userWalletId, cryptoCurrencies) + .map> { it.right() } + .catch { emit(TokenListError.DataError(it).left()) } + .distinctUntilChanged() } private fun getIds(currencies: List): Pair, NonEmptySet> { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 6cbaf22b37..ff31ccc64a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -16,7 +16,9 @@ internal class CurrencyStatusOperations( private fun createStatus(): CryptoCurrencyStatus.Value { return when (val status = networkStatus?.value) { - null -> CryptoCurrencyStatus.Loading + null, + is NetworkStatus.Refreshing, + -> CryptoCurrencyStatus.Loading is NetworkStatus.MissedDerivation -> createMissedDerivationStatus() is NetworkStatus.Unreachable -> createUnreachableStatus(status) is NetworkStatus.NoAccount -> createNoAccountStatus(status) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index d8c8c7cf37..8a19b705e9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -81,7 +81,7 @@ interface CurrenciesRepository { * @param userWalletId The unique identifier of the user wallet. * @return A list of [CryptoCurrency]. */ - fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow> + fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> /** * Retrieves the primary cryptocurrency for a specific single-currency user wallet. diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 32838c0406..9b702bacbb 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -4,8 +4,6 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.transaction.Fee -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo @@ -20,6 +18,7 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.flowOf import org.joda.time.DateTime import java.math.BigDecimal @@ -157,16 +156,10 @@ class MockStakingRepository : StakingRepository { ) } - override fun getMultiYieldBalanceLce( + override fun getMultiYieldBalance( userWalletId: UserWalletId, cryptoCurrencies: List, - ): LceFlow = lceFlow { - send( - YieldBalanceList.Data( - balances = listOf(YieldBalance.Error), - ), - ) - } + ): Flow = flowOf() override suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 5ea90e12d4..90b969933f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -7,7 +7,6 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.promo.PromoBanner import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.settings.ShouldShowRingPromoUseCase -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -30,7 +29,7 @@ import kotlin.collections.count @Suppress("LongParameterList") @ViewModelScoped internal class GetMultiWalletWarningsFactory @Inject constructor( - private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val shouldShowRingPromoUseCase: ShouldShowRingPromoUseCase, @@ -44,7 +43,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val promoFlow = flow { emit(promoRepository.getRingPromoBanner()) } return combine( - flow = getTokenListUseCase.launch(userWallet.walletId), + flow = tokenListStore.getOrThrow(userWallet.walletId), flow2 = isReadyToShowRateAppUseCase(), flow3 = isNeedToBackupUseCase(userWallet.walletId), flow4 = shouldShowRingPromoUseCase(userWalletId = userWallet.walletId), @@ -129,6 +128,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( clickIntents: WalletClickIntents, ) { val currencies = maybeTokenList.getMissingAddressCurrencies() + .ifEmpty { return } addIf( element = WalletNotification.Informational.MissingAddresses( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt new file mode 100644 index 0000000000..d25f60ca3e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWalletId +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +@ViewModelScoped +internal class MultiWalletTokenListStore @Inject constructor( + private val getTokenListUseCase: GetTokenListUseCase, +) { + + private val flows: ConcurrentHashMap> by lazy { + ConcurrentHashMap() + } + + fun addIfNot(userWalletId: UserWalletId, coroutineScope: CoroutineScope) { + if (flows[userWalletId] != null) { + Timber.d("Flow with token list for $userWalletId already exists") + return + } + + coroutineScope.ensureActive() + + flows[userWalletId] = getTokenListUseCase + .launch(userWalletId) + .shareIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) + + Timber.d("Flow with token list for $userWalletId created") + } + + fun getOrThrow(userWalletId: UserWalletId): LceFlow { + return requireNotNull(flows[userWalletId]) { + "Flow with token list for $userWalletId doesn't exist" + } + } + + fun remove(userWalletId: UserWalletId) { + flows.remove(userWalletId) + + Timber.d("Flow with token list for $userWalletId removed") + } + + fun clear() { + flows.clear() + + Timber.d("All flows with token list cleared") + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 7dc59658c4..ee5cdac9f1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -2,12 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber @@ -23,7 +23,7 @@ internal class MultiWalletContentLoader( private val tokenListAnalyticsSender: TokenListAnalyticsSender, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, - private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, @@ -38,7 +38,7 @@ internal class MultiWalletContentLoader( clickIntents = clickIntents, tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, - getTokenListUseCase = getTokenListUseCase, + tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, applyTokenListSortingUseCase = applyTokenListSortingUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 1b405c8af6..7a70b865b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -2,12 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents @@ -21,7 +21,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val tokenListAnalyticsSender: TokenListAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, @@ -35,7 +35,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( stateHolder = stateHolder, tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, - getTokenListUseCase = getTokenListUseCase, + tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index f074661604..9c1bbbd713 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -41,11 +41,11 @@ internal abstract class BasicTokenListSubscriber( private val sendAnalyticsJobHolder = JobHolder() private val onTokenListReceivedJobHolder = JobHolder() - protected abstract fun tokenListFlow(): LceFlow + protected abstract fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow override fun create(coroutineScope: CoroutineScope): Flow<*> { return combine( - flow = tokenListFlow() + flow = tokenListFlow(coroutineScope) .onEach { maybeTokenList -> coroutineScope.launch { sendTokenListAnalytics(maybeTokenList) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index 12d5115bcf..79cef234af 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -4,7 +4,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -12,14 +11,16 @@ import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import kotlinx.coroutines.CoroutineScope @Suppress("LongParameterList") internal class MultiWalletTokenListSubscriber( private val userWallet: UserWallet, - private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, stateHolder: WalletStateController, clickIntents: WalletClickIntents, @@ -37,8 +38,10 @@ internal class MultiWalletTokenListSubscriber( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) { - override fun tokenListFlow(): LceFlow { - return getTokenListUseCase.launch(userWallet.walletId) + override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { + tokenListStore.addIfNot(userWallet.walletId, coroutineScope) + + return tokenListStore.getOrThrow(userWallet.walletId) } override suspend fun onTokenListReceived(maybeTokenList: Lce) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index c2c561afd8..c8848d06e9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -12,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAn import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map @Suppress("LongParameterList") @@ -34,6 +35,9 @@ internal class SingleWalletWithTokenListSubscriber( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) { - override fun tokenListFlow(): LceFlow = getNodlTokenListUseCase(userWallet.walletId) - .map { it.toLce() } + override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow = + getNodlTokenListUseCase( + userWallet.walletId, + ) + .map { it.toLce() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 87496adf65..14bb262aa5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -16,6 +16,7 @@ import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader @@ -69,6 +70,7 @@ internal class WalletViewModel @Inject constructor( private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, private val marketsFeatureToggles: MarketsFeatureToggles, private val walletImageResolver: WalletImageResolver, + private val tokenListStore: MultiWalletTokenListStore, analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -110,6 +112,8 @@ internal class WalletViewModel @Inject constructor( override fun onCleared() { super.onCleared() + + tokenListStore.clear() stateHolder.clear() walletScreenContentLoader.cancelAll() } @@ -280,7 +284,7 @@ internal class WalletViewModel @Inject constructor( stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name)) } is WalletsUpdateActionResolver.Action.Unknown -> { - Timber.w("Unable to perfom action: $action") + Timber.w("Unable to perform action: $action") } } } @@ -318,6 +322,7 @@ internal class WalletViewModel @Inject constructor( private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) { walletScreenContentLoader.cancel(action.prevWalletId) + tokenListStore.remove(action.prevWalletId) walletScreenContentLoader.load( userWallet = action.selectedWallet, @@ -357,6 +362,7 @@ internal class WalletViewModel @Inject constructor( private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { walletScreenContentLoader.cancel(action.deletedWalletId) + tokenListStore.remove(action.deletedWalletId) walletScreenContentLoader.load( userWallet = action.selectedWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index 835a480b48..324b2fc7ca 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -136,7 +136,9 @@ internal class WalletsUpdateActionResolver @Inject constructor( unlockedWallets = wallets.filterNot(UserWallet::isLocked), ) } - isSelectedWalletCardsCountChanged(state, selectedWallet) -> Action.UpdateWalletCardCount(selectedWallet) + isSelectedWalletCardsCountChanged(state, selectedWallet) -> { + Action.UpdateWalletCardCount(selectedWallet) + } else -> Action.Unknown } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 7c128ca00d..066fe95381 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -12,6 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState @@ -37,6 +38,7 @@ internal interface WalletCardClickIntents { @Suppress("LongParameterList") internal class WalletCardClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, + private val tokenListStore: MultiWalletTokenListStore, private val walletEventSender: WalletEventSender, private val walletScreenContentLoader: WalletScreenContentLoader, private val renameWalletUseCase: RenameWalletUseCase, @@ -99,6 +101,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { viewModelScope.launch(dispatchers.main) { walletScreenContentLoader.cancel(userWalletId) + tokenListStore.remove(userWalletId) val walletToDelete = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { @@ -117,6 +120,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( reduxStateHolder.onUserWalletSelected(selectedWallet) } else { + tokenListStore.clear() stateHolder.clear() appRouter.replaceAll(AppRoute.Home) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index ec3f2d992d..a1ddf993ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -8,6 +8,7 @@ import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchTokenListUseCase +import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -117,7 +118,7 @@ internal class WalletClickIntents @Inject constructor( val maybeFetchResult = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) } else { - fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL) } maybeFetchResult.onLeft { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index e7386f4925..fb53cabe3d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -13,6 +13,7 @@ import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.* import com.tangem.domain.tokens.FetchTokenListUseCase +import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType @@ -124,18 +125,19 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main)) analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped) - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { val userWallet = getSelectedUserWallet() ?: return@launch derivePublicKeysUseCase( userWalletId = userWallet.walletId, currencies = missedAddressCurrencies, - ) - .onRight { - // Refresh must be set to true to ensure that yield balances are updated - fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) - } - .onLeft { Timber.e("Failed to derive public keys: $it") } + ).onLeft { + Timber.e(it, "Failed to derive public keys") + return@launch + } + + // Refresh must be set to true to ensure that yield balances are updated + fetchTokenListUseCase(userWallet.walletId, mode = RefreshMode.SKIP_CURRENCIES) } } From 568e735d2368dc4dac54b56eeb2aa5551be7d051 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Oct 2024 22:28:06 +0400 Subject: [PATCH 12/18] Updated on 2026-08-14 --- .../data/staking/DefaultStakingRepository.kt | 101 ++++++------------ .../repository/DefaultCurrenciesRepository.kt | 45 +------- .../repository/DefaultNetworksRepository.kt | 88 +-------------- .../staking/repositories/StakingRepository.kt | 7 +- .../CurrenciesStatusesLceOperations.kt | 2 +- .../tokens/repository/CurrenciesRepository.kt | 12 --- .../tokens/repository/NetworksRepository.kt | 41 ------- .../repository/MockCurrenciesRepository.kt | 10 +- .../repository/MockNetworksRepository.kt | 38 ------- .../repository/MockStakingRepository.kt | 13 +-- 10 files changed, 43 insertions(+), 314 deletions(-) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 8633e9d88c..bbf7601d27 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -101,10 +101,6 @@ internal class DefaultStakingRepository( private val yieldBalanceConverter = YieldBalanceConverter() private val yieldBalanceListConverter = YieldBalanceListConverter(yieldBalanceConverter) - private val isYieldBalanceFetching = MutableStateFlow( - value = emptyMap(), - ) - private val tronStakeKitTransactionAdapter by lazy { moshi.adapter(TronStakeKitTransaction::class.java) } override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) { @@ -384,79 +380,48 @@ internal class DefaultStakingRepository( ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext - try { - cacheRegistry.invokeOnExpire( - key = getYieldBalancesKey(userWalletId), - skipCache = refresh, - block = { - isYieldBalanceFetching.update { - it + (userWalletId to true) - } + cacheRegistry.invokeOnExpire( + key = getYieldBalancesKey(userWalletId), + skipCache = refresh, + block = { + val yields = getEnabledYields().ifEmpty { + Timber.i("No enabled yields for $userWalletId") + stakingBalanceStore.store(userWalletId, emptySet()) - val yields = getEnabledYields().ifEmpty { - Timber.i("No enabled yields for $userWalletId") + return@invokeOnExpire + } + val availableCurrencies = cryptoCurrencies + .mapNotNull { currency -> + val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) + val integrationId = integrationIdMap[getIntegrationKey(currency.id)] + + if (integrationId != null && yields.any { it.id == integrationId }) { + addresses to integrationId + } else { + null + } + } + .flatMap { (addresses, integrationId) -> + addresses.map { address -> address to integrationId } + } + .map { getBalanceRequestData(it.first.value, it.second) } + .ifEmpty { + Timber.i("No yield balances available for $userWalletId") stakingBalanceStore.store(userWalletId, emptySet()) return@invokeOnExpire } - val availableCurrencies = cryptoCurrencies - .mapNotNull { currency -> - val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) - val integrationId = integrationIdMap[getIntegrationKey(currency.id)] - if (integrationId != null && yields.any { it.id == integrationId }) { - addresses to integrationId - } else { - null - } - } - .flatMap { (addresses, integrationId) -> - addresses.map { address -> address to integrationId } - } - .map { getBalanceRequestData(it.first.value, it.second) } - .ifEmpty { - Timber.i("No yield balances available for $userWalletId") - stakingBalanceStore.store(userWalletId, emptySet()) + val result = stakeKitApi + .getMultipleYieldBalances(availableCurrencies) + .getOrThrow() - return@invokeOnExpire - } - - val result = stakeKitApi - .getMultipleYieldBalances(availableCurrencies) - .getOrThrow() - - stakingBalanceStore.store(userWalletId, result) - }, - ) - } finally { - isYieldBalanceFetching.update { - it - userWalletId - } - } + stakingBalanceStore.store(userWalletId, result) + }, + ) } - override fun getMultiYieldBalanceFlow( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): Flow = channelFlow { - if (!stakingFeatureToggle.isStakingEnabled) { - send(YieldBalanceList.Empty) - } else { - launch(dispatchers.io) { - stakingBalanceStore.get(userWalletId) - .collectLatest { send(yieldBalanceListConverter.convert(it)) } - } - - withContext(dispatchers.io) { - fetchMultiYieldBalance( - userWalletId, - cryptoCurrencies, - ) - } - } - }.cancellable() - - override fun getMultiYieldBalance( + override fun getMultiYieldBalanceUpdates( userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow = channelFlow { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 4664e120ce..e5b5f561bd 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -1,6 +1,5 @@ package com.tangem.data.tokens.repository -import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison import com.tangem.blockchainsdk.utils.toCoinId @@ -27,8 +26,6 @@ import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.core.error.DataError -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -67,10 +64,6 @@ internal class DefaultCurrenciesRepository( private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() private val customTokensMerger = CustomTokensMerger(tangemTechApi, dispatchers) - private val isMultiCurrencyWalletCurrenciesFetching = MutableStateFlow( - value = emptyMap(), - ) - override suspend fun saveTokens( userWalletId: UserWalletId, currencies: List, @@ -267,30 +260,6 @@ internal class DefaultCurrenciesRepository( } } - override fun getMultiCurrencyWalletCurrenciesUpdatesLce( - userWalletId: UserWalletId, - ): LceFlow> = lceFlow { - val userWallet = getUserWallet(userWalletId) - catch({ ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) }) { - raise(it) - } - - launch(dispatchers.io) { - combine( - getMultiCurrencyWalletCurrencies(userWallet), - isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } }, - ) { currencies, isFetching -> - send(currencies, isStillLoading = isFetching) - }.collect() - } - - withContext(dispatchers.io) { - catch({ fetchTokensIfCacheExpired(userWallet, refresh = false) }) { - raise(it) - } - } - } - override suspend fun getMultiCurrencyWalletCurrenciesSync( userWalletId: UserWalletId, refresh: Boolean, @@ -540,19 +509,7 @@ internal class DefaultCurrenciesRepository( cacheRegistry.invokeOnExpire( key = getTokensCacheKey(userWallet.walletId), skipCache = refresh, - block = { - isMultiCurrencyWalletCurrenciesFetching.update { - it + (userWallet.walletId to true) - } - - try { - fetchTokens(userWallet) - } finally { - isMultiCurrencyWalletCurrenciesFetching.update { - it - userWallet.walletId - } - } - }, + block = { fetchTokens(userWallet) }, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index d61f12d1a3..26ab1470f2 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -1,6 +1,5 @@ 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 @@ -15,8 +14,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress @@ -28,7 +25,10 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import timber.log.Timber @Suppress("LongParameterList") @@ -46,10 +46,6 @@ internal class DefaultNetworksRepository( private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory() } private val networkStatusFactory by lazy { NetworkStatusFactory() } - private val isNetworkStatusesFetching = MutableStateFlow( - value = emptyMap(), - ) - override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: Set, @@ -63,24 +59,6 @@ internal class DefaultNetworksRepository( } } - override fun getNetworkStatusesUpdatesLce( - userWalletId: UserWalletId, - networks: Set, - ): LceFlow> = lceFlow { - combine( - networksStatusesStore.get(userWalletId), - isNetworkStatusesFetching.map { it.getOrElse(userWalletId) { false } }, - ) { statuses, isFetching -> - send(statuses, isStillLoading = isFetching || networks.size != statuses.size) - }.launchIn(scope = this + dispatchers.io) - - withContext(dispatchers.io) { - catch({ fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) }) { - raise(it) - } - } - } - override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { val currencies = getCurrencies(userWalletId, networks) withContext(dispatchers.io) { @@ -123,67 +101,13 @@ internal class DefaultNetworksRepository( } } - override suspend fun getNetworkAddress( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): CryptoCurrencyAddress = withContext(dispatchers.io) { - CryptoCurrencyAddress( - cryptoCurrency = currency, - address = walletManagersFacade.getAddresses(userWalletId, currency.network) - .firstOrNull { it.type == AddressType.Default } - ?.value.orEmpty(), - ) - } - - override fun getNetworkAddressFlow( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): Flow = channelFlow { - launch(dispatchers.io) { - send(getNetworkAddress(userWalletId, currency)) - } - } - - override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List = - withContext(dispatchers.io) { - // Get list of currencies matching [network] - val currencies = getCurrencies(userWalletId) - - // There is no currencies matching given [networks] in [userWalletId] - if (currencies.toList().isEmpty()) return@withContext emptyList() - - currencies.toList().map { currency -> - CryptoCurrencyAddress( - cryptoCurrency = currency, - address = walletManagersFacade.getAddresses(userWalletId, currency.network) - .firstOrNull { it.type == AddressType.Default } - ?.value.orEmpty(), - ) - } - } - - override fun getNetworkAddressesFlow( - userWalletId: UserWalletId, - network: Network, - ): Flow> = channelFlow { - launch(dispatchers.io) { - send(getNetworkAddresses(userWalletId, network)) - } - } - - override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> = channelFlow { - launch(dispatchers.io) { - send(getNetworkAddresses(userWalletId)) - } - } - private suspend fun fetchNetworksStatusesIfCacheExpired( userWalletId: UserWalletId, networks: Set, refresh: Boolean, ) = coroutineScope { if (refresh) { - val statusesToRefresh = networks.map { NetworkStatus(it, NetworkStatus.Loading) } + val statusesToRefresh = networks.map { NetworkStatus(it, NetworkStatus.Refreshing) } networksStatusesStore.storeAll(userWalletId, statusesToRefresh) } @@ -230,8 +154,6 @@ internal class DefaultNetworksRepository( network: Network, currencies: Sequence, ) { - networksStatusesStore.store(userWalletId, NetworkStatus(network, NetworkStatus.Loading)) - val result = walletManagersFacade.update( userWalletId = userWalletId, network = network, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 6e17391a27..568683be77 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -49,12 +49,7 @@ interface StakingRepository { refresh: Boolean = false, ) - fun getMultiYieldBalanceFlow( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): Flow - - fun getMultiYieldBalance( + fun getMultiYieldBalanceUpdates( userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 730dbc541b..cc96309f1f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -186,7 +186,7 @@ internal class CurrenciesStatusesLceOperations( userWalletId: UserWalletId, cryptoCurrencies: List, ): EitherFlow { - return stakingRepository.getMultiYieldBalance(userWalletId, cryptoCurrencies) + return stakingRepository.getMultiYieldBalanceUpdates(userWalletId, cryptoCurrencies) .map> { it.right() } .catch { emit(TokenListError.DataError(it).left()) } .distinctUntilChanged() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 8a19b705e9..bda0e8fdbd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.core.error.DataError -import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency @@ -130,17 +129,6 @@ interface CurrenciesRepository { */ fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> - /** - * Retrieves updates of the list of cryptocurrencies within a multi-currency wallet. - * - * Loads remote cryptocurrencies if they have expired. - * - * @param userWalletId The unique identifier of the user wallet. - * @return A [LceFlow] emitting the set of cryptocurrencies associated with the user wallet. May emit an - * [DataError.UserWalletError.WrongUserWallet] if single-currency user wallet ID provided. - */ - fun getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId: UserWalletId): LceFlow> - /** * Retrieves the list of cryptocurrencies within a multi-currency wallet. * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index a2a99ddea2..2e568b83b4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -1,7 +1,5 @@ package com.tangem.domain.tokens.repository -import com.tangem.domain.core.lce.LceFlow -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 @@ -23,20 +21,6 @@ interface NetworksRepository { */ fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> - /** - * Retrieves updates of network statuses of specified blockchain networks for a specific user wallet. - * - * Loads remote network statuses if they have expired. - * - * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network which statuses are to be retrieved. - * @return A [LceFlow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. - */ - fun getNetworkStatusesUpdatesLce( - userWalletId: UserWalletId, - networks: Set, - ): LceFlow> - /** * Fetches pending transactions for given network * @@ -63,33 +47,8 @@ interface NetworksRepository { fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean - /** - * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] - */ - fun getNetworkAddressesFlow(userWalletId: UserWalletId, network: Network): Flow> - /** * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] */ suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List - - /** - * Returns address of [cryptoCurrency] in selected wallet [userWalletId] - */ - suspend fun getNetworkAddress(userWalletId: UserWalletId, currency: CryptoCurrency): CryptoCurrencyAddress - - /** - * Returns address of [cryptoCurrency] in selected wallet [userWalletId] - */ - fun getNetworkAddressFlow(userWalletId: UserWalletId, currency: CryptoCurrency): Flow - - /** - * Returns list of addresses and crypto currency info in selected wallet [userWalletId] - */ - fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> - - /** - * Returns list of addresses and crypto currency info in selected wallet [userWalletId] - */ - suspend fun getNetworkAddresses(userWalletId: UserWalletId): List } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index cc16af1aba..f68979c6f4 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -3,8 +3,6 @@ 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.core.lce.LceFlow -import com.tangem.domain.core.utils.toLce import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency @@ -57,7 +55,7 @@ internal class MockCurrenciesRepository( override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) = Unit - override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow> { + override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return emptyFlow() } @@ -91,12 +89,6 @@ internal class MockCurrenciesRepository( return tokens.map { it.getOrElse { e -> throw e } } } - override fun getMultiCurrencyWalletCurrenciesUpdatesLce( - userWalletId: UserWalletId, - ): LceFlow> { - return tokens.map { it.toLce() } - } - override suspend fun getMultiCurrencyWalletCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 5b5019dfd6..2e56eb6aa0 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -3,15 +3,11 @@ 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.core.lce.LceFlow -import com.tangem.domain.core.utils.toLce -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.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -26,13 +22,6 @@ internal class MockNetworksRepository( return statuses.map { it.getOrElse { e -> throw e } } } - override fun getNetworkStatusesUpdatesLce( - userWalletId: UserWalletId, - networks: Set, - ): LceFlow> { - return statuses.map { it.toLce() } - } - override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { // no-op } @@ -47,37 +36,10 @@ internal class MockNetworksRepository( override fun isNeedToCreateAccountWithoutReserve(network: Network) = false - override fun getNetworkAddressesFlow( - userWalletId: UserWalletId, - network: Network, - ): Flow> = channelFlow { - send(emptyList()) - } - - override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> = channelFlow { - send(emptyList()) - } - override suspend fun getNetworkAddresses( userWalletId: UserWalletId, network: Network, ): List { return emptyList() } - - override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List { - return emptyList() - } - - override suspend fun getNetworkAddress( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): CryptoCurrencyAddress = CryptoCurrencyAddress(currency, "") - - override fun getNetworkAddressFlow( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): Flow = channelFlow { - send(CryptoCurrencyAddress(currency, "")) - } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 9b702bacbb..0dc93edf8d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -145,18 +145,7 @@ class MockStakingRepository : StakingRepository { /* no-op */ } - override fun getMultiYieldBalanceFlow( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): Flow = channelFlow { - send( - YieldBalanceList.Data( - balances = listOf(YieldBalance.Error), - ), - ) - } - - override fun getMultiYieldBalance( + override fun getMultiYieldBalanceUpdates( userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow = flowOf() From ba0d4ff055ca73473b4059c9b494dbbca75a21e9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Oct 2024 13:31:04 +0400 Subject: [PATCH 13/18] Updated on 2026-08-14 --- .../preview/PreviewManageTokensComponent.kt | 2 ++ .../entity/managetokens/ManageTokensUM.kt | 3 +++ .../managetokens/model/ManageTokensModel.kt | 23 ++++++++++++++---- .../managetokens/ui/ManageTokensScreen.kt | 24 ++++++++++++++++--- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index a0058fc2e9..abecdf97cb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.update internal class PreviewManageTokensComponent( private val isLoading: Boolean, + private val showTangemIcon: Boolean, params: ManageTokensComponent.Params, ) : ManageTokensComponent { @@ -65,6 +66,7 @@ internal class PreviewManageTokensComponent( loadMore = { false }, saveChanges = {}, isSavingInProgress = false, + needToAddDerivations = showTangemIcon, ), ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt index d16e17e84a..708b149aca 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt @@ -42,6 +42,7 @@ internal sealed class ManageTokensUM { val saveChanges: () -> Unit, val hasChanges: Boolean, val isSavingInProgress: Boolean, + val needToAddDerivations: Boolean, ) : ManageTokensUM() fun copySealed( @@ -52,6 +53,7 @@ internal sealed class ManageTokensUM { isNextBatchLoading: Boolean = this.isNextBatchLoading, isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress, scrollToTop: StateEvent = this.scrollToTop, + needToAddDerivations: Boolean = this is ManageContent && this.needToAddDerivations, ): ManageTokensUM { return when (this) { is ManageContent -> copy( @@ -62,6 +64,7 @@ internal sealed class ManageTokensUM { isNextBatchLoading = isNextBatchLoading, isSavingInProgress = isSavingInProgress, scrollToTop = scrollToTop, + needToAddDerivations = needToAddDerivations, ) is ReadContent -> copy( search = search, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index f5375b9b43..36916e335d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent @@ -47,6 +48,7 @@ internal class ManageTokensModel @Inject constructor( private val router: Router, private val manageTokensListManager: ManageTokensListManager, private val messageSender: UiMessageSender, + private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val saveManagedTokensUseCase: SaveManagedTokensUseCase, private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, @@ -140,6 +142,7 @@ internal class ManageTokensModel @Inject constructor( hasChanges = false, saveChanges = ::saveChanges, loadMore = ::loadMoreItems, + needToAddDerivations = false, isSavingInProgress = false, ) } @@ -255,10 +258,22 @@ internal class ManageTokensModel @Inject constructor( } private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) { - state.update { state -> - state.copySealed( - hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(), - ) + modelScope.launch { + val hasMissedDerivations = params.userWalletId?.let { walletId -> + val networks = currenciesToAdd.values + .flatten() + .toSet() + .associate { it.backendId to null } + + hasMissedDerivationsUseCase(walletId, networks) + } + + state.update { state -> + state.copySealed( + hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(), + needToAddDerivations = hasMissedDerivations ?: false, + ) + } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 6b2547a8fd..2d604c67ee 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -33,11 +33,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.SearchBar @@ -122,6 +128,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi .fillMaxWidth(), isVisible = state.hasChanges, showProgress = state.isSavingInProgress, + showIcon = state.needToAddDerivations, onClick = state.saveChanges, ) } @@ -158,6 +165,7 @@ private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarU private fun SaveChangesButton( isVisible: Boolean, showProgress: Boolean, + showIcon: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -168,10 +176,17 @@ private fun SaveChangesButton( exit = fadeOut(), label = "save_button_visibility", ) { - PrimaryButtonIconEnd( + TangemButton( text = stringResource(id = R.string.common_save), - iconResId = R.drawable.ic_tangem_24, + icon = if (showIcon) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + }, showProgress = showProgress, + colors = TangemButtonsDefaults.primaryButtonColors, + enabled = true, + animateContentChange = true, onClick = onClick, ) } @@ -472,6 +487,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider Date: Tue, 22 Oct 2024 19:28:49 +0400 Subject: [PATCH 14/18] Updated on 2026-08-14 --- .../entity/customtoken/CustomTokenFormUM.kt | 1 + .../managetokens/model/CustomTokenFormModel.kt | 10 +++++++++- .../managetokens/ui/CustomTokenFormContent.kt | 13 +++++++++++-- .../utils/ui/CustomCurrencyFormOperations.kt | 2 ++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index b4fbaea7be..8f3c97fef8 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -14,6 +14,7 @@ internal data class CustomTokenFormUM( val notifications: PersistentList = persistentListOf(), val canAddToken: Boolean = false, val isValidating: Boolean = false, + val needToAddDerivation: Boolean = false, val saveToken: () -> Unit, ) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index c1c6f9922e..f6c43189f7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.ContentMessage import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency @@ -43,6 +44,7 @@ internal class CustomTokenFormModel @Inject constructor( private val customCurrencyValidator: CustomCurrencyValidator, private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val derivePublicKeysUseCase: DerivePublicKeysUseCase, + private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val messageSender: UiMessageSender, private val customTokenFormManager: CustomCurrencyFormBuilder, private val analyticsEventHandler: AnalyticsEventHandler, @@ -156,7 +158,12 @@ internal class CustomTokenFormModel @Inject constructor( fillForm: Boolean, isAlreadyAdded: Boolean, isCustom: Boolean, - ) { + ) = modelScope.launch { + val needToAddDerivation = hasMissedDerivationsUseCase( + userWalletId = params.userWalletId, + networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value), + ) + state.update { state -> var updatedState = state .updateWithProgress( @@ -166,6 +173,7 @@ internal class CustomTokenFormModel @Inject constructor( clearNotifications = true, clearFieldErrors = true, disableSecondaryFields = !isCustom, + needToAddDerivation = needToAddDerivation, ) if (fillForm) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt index 5f5798395b..de79186792 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt @@ -22,9 +22,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.bottomFade +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.fields.SimpleTextField import com.tangem.core.ui.components.isOpened import com.tangem.core.ui.components.keyboardAsState @@ -82,14 +84,21 @@ internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier } } - PrimaryButton( + TangemButton( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = TangemTheme.dimens.spacing16 + bottomBarHeight) .fillMaxWidth(), text = stringResource(id = R.string.custom_token_add_token), + colors = TangemButtonsDefaults.primaryButtonColors, enabled = model.canAddToken, showProgress = model.isValidating, + animateContentChange = true, + icon = if (model.needToAddDerivation) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + }, onClick = model.saveToken, ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt index d99bb9473b..147642b2bf 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt @@ -27,6 +27,7 @@ internal fun CustomTokenFormUM.updateWithProgress( showProgress: Boolean, isWasFilled: Boolean = this.tokenForm?.wasFilled ?: false, canAddToken: Boolean = this.canAddToken, + needToAddDerivation: Boolean = false, clearNotifications: Boolean = false, clearFieldErrors: Boolean = false, disableSecondaryFields: Boolean = false, @@ -34,6 +35,7 @@ internal fun CustomTokenFormUM.updateWithProgress( return copy( isValidating = showProgress, canAddToken = canAddToken, + needToAddDerivation = needToAddDerivation, notifications = if (clearNotifications) persistentListOf() else notifications, ).updateTokenForm { val updatedFields = fields.mapValues { (key, field) -> From f1a23ce0472b427596e83da14bda306f97c527be Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Oct 2024 13:01:54 +0400 Subject: [PATCH 15/18] Updated on 2026-08-14 --- .../data/staking/DefaultStakingRepository.kt | 2 ++ .../intents/WalletWarningsClickIntents.kt | 15 ++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index bbf7601d27..2ee25c9a24 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -409,6 +409,8 @@ internal class DefaultStakingRepository( Timber.i("No yield balances available for $userWalletId") stakingBalanceStore.store(userWalletId, emptySet()) + cacheRegistry.invalidate(getYieldBalancesKey(userWalletId)) + return@invokeOnExpire } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index fb53cabe3d..2db549db07 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -131,13 +131,14 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( derivePublicKeysUseCase( userWalletId = userWallet.walletId, currencies = missedAddressCurrencies, - ).onLeft { - Timber.e(it, "Failed to derive public keys") - return@launch - } - - // Refresh must be set to true to ensure that yield balances are updated - fetchTokenListUseCase(userWallet.walletId, mode = RefreshMode.SKIP_CURRENCIES) + ).fold( + ifLeft = { Timber.e(it, "Failed to derive public keys") }, + ifRight = { + fetchTokenListUseCase(userWallet.walletId, mode = RefreshMode.SKIP_CURRENCIES).onLeft { + Timber.e("Unable to refresh token list: $it") + } + }, + ) } } From 5fba9250e815c1ec6f9a7c97207cdbf9e7cc0049 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Oct 2024 15:54:21 +0400 Subject: [PATCH 16/18] Updated on 2026-08-14 --- .../tangem/features/managetokens/ui/CustomTokenFormContent.kt | 1 + .../com/tangem/features/managetokens/ui/ManageTokensScreen.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt index de79186792..fca42a82dc 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt @@ -99,6 +99,7 @@ internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier } else { TangemButtonIconPosition.None }, + textStyle = TangemTheme.typography.subtitle1, onClick = model.saveToken, ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 2d604c67ee..3a85636b3e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -185,6 +185,7 @@ private fun SaveChangesButton( }, showProgress = showProgress, colors = TangemButtonsDefaults.primaryButtonColors, + textStyle = TangemTheme.typography.subtitle1, enabled = true, animateContentChange = true, onClick = onClick, From ff5d3cb8b7bb3ba5511c5cbe04b2c02d09593d31 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Oct 2024 14:32:20 +0400 Subject: [PATCH 17/18] Updated on 2026-08-14 --- .../tokenlist/entity/TokenListUM.kt | 35 ++++++++++++ .../tokenlist/entity/TokenListUMController.kt | 54 +++++++++++++++++++ .../entity/TokenListUMTransformer.kt | 10 ++++ .../transformer/SearchBarUMTransformer.kt | 33 ++++++++++++ .../UpdateSearchBarActiveStateTransformer.kt | 19 +++++++ .../UpdateSearchBarIntentsTransformer.kt | 13 +++++ .../UpdateSearchQueryTransformer.kt | 10 ++++ .../UpdateTokenItemsTransformer.kt | 28 ++++++++++ .../state/model/WalletTokensListState.kt | 6 +++ .../multicurrency/MultiCurrencyContentItem.kt | 9 ++++ 10 files changed, 217 insertions(+) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUM.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMController.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/SearchBarUMTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarIntentsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUM.kt new file mode 100644 index 0000000000..83d5fc1640 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUM.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.wallet.presentation.tokenlist.entity + +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * Token list UM + * + * @property items items (search bar, tokens, headers) + * @property isBalanceHidden flag that indicates if balance should be hidden + * +[REDACTED_AUTHOR] + */ +internal data class TokenListUM( + val items: ImmutableList, + val isBalanceHidden: Boolean, +) { + + /** Get search bar if it exists */ + fun getSearchBar(): TokensListItemState.SearchBar? { + return items.firstOrNull() as? TokensListItemState.SearchBar + } + + /** Get tokens */ + fun getTokens(): ImmutableList { + if (getSearchBar() == null) return items + + return if (items.size > 1) { + items.subList(fromIndex = 1, toIndex = items.size) + } else { + persistentListOf() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMController.kt new file mode 100644 index 0000000000..6e3f7a5c6a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMController.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.wallet.presentation.tokenlist.entity + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber +import javax.inject.Inject + +/** + * [TokenListUM] controller + * +[REDACTED_AUTHOR] + */ +internal class TokenListUMController @Inject constructor() { + + val state: StateFlow get() = _state + + private val _state: MutableStateFlow = MutableStateFlow( + value = TokenListUM( + items = persistentListOf(createInitialSearchBar()), + isBalanceHidden = false, + ), + ) + + fun update(transform: (TokenListUM) -> TokenListUM) { + Timber.d("Applying non-name transformation") + _state.update(transform) + } + + fun update(transformer: TokenListUMTransformer) { + Timber.d("Applying ${transformer::class.simpleName}") + _state.update(transformer::transform) + } + + /** Get search bar if it exists */ + fun getSearchBar(): TokensListItemState.SearchBar? = _state.value.getSearchBar() + + private fun createInitialSearchBar(): TokensListItemState.SearchBar { + return TokensListItemState.SearchBar( + searchBarUM = SearchBarUM( + placeholderText = resourceReference(id = R.string.common_search), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMTransformer.kt new file mode 100644 index 0000000000..49678c022f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.wallet.presentation.tokenlist.entity + +import com.tangem.utils.transformer.Transformer + +/** + * Base [TokenListUM] transformer + * +[REDACTED_AUTHOR] + */ +internal interface TokenListUMTransformer : Transformer \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/SearchBarUMTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/SearchBarUMTransformer.kt new file mode 100644 index 0000000000..a177307b2c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/SearchBarUMTransformer.kt @@ -0,0 +1,33 @@ +package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM +import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUMTransformer +import kotlinx.collections.immutable.persistentListOf + +/** + * Base [SearchBarUM] transformer + * +[REDACTED_AUTHOR] + */ +internal abstract class SearchBarUMTransformer : TokenListUMTransformer { + + abstract fun transform(prevState: SearchBarUM): SearchBarUM + + override fun transform(prevState: TokenListUM): TokenListUM { + val searchBarItem = prevState.getSearchBar() + + return if (searchBarItem != null) { + val updatedSearchBar = searchBarItem.copy(searchBarUM = transform(searchBarItem.searchBarUM)) + + prevState.copy( + items = persistentListOf( + updatedSearchBar, + *prevState.getTokens().toTypedArray(), + ), + ) + } else { + prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt new file mode 100644 index 0000000000..f33d3ea4ee --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.wallet.impl.R + +internal class UpdateSearchBarActiveStateTransformer(private val isActive: Boolean) : SearchBarUMTransformer() { + + override fun transform(prevState: SearchBarUM): SearchBarUM { + val placeholderText = if (isActive) { + TextReference.EMPTY + } else { + resourceReference(id = R.string.common_search) + } + + return prevState.copy(placeholderText = placeholderText, isActive = isActive) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarIntentsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarIntentsTransformer.kt new file mode 100644 index 0000000000..de58815cd1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarIntentsTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer + +import com.tangem.core.ui.components.fields.entity.SearchBarUM + +internal class UpdateSearchBarIntentsTransformer( + private val onQueryChange: (String) -> Unit, + private val onActiveChange: (Boolean) -> Unit, +) : SearchBarUMTransformer() { + + override fun transform(prevState: SearchBarUM): SearchBarUM { + return prevState.copy(onQueryChange = onQueryChange, onActiveChange = onActiveChange) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt new file mode 100644 index 0000000000..93a6a20675 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer + +import com.tangem.core.ui.components.fields.entity.SearchBarUM + +internal class UpdateSearchQueryTransformer(private val newQuery: String) : SearchBarUMTransformer() { + + override fun transform(prevState: SearchBarUM): SearchBarUM { + return prevState.copy(query = newQuery) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt new file mode 100644 index 0000000000..89d171d3f9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer + +import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM +import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUMTransformer +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState +import kotlinx.collections.immutable.toImmutableList + +internal class UpdateTokenItemsTransformer( + appCurrency: AppCurrency, + onItemClick: (CryptoCurrencyStatus) -> Unit, + private val statuses: List, + private val isBalanceHidden: Boolean, +) : TokenListUMTransformer { + + private val converter = TokenItemStateConverter(appCurrency = appCurrency, onItemClick = onItemClick) + + override fun transform(prevState: TokenListUM): TokenListUM { + val items = converter.convertList(input = statuses).map(TokensListItemState::Token) + + return prevState.copy( + items = (listOfNotNull(prevState.getSearchBar()) + items).toImmutableList(), + isBalanceHidden = isBalanceHidden, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt index c582f8d8d8..29daa446b4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.wallet.impl.R @@ -43,6 +44,11 @@ internal sealed class WalletTokensListState { abstract val id: Any + data class SearchBar( + override val id: Any = "search_bar", + val searchBarUM: SearchBarUM, + ) : TokensListItemState() + data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemState() data class Token(val state: TokenItemState) : TokensListItemState() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt index 6c3ca94c12..0b8e4ea13b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -1,8 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.token.TokenItem import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -36,5 +39,11 @@ internal fun MultiCurrencyContentItem( modifier = modifierWithBackground, ) } + is TokensListItemState.SearchBar -> { + SearchBar( + state = state.searchBarUM, + modifier = modifierWithBackground.padding(all = 12.dp), + ) + } } } \ No newline at end of file From 7476cc4e984252772ebf224156de7d534987eb4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Oct 2024 16:14:28 +0400 Subject: [PATCH 18/18] Updated on 2026-08-14 --- .../tangem/domain/tokens/model/TokenList.kt | 9 +++++++++ .../operations/TokenListSortingOperations.kt | 6 +----- .../utils/TokenListAnalyticsSender.kt | 9 +-------- .../domain/GetMultiWalletWarningsFactory.kt | 18 +++--------------- .../wallet/domain/WalletWithFundsChecker.kt | 11 +---------- .../subscribers/BasicTokenListSubscriber.kt | 18 ++++++++---------- .../MultiWalletTokenListSubscriber.kt | 8 +------- 7 files changed, 24 insertions(+), 55 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt index 88c7af38bd..f5eaf0b0e3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -54,4 +54,13 @@ sealed class TokenList { enum class SortType { NONE, BALANCE, } + + /** Get flatten list of cryptocurrency status [CryptoCurrencyStatus] */ + fun flattenCurrencies(): List { + return when (this) { + is GroupedByNetwork -> groups.flatMap(NetworkGroup::currencies) + is Ungrouped -> currencies + is Empty -> emptyList() + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index 7e4bc12ae2..6e27d5c2d7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -23,11 +23,7 @@ internal class TokenListSortingOperations( sortByBalance: Boolean = tokenList.sortedBy == TokenList.SortType.BALANCE, isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TotalFiatBalance.Loading, ) : this( - currencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies } - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty -> emptyList() - }, + currencies = tokenList.flattenCurrencies(), isAnyTokenLoading = isAnyTokenLoading, sortByBalance = sortByBalance, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index 5d9840556b..28dfac6367 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -9,7 +9,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase import com.tangem.domain.analytics.model.WalletBalanceState import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet @@ -38,7 +37,7 @@ internal class TokenListAnalyticsSender @Inject constructor( if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return if (tokenList.totalFiatBalance is TotalFiatBalance.Loading) return - val currenciesStatuses = getCurrenciesStatuses(tokenList) + val currenciesStatuses = tokenList.flattenCurrencies() sendBalanceLoadedEventIfNeeded(tokenList.totalFiatBalance, currenciesStatuses) sendToppedUpEventIfNeeded(userWallet, tokenList.totalFiatBalance, currenciesStatuses) @@ -46,12 +45,6 @@ internal class TokenListAnalyticsSender @Inject constructor( sendTokenBalancesIfNeeded(currenciesStatuses) } - private fun getCurrenciesStatuses(tokenList: TokenList): List = when (tokenList) { - is TokenList.Empty -> emptyList() - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) - is TokenList.Ungrouped -> tokenList.currencies - } - private fun sendBalanceLoadedEventIfNeeded( fiatBalance: TotalFiatBalance, currenciesStatuses: List, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 90b969933f..7178667b98 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -10,7 +10,6 @@ import com.tangem.domain.settings.ShouldShowRingPromoUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.repository.PromoRepository import com.tangem.domain.wallets.models.UserWallet @@ -144,13 +143,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun Lce.getMissingAddressCurrencies(): List { val tokenList = getOrNull(isPartialContentAccepted = false) ?: return emptyList() - val currencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty -> emptyList() - } - - return currencies + return tokenList + .flattenCurrencies() .filter { it.value is CryptoCurrencyStatus.MissedDerivation } .map(CryptoCurrencyStatus::currency) } @@ -182,13 +176,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun Lce.hasUnreachableNetworks(): Boolean { val tokenList = getOrNull(isPartialContentAccepted = false) ?: return false - val currencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty -> emptyList() - } - - return currencies.any { it.value is CryptoCurrencyStatus.Unreachable } + return tokenList.flattenCurrencies().any { it.value is CryptoCurrencyStatus.Unreachable } } private fun MutableList.addRateTheAppNotification( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt index bec1af9947..586234c6d6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.common.extensions.isZero import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import javax.inject.Inject @@ -12,15 +11,7 @@ internal class WalletWithFundsChecker @Inject constructor( ) { suspend fun check(tokenList: TokenList) { - val hasNonZeroWallets = when (tokenList) { - is TokenList.GroupedByNetwork -> { - tokenList.groups - .flatMap(NetworkGroup::currencies) - .hasNonZeroWallets() - } - is TokenList.Ungrouped -> tokenList.currencies.hasNonZeroWallets() - is TokenList.Empty -> false - } + val hasNonZeroWallets = tokenList.flattenCurrencies().hasNonZeroWallets() if (hasNonZeroWallets) setWalletWithFundsFoundUseCase() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index 9c1bbbd713..a21e20afc9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -8,7 +8,6 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -99,15 +98,14 @@ internal abstract class BasicTokenListSubscriber( private suspend fun startCheck(maybeTokenList: Lce) { // Run Polkadot account health check maybeTokenList.getOrNull()?.let { tokenList -> - val cryptoCurrencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty -> emptyList() - } - - cryptoCurrencies.forEach { - runPolkadotAccountHealthCheckUseCase(userWallet.walletId, it.currency.network) - } + tokenList + .flattenCurrencies() + .forEach { + runPolkadotAccountHealthCheckUseCase( + userWalletId = userWallet.walletId, + network = it.currency.network, + ) + } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index 79cef234af..a1fb33dfd0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -70,12 +70,6 @@ internal class MultiWalletTokenListSubscriber( } private fun getCurrenciesIds(tokenList: TokenList): List { - return when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { group -> - group.currencies.map { it.currency.id } - } - is TokenList.Ungrouped -> tokenList.currencies.map { it.currency.id } - is TokenList.Empty -> emptyList() - } + return tokenList.flattenCurrencies().map { it.currency.id } } } \ No newline at end of file