From 6465307d0ad616ee58090d52a1e3e86065e2c90a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Oct 2024 12:07:48 +0300 Subject: [PATCH 01/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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 From 1ab4cdd170be92b72bf2518f94bd04e37f4fef31 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Oct 2024 09:27:57 +0300 Subject: [PATCH 19/41] Updated on 2026-08-14 --- .../app/TangemWcBlockchainHelper.kt | 35 ++++++++++++------- .../domain/WalletConnectInteractor.kt | 18 ++++------ .../domain/WcBlockchainHelper.kt | 6 +++- .../walletconnect2/domain/WcJrpcMethods.kt | 4 +-- .../domain/WcSessionRequestConverter.kt | 20 +++++++++-- .../models/solana/SolanaTransactionRequest.kt | 5 ++- .../walletconnect/WalletConnectMiddleware.kt | 17 ++++----- .../configs/feature_toggles_config.json | 2 +- .../DefaultBlockchainSDKFactory.kt | 3 +- version.properties | 2 +- 10 files changed, 69 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt index edddd1a886..8ed86f9fa2 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper +import com.tangem.tap.domain.walletconnect2.domain.models.Account import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles internal class TangemWcBlockchainHelper( @@ -35,11 +36,12 @@ internal class TangemWcBlockchainHelper( } } - override fun networkIdToChainIdOrNull(networkId: String): String? { + override fun networkIdToChainIdOrNull(networkId: String): List { val blockchain = Blockchain.fromNetworkId(networkId) - val namespace = blockchain?.getCaip2Namespace() ?: return null - val chainId = blockchain.getCaip2ChainId() ?: return null - return "$namespace$CHAIN_SEPARATOR$chainId" + val namespace = blockchain?.getCaip2Namespace() ?: return emptyList() + return blockchain.getCaip2ChainIds().map { + "$namespace$CHAIN_SEPARATOR$it" + } } override fun getNamespaceFromFullChainIdOrNull(chainId: String): String? { @@ -52,8 +54,18 @@ internal class TangemWcBlockchainHelper( return Blockchain.fromNetworkId(networkId)?.fullName } - private fun Blockchain.getCaip2ChainId(): String? { - if (this.isEvm()) return this.getChainId()?.toString() + override fun chainIdsToAccounts( + walletAddress: String, + chainIds: List, + derivationPath: String?, + ): List { + return chainIds.map { chainId -> + Account(chainId, walletAddress, derivationPath) + } + } + + private fun Blockchain.getCaip2ChainIds(): List { + if (this.isEvm()) return listOfNotNull(this.getChainId()?.toString()) return when (this) { /* @@ -61,13 +73,12 @@ internal class TangemWcBlockchainHelper( * uncommented is used. * Docs: https://docs.walletconnect.com/advanced/multichain/chain-list * - * Blockchain.Solana -> "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" * */ - Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ" - Blockchain.SolanaTestnet -> "z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z" - Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182" - Blockchain.Tron -> "0x2b6653dc" - else -> null + Blockchain.Solana -> listOf("5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ") + Blockchain.SolanaTestnet -> listOf("z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z") + Blockchain.Polkadot -> listOf("91b171bb158e2d3848fa23a9f1c25182") + Blockchain.Tron -> listOf("0x2b6653dc") + else -> emptyList() } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 122a9ed76f..9d3e956799 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -102,7 +102,7 @@ class WalletConnectInteractor( private suspend fun setupUserChains(userWallet: UserWallet, currencies: List) { val accounts = getAccountsForWc( userWallet = userWallet, - networks = currencies.map { it.network }, + networks = currencies.map { it.network }.distinct(), ) setUserChains(accounts) handleDeeplinkStack(accounts) @@ -403,18 +403,14 @@ class WalletConnectInteractor( derivationPath = it.derivationPath.value, ) } - return walletManagers.mapNotNull { + return walletManagers.flatMap { val wallet = it.wallet - val chainId = blockchainHelper.networkIdToChainIdOrNull( - wallet.blockchain.toNetworkId(), + val chainIds = blockchainHelper.networkIdToChainIdOrNull(wallet.blockchain.toNetworkId()) + blockchainHelper.chainIdsToAccounts( + walletAddress = wallet.address, + chainIds = chainIds, + derivationPath = wallet.publicKey.derivationPath?.rawPath, ) - chainId?.let { - Account( - chainId, - wallet.address, - wallet.publicKey.derivationPath?.rawPath, - ) - } } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt index 489d086af0..111c5c3d6f 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt @@ -1,13 +1,17 @@ package com.tangem.tap.domain.walletconnect2.domain +import com.tangem.tap.domain.walletconnect2.domain.models.Account + interface WcBlockchainHelper { fun chainIdToNetworkIdOrNull(chainId: String): String? fun chainIdToMissingNetworkNameOrNull(chainId: String): String? - fun networkIdToChainIdOrNull(networkId: String): String? + fun networkIdToChainIdOrNull(networkId: String): List fun getNamespaceFromFullChainIdOrNull(chainId: String): String? fun chainIdToFullNameOrNull(chainId: String): String? + + fun chainIdsToAccounts(walletAddress: String, chainIds: List, derivationPath: String?): List } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt index 3cb1187509..7c937b3772 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt @@ -223,14 +223,14 @@ internal class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private WcRequest.AddChain(data = deserializedParams) } WcJrpcMethods.SOLANA_SIGN_TX -> { - val tx = moshi.adapter(SolanaTransactionRequest::class.java) + val tx = moshi.adapter(SolanaTransactionRequest::class.java) .fromJsonOrNull(params) ?: return customRequest WcRequest.SolanaSignRequest(data = tx) } WcJrpcMethods.SOLANA_SIGN_MESSAGE -> { - val signMessage = moshi.adapter(SolanaSignMessage::class.java) + val signMessage = moshi.adapter(SolanaSignMessage::class.java) .fromJsonOrNull(params) ?: return customRequest val data = WcSignMessage( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt index e02d7cc679..367d7b6a9e 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt @@ -1,12 +1,12 @@ package com.tangem.tap.domain.walletconnect2.domain import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper -import com.tangem.tap.domain.walletconnect2.domain.mapper.mapToTransaction import com.tangem.tap.domain.walletconnect2.domain.models.BnbData import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType +import okio.ByteString.Companion.decodeBase64 internal class WcSessionRequestConverter( private val blockchainHelper: WcBlockchainHelper, @@ -115,11 +115,11 @@ internal class WcSessionRequestConverter( ) } is WcRequest.SolanaSignRequest -> { - val data = request.data.mapToTransaction() + val transaction = request.data.transaction WcPreparedRequest.SignTransaction( preparedRequestData = WcGenericTransactionData( - hashToSign = data.getSerializedMessage(), + hashToSign = transaction.prepareSolanaTransaction(), dAppName = sessionRequest.metaName, type = TransactionType.SOLANA_TX, ), @@ -132,6 +132,16 @@ internal class WcSessionRequestConverter( } } + /** + * Input transaction in Base64 string + */ + private fun String.prepareSolanaTransaction(): ByteArray { + return this.decodeBase64() + ?.toByteArray() + ?.drop(SOLANA_SIGNATURE_PLACEHOLDER_LENGTH) + ?.toByteArray() ?: ByteArray(0) + } + private fun getWalletAddress(request: WcRequest): String? { return when (request) { is WcRequest.BnbTrade -> request.data.accountNumber @@ -156,4 +166,8 @@ internal class WcSessionRequestConverter( it.chainId == sessionRequest.chainId && it.walletAddress.lowercase() == walletAddress?.lowercase() }?.derivationPath } + + companion object { + private const val SOLANA_SIGNATURE_PLACEHOLDER_LENGTH = 65 + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt index 7ea66042f4..b77ecffae0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt @@ -14,6 +14,9 @@ data class SolanaTransactionRequest( @Json(name = "instructions") val instructions: List, + + @Json(name = "transaction") + val transaction: String, ) : WcRequestData { @JsonClass(generateAdapter = true) @@ -22,7 +25,7 @@ data class SolanaTransactionRequest( val programId: String, @Json(name = "data") - val data: List, + val data: String, @Json(name = "keys") val keys: List, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 6b0e0898ec..27df30674a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -13,7 +13,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.domain.walletconnect2.domain.models.Account import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.proxy.redux.DaggerGraphState @@ -87,18 +86,16 @@ class WalletConnectMiddleware { is WalletConnectAction.ApproveProposal -> { scope.launch { val accounts = getWalletManagers() - .mapNotNull { + .flatMap { val wallet = it.wallet - val chainId = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull( + val chainIds = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull( wallet.blockchain.toNetworkId(), ) - chainId?.let { - Account( - chainId, - wallet.address, - wallet.publicKey.derivationPath?.rawPath, - ) - } + walletConnectInteractor.blockchainHelper.chainIdsToAccounts( + walletAddress = wallet.address, + chainIds = chainIds, + derivationPath = wallet.publicKey.derivationPath?.rawPath, + ) } walletConnectInteractor.approveSessionProposal(accounts) } 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 97119cabb4..b906e0ce74 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -5,7 +5,7 @@ }, { "name": "WC_SOLANA_TX_SIGN_ENABLED", - "version": "undefined" + "version": "5.18.0" }, { "name": "STAKING_ENABLED", 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 9e5dbd53c8..741f900b6d 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 @@ -55,7 +55,8 @@ internal class DefaultBlockchainSDKFactory( // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] transform = walletManagerFactoryCreator::create, ) - .stateIn(scope = mainScope, started = SharingStarted.Lazily, initialValue = null) + // don't use Lazily because some features (WC) require initialized factory on app started + .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) } private fun CoroutineScope.updateBlockchainProviderTypes() { diff --git a/version.properties b/version.properties index e7ffe36363..3a969ad8fe 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -versionName=5.17.0 \ No newline at end of file +versionName=5.18.0 \ No newline at end of file From 50286e25581da3e70edbab1aa28997a512494cff Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Oct 2024 13:50:31 +0300 Subject: [PATCH 20/41] Updated on 2026-08-14 --- .../impl/presentation/state/utils/StakingPendingActionUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ae8f6d4cb2..2a8113944f 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 @@ -56,7 +56,7 @@ internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState) } internal fun isTronStakedBalance(networkId: String, pendingAction: PendingAction?): Boolean { - return isTron(networkId) && pendingAction?.type == StakingActionType.REVOKE + return isTron(networkId) && pendingAction?.type == StakingActionType.REVOTE } internal fun isComposePendingActions(networkId: String, pendingActions: ImmutableList?): Boolean { From 08023dffac3f89afc9450ec558368dbaec7f6272 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Oct 2024 13:47:10 +0500 Subject: [PATCH 21/41] Updated on 2026-08-14 --- .../model/OnboardingManageTokensModel.kt | 15 ++++-- .../utils/list/ManageTokensListManager.kt | 1 + .../utils/list/ManageTokensUiManager.kt | 53 +++++++++++++------ 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index 4c3c730ef6..6b4e801849 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -61,9 +61,11 @@ internal class OnboardingManageTokensModel @Inject constructor( .onEach { status -> updatePaginationStatus(status) } .launchIn(modelScope) - manageTokensListManager.currenciesToAdd - .onEach(::handleNewAddedCurrencies) - .launchIn(modelScope) + combine( + manageTokensListManager.currenciesToAdd, + manageTokensListManager.currenciesToRemove, + ::handleChangedCurrencies, + ).launchIn(modelScope) observeSearchQueryChanges() @@ -192,8 +194,11 @@ internal class OnboardingManageTokensModel @Inject constructor( state.update { state -> state.copy(scrollToTop = consumedEvent()) } } - private suspend fun handleNewAddedCurrencies(currenciesToAdd: ChangedCurrencies) { - if (currenciesToAdd.isEmpty()) { + private suspend fun handleChangedCurrencies( + currenciesToAdd: ChangedCurrencies, + currenciesToRemove: ChangedCurrencies, + ) { + if (currenciesToAdd.isEmpty() && currenciesToRemove.isEmpty()) { state.update { state -> state.copy( actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(onClick = ::onLaterClick), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 551debff20..414fd1161a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -68,6 +68,7 @@ internal class ManageTokensListManager @Inject constructor( dispatchers = dispatchers, actions = this, scopeProvider = Provider { scope }, + sourceProvider = Provider { source }, clipboardManager = clipboardManager, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 167a7f88a7..8ed4841f87 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.dialog.CurrencyUnsupportedDialog @@ -29,11 +30,13 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +@Suppress("LongParameterList") internal class ManageTokensUiManager( private val state: MutableStateFlow, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, private val scopeProvider: Provider, + private val sourceProvider: Provider, private val actions: ManageTokensUiActions, private val clipboardManager: ClipboardManager, ) { @@ -41,6 +44,9 @@ internal class ManageTokensUiManager( private val scope: CoroutineScope get() = scopeProvider() + private val source: ManageTokensSource + get() = sourceProvider() + @OptIn(ExperimentalCoroutinesApi::class) val items: Flow> = state .mapLatest { state -> @@ -225,26 +231,39 @@ internal class ManageTokensUiManager( } else { actions.checkHasLinkedTokens(userWalletId, network) } + val canHideWithoutConfirming = source == ManageTokensSource.ONBOARDING - val message = ContentMessage { onDismiss -> - if (hasLinkedTokens) { - HasLinkedTokensWarning( - currency = currency, - network = network, - onDismiss = onDismiss, - ) - } else { - HideTokenWarning( - currency = currency, - onConfirm = { - onConfirm() - onDismiss() - }, - onDismiss = onDismiss, - ) - } + if (hasLinkedTokens) { + showLinkedTokensWarning(currency, network) + } else if (canHideWithoutConfirming) { + onConfirm() + } else { + showHideTokenWarning(currency, onConfirm) } + } + private fun showLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network) { + val message = ContentMessage { onDismiss -> + HasLinkedTokensWarning( + currency = currency, + network = network, + onDismiss = onDismiss, + ) + } + messageSender.send(message) + } + + private fun showHideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit) { + val message = ContentMessage { onDismiss -> + HideTokenWarning( + currency = currency, + onConfirm = { + onConfirm() + onDismiss() + }, + onDismiss = onDismiss, + ) + } messageSender.send(message) } From 321b026fb405dadf2efb322b0f4c49b62035cd88 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Oct 2024 12:18:09 +0400 Subject: [PATCH 22/41] Updated on 2026-08-14 --- .../com/tangem/utils/coroutines/JobHolder.kt | 14 ++- .../tokenlist/DefaultTokenListComponent.kt | 35 ++++++ .../tokenlist/TokenListComponent.kt | 28 +++++ .../tokenlist/di/TokenListComponentModule.kt | 18 +++ .../tokenlist/di/TokenListModelModule.kt | 20 ++++ .../tokenlist/entity/TokenListUMController.kt | 19 +--- .../UpdateSearchBarIntentsTransformer.kt | 13 --- .../UpdateTokenItemsTransformer.kt | 34 ++++-- .../tokenlist/model/TokenListModel.kt | 105 ++++++++++++++++++ .../presentation/tokenlist/ui/TokenList.kt | 64 +++++++++++ .../ui/preview/PreviewTokenListUMProvider.kt | 62 +++++++++++ .../tokenlist/utils/SearchTokensManager.kt | 29 +++++ 12 files changed, 404 insertions(+), 37 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListComponentModule.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListModelModule.kt delete 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/model/TokenListModel.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index d50f9f295c..6e5a872f02 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -1,6 +1,9 @@ package com.tangem.utils.coroutines +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * Job holder. It is automatically finished old job if new one is started @@ -26,4 +29,13 @@ class JobHolder { fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this) -suspend fun Job.saveInAndJoin(jobHolder: JobHolder) = saveIn(jobHolder).join() \ No newline at end of file +suspend fun Job.saveInAndJoin(jobHolder: JobHolder) = saveIn(jobHolder).join() + +fun CoroutineScope.withDebounce(jobHolder: JobHolder, timeMillis: Long = 800L, function: () -> Unit) { + launch { + delay(timeMillis = timeMillis) + + function() + } + .saveIn(jobHolder) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt new file mode 100644 index 0000000000..c74231c846 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.wallet.presentation.tokenlist + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.feature.wallet.presentation.tokenlist.model.TokenListModel +import com.tangem.feature.wallet.presentation.tokenlist.ui.TokenList +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultTokenListComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: TokenListComponent.Params, +) : TokenListComponent, AppComponentContext by context { + + private val model: TokenListModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + TokenList(state = state, modifier = modifier) + } + + @AssistedFactory + interface Factory : TokenListComponent.Factory { + override fun create(context: AppComponentContext, params: TokenListComponent.Params): DefaultTokenListComponent + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt new file mode 100644 index 0000000000..3c271c0894 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.wallet.presentation.tokenlist + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWalletId + +/** Token list component that present list of token for multi-currency wallet */ +@Stable +internal interface TokenListComponent : ComposableContentComponent { + + /** Component factory */ + interface Factory : ComponentFactory + + /** + * Params + * + * @property hasSearchBar flag that indicates if search bar should be shown + * @property userWalletId id of multi-currency wallet + * @property onTokenClick callback for token click + */ + data class Params( + val hasSearchBar: Boolean, + val userWalletId: UserWalletId, + val onTokenClick: (CryptoCurrencyStatus) -> Unit, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListComponentModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListComponentModule.kt new file mode 100644 index 0000000000..1e2672c2af --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.presentation.tokenlist.di + +import com.tangem.feature.wallet.presentation.tokenlist.DefaultTokenListComponent +import com.tangem.feature.wallet.presentation.tokenlist.TokenListComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface TokenListComponentModule { + + @Binds + @Singleton + fun bindTokenListComponentFactory(factory: DefaultTokenListComponent.Factory): TokenListComponent.Factory +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListModelModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListModelModule.kt new file mode 100644 index 0000000000..732340f818 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.wallet.presentation.tokenlist.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.feature.wallet.presentation.tokenlist.model.TokenListModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface TokenListModelModule { + + @Binds + @IntoMap + @ClassKey(TokenListModel::class) + fun provideTokenListModel(model: TokenListModel): Model +} \ 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 index 6e3f7a5c6a..04592b5ed0 100644 --- 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 @@ -1,8 +1,5 @@ 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 @@ -22,7 +19,7 @@ internal class TokenListUMController @Inject constructor() { private val _state: MutableStateFlow = MutableStateFlow( value = TokenListUM( - items = persistentListOf(createInitialSearchBar()), + items = persistentListOf(), isBalanceHidden = false, ), ) @@ -38,17 +35,7 @@ internal class TokenListUMController @Inject constructor() { } /** 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 = {}, - ), - ) + fun getSearchBar(): TokensListItemState.SearchBar? { + return _state.value.getSearchBar() } } \ 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 deleted file mode 100644 index de58815cd1..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarIntentsTransformer.kt +++ /dev/null @@ -1,13 +0,0 @@ -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/UpdateTokenItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt index 89d171d3f9..ed4dcf5c9e 100644 --- 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 @@ -1,28 +1,48 @@ 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.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.wallet.impl.R 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 tokenItemStateConverter: TokenItemStateConverter, private val statuses: List, private val isBalanceHidden: Boolean, + private val hasSearchBar: Boolean, + private val onQueryChange: (String) -> Unit, + private val onActiveChange: (Boolean) -> Unit, ) : TokenListUMTransformer { - private val converter = TokenItemStateConverter(appCurrency = appCurrency, onItemClick = onItemClick) - override fun transform(prevState: TokenListUM): TokenListUM { - val items = converter.convertList(input = statuses).map(TokensListItemState::Token) + val items = tokenItemStateConverter.convertList(input = statuses).map(TokensListItemState::Token) + + val searchBarItem = if (hasSearchBar) { + prevState.getSearchBar() ?: createSearchBarItem() + } else { + null + } return prevState.copy( - items = (listOfNotNull(prevState.getSearchBar()) + items).toImmutableList(), + items = (listOfNotNull(searchBarItem) + items).toImmutableList(), isBalanceHidden = isBalanceHidden, ) } + + private fun createSearchBarItem(): TokensListItemState.SearchBar { + return TokensListItemState.SearchBar( + searchBarUM = SearchBarUM( + placeholderText = resourceReference(id = R.string.common_search), + query = "", + onQueryChange = onQueryChange, + isActive = false, + onActiveChange = onActiveChange, + ), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/model/TokenListModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/model/TokenListModel.kt new file mode 100644 index 0000000000..fb13ef8d9d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/model/TokenListModel.kt @@ -0,0 +1,105 @@ +package com.tangem.feature.wallet.presentation.tokenlist.model + +import arrow.core.getOrElse +import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.core.utils.getOrElse +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.tokenlist.TokenListComponent +import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM +import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUMController +import com.tangem.feature.wallet.presentation.tokenlist.entity.transformer.UpdateSearchBarActiveStateTransformer +import com.tangem.feature.wallet.presentation.tokenlist.entity.transformer.UpdateSearchQueryTransformer +import com.tangem.feature.wallet.presentation.tokenlist.entity.transformer.UpdateTokenItemsTransformer +import com.tangem.feature.wallet.presentation.tokenlist.utils.SearchTokensManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class TokenListModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val tokenListUMController: TokenListUMController, + private val searchTokensManager: SearchTokensManager, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getTokenListUseCase: GetTokenListUseCase, +) : Model() { + + val state: StateFlow = tokenListUMController.state + + private val params: TokenListComponent.Params = paramsContainer.require() + + init { + subscribeOnUpdateState() + } + + private fun subscribeOnUpdateState() { + combine( + flow = getTokenListUseCase.launch(userWalletId = params.userWalletId).distinctUntilChanged(), + flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(), + flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(), + flow4 = searchTokensManager.query, + ) { maybeTokenList, appCurrency, isBalanceHidden, query -> + val currencies = maybeTokenList.getOrElse( + ifLoading = { it ?: TokenList.Empty }, + ifError = { TokenList.Empty }, + ) + .flattenCurrencies() + + val filterTokenList = currencies + .filterByQuery(query = query) + .filterByAvailability() + + UpdateTokenItemsTransformer( + tokenItemStateConverter = TokenItemStateConverter( + appCurrency = appCurrency, + onItemClick = params.onTokenClick, + ), + statuses = filterTokenList, + isBalanceHidden = isBalanceHidden, + hasSearchBar = params.hasSearchBar && currencies.isNotEmpty(), + onQueryChange = ::onSearchQueryChange, + onActiveChange = ::onSearchBarActiveChange, + ) + } + .onEach(tokenListUMController::update) + .flowOn(dispatchers.main) + .launchIn(modelScope) + } + + private fun onSearchQueryChange(newQuery: String) { + val searchBar = tokenListUMController.getSearchBar() + if (searchBar?.searchBarUM?.query == newQuery) return + + modelScope.launch { + tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery)) + + searchTokensManager.update(newQuery) + } + } + + private fun onSearchBarActiveChange(isActive: Boolean) { + tokenListUMController.update(transformer = UpdateSearchBarActiveStateTransformer(isActive)) + } + + private fun List.filterByQuery(query: String): List { + return filter { + it.currency.name.contains(other = query, ignoreCase = true) || + it.currency.symbol.contains(other = query, ignoreCase = true) + } + } + + // TODO: [REDACTED_JIRA] + private fun List.filterByAvailability(): List { + return this + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt new file mode 100644 index 0000000000..e780c03e44 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.tokenlist.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM +import com.tangem.feature.wallet.presentation.tokenlist.ui.preview.PreviewTokenListUMProvider +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.MultiCurrencyContentItem + +/** + * Token list + * + * @param state state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier) { + itemsIndexed( + items = state.items, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { index, item -> + MultiCurrencyContentItem( + state = item, + isBalanceHidden = state.isBalanceHidden, + modifier = Modifier + .animateItem() + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = state.items.lastIndex, + addDefaultPadding = false, + ), + ) + }, + ) + } +} + +@Preview +@Composable +private fun Preview_TokenList(@PreviewParameter(PreviewTokenListUMProvider::class) state: TokenListUM) { + TangemThemePreview { + TokenList( + state = state, + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.secondary) + .padding(16.dp), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt new file mode 100644 index 0000000000..4a0b5ad769 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.tokenlist.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState +import kotlinx.collections.immutable.toImmutableList + +internal class PreviewTokenListUMProvider : PreviewParameterProvider { + + override val values: Sequence = sequenceOf( + createTokensList(hasSearchBar = false, createDefaultTokenItem()), + createTokensList(hasSearchBar = true, createDefaultTokenItem()), + ) + + private fun createTokensList(hasSearchBar: Boolean, vararg items: TokensListItemState): TokenListUM { + return TokenListUM( + items = buildList { + if (hasSearchBar) { + TokensListItemState.SearchBar( + searchBarUM = SearchBarUM( + placeholderText = resourceReference(id = R.string.common_search), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ), + ) + .let { add(element = it) } + } + + addAll(items) + } + .toImmutableList(), + isBalanceHidden = false, + ) + } + + private fun createDefaultTokenItem(): TokensListItemState.Token { + return TokensListItemState.Token( + state = TokenItemState.Content( + id = "1", + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "34 496,75 \$", + priceChangePercent = "0,43 %", + type = PriceChangeType.DOWN, + ), + onItemClick = {}, + onItemLongClick = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt new file mode 100644 index 0000000000..98258cca19 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.tokenlist.utils + +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.withDebounce +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +/** + * Search tokens manager + * +[REDACTED_AUTHOR] + */ +internal class SearchTokensManager @Inject constructor() { + + val query: Flow + get() = _query + + private val _query = MutableStateFlow(value = "") + + private val jobHolder = JobHolder() + + suspend fun update(value: String) { + coroutineScope { + withDebounce(jobHolder) { _query.value = value } + } + } +} \ No newline at end of file From 5ae666646f09363df15e326ef5c3fd529296e72d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Oct 2024 14:18:36 +0500 Subject: [PATCH 23/41] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 9 + .../tangem/tap/routing/utils/ChildFactory.kt | 9 + .../com/tangem/common/routing/AppRoute.kt | 3 + .../entity/item/CurrencyNetworkUM.kt | 2 - .../ui/CustomTokenSelectorContent.kt | 12 ++ .../onramp/component/OnrampComponent.kt | 4 +- features/onramp/impl/build.gradle.kts | 9 +- .../component/ConfirmResidencyComponent.kt | 16 ++ .../impl/DefaultConfirmResidencyComponent.kt | 55 ++++++ .../component/impl/DefaultOnrampComponent.kt | 64 +++++++ .../PreviewConfirmResidencyComponent.kt | 46 +++++ .../onramp/di/OnrampComponentModule.kt | 26 +++ ...eatureModule.kt => OnrampFeatureModule.kt} | 2 +- .../features/onramp/di/OnrampModelModule.kt | 26 +++ .../onramp/entity/ConfirmResidencyUM.kt | 16 ++ .../onramp/entity/OnrampBottomSheetConfig.kt | 10 ++ .../onramp/model/ConfirmResidencyModel.kt | 53 ++++++ .../features/onramp/model/OnrampModel.kt | 38 ++++ .../onramp/ui/ResidenceBottomSheet.kt | 165 ++++++++++++++++++ 19 files changed, 559 insertions(+), 6 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/ConfirmResidencyComponent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultConfirmResidencyComponent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultOnrampComponent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/preview/PreviewConfirmResidencyComponent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampComponentModule.kt rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/{FeatureModule.kt => OnrampFeatureModule.kt} (94%) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampModelModule.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampBottomSheetConfig.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/ConfirmResidencyModel.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/OnrampModel.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/ui/ResidenceBottomSheet.kt diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 2f26b2a39a..a21351a6b9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -54,6 +54,15 @@ object TradeCryptoMiddleware { } private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { + val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled + if (isOnrampEnabled) proceedWithOnramp() else proceedWithLegacyBuyAction(state, action) + } + + private fun proceedWithOnramp() { + store.dispatchNavigationAction { push(AppRoute.Onramp) } + } + + private fun proceedWithLegacyBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { val networkAddress = action.cryptoCurrencyStatus.value.networkAddress ?.defaultAddress ?.let(NetworkAddress.Address::value) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 04e0e5f804..cca2474fa1 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -11,6 +11,7 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.features.onramp.component.OnrampComponent import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -46,6 +47,7 @@ internal class ChildFactory @Inject constructor( private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val manageTokensComponentFactory: ManageTokensComponent.Factory, private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, + private val onrampComponentFactory: OnrampComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, @@ -186,6 +188,13 @@ internal class ChildFactory @Inject constructor( componentFactory = marketsTokenDetailsComponentFactory, ) } + is AppRoute.Onramp -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = OnrampComponent.Params(), + componentFactory = onrampComponentFactory, + ) + } } } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 398cb634a8..94e8f8487c 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -288,4 +288,7 @@ sealed class AppRoute(val path: String) : Route { val source: String, ) } + + @Serializable + data object Onramp : AppRoute(path = "/onramp") } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt index bdec914faa..fbe35f92e4 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt @@ -14,6 +14,4 @@ internal data class CurrencyNetworkUM( ) : SelectableItemUM { override val id: String = network.id.value - - data class LongTapConfig(val contractAddress: String, val onLongTap: () -> Unit) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 21f3daf5e1..2fed1003f0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -285,6 +285,18 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : onDerivationPathSelected = {}, ), ), + PreviewCustomTokenSelectorComponent( + params = CustomTokenSelectorComponent.Params.NetworkSelector( + userWalletId = UserWalletId(stringValue = "321"), + selectedNetwork = SelectedNetwork( + id = Network.ID(value = "0"), + name = "Ethereum", + derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), + canHandleTokens = true, + ), + onNetworkSelected = {}, + ), + ), ) } // endregion Preview \ 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 index b5cd6141fb..16ac6b24f7 100644 --- 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 @@ -5,5 +5,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent interface OnrampComponent : ComposableContentComponent { - interface Factory : ComponentFactory + class Params + + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 37ef2d093d..94b9c2785a 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) alias(deps.plugins.kotlin.kapt) alias(deps.plugins.hilt.android) id("configuration") @@ -23,11 +24,11 @@ dependencies { implementation(deps.hilt.android) kapt(deps.hilt.kapt) - /* AndroidX */ + /** AndroidX */ implementation(deps.androidx.activity.compose) implementation(deps.lifecycle.compose) - /* Compose */ + /** Compose */ implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.accompanist.systemUiController) @@ -35,4 +36,8 @@ dependencies { implementation(deps.compose.material3) implementation(deps.compose.shimmer) implementation(deps.compose.coil) + implementation(deps.compose.material) + + /** Other */ + implementation(deps.decompose.ext.compose) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/ConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/ConfirmResidencyComponent.kt new file mode 100644 index 0000000000..74742aa575 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/ConfirmResidencyComponent.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onramp.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent + +internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent { + + data class Params( + val countryName: String, + val isOnrampSupported: Boolean, + val countryFlagUrl: String, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultConfirmResidencyComponent.kt new file mode 100644 index 0000000000..177e697eb8 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultConfirmResidencyComponent.kt @@ -0,0 +1,55 @@ +package com.tangem.features.onramp.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.model.ConfirmResidencyModel +import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheet +import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheetContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultConfirmResidencyComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: ConfirmResidencyComponent.Params, +) : ConfirmResidencyComponent, AppComponentContext by context { + + private val model: ConfirmResidencyModel = getOrCreateModel(params) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.state.collectAsStateWithLifecycle() + val bottomSheetConfig = remember { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + ConfirmResidencyBottomSheet( + config = bottomSheetConfig, + content = { modifier -> + ConfirmResidencyBottomSheetContent(model = state, modifier = modifier) + }, + ) + } + + @AssistedFactory + interface Factory : ConfirmResidencyComponent.Factory { + override fun create( + context: AppComponentContext, + params: ConfirmResidencyComponent.Params, + ): DefaultConfirmResidencyComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultOnrampComponent.kt new file mode 100644 index 0000000000..7ec0fb692a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultOnrampComponent.kt @@ -0,0 +1,64 @@ +package com.tangem.features.onramp.component.impl + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.onramp.component.OnrampComponent +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.entity.OnrampBottomSheetConfig +import com.tangem.features.onramp.model.OnrampModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnrampComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: OnrampComponent.Params, + private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, +) : OnrampComponent, AppComponentContext by context { + + private val model: OnrampModel = getOrCreateModel(params) + private val confirmResidencyBottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val bottomSheet by confirmResidencyBottomSheetSlot.subscribeAsState() + BackHandler(onBack = router::pop) + + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: OnrampBottomSheetConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + OnrampBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create( + context = childByContext(componentContext), + params = ConfirmResidencyComponent.Params( + countryName = "United States", + isOnrampSupported = true, + countryFlagUrl = "https://hatscripts.github.io/circle-flags/flags/us.svg", + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + } + + @AssistedFactory + interface Factory : OnrampComponent.Factory { + override fun create(context: AppComponentContext, params: OnrampComponent.Params): DefaultOnrampComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/preview/PreviewConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/preview/PreviewConfirmResidencyComponent.kt new file mode 100644 index 0000000000..b5e167f157 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/preview/PreviewConfirmResidencyComponent.kt @@ -0,0 +1,46 @@ +package com.tangem.features.onramp.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.entity.ConfirmResidencyUM +import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheet +import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheetContent +import kotlinx.coroutines.flow.MutableStateFlow + +internal class PreviewConfirmResidencyComponent( + initialState: ConfirmResidencyUM = ConfirmResidencyUM( + country = "United States", + countryFlagUrl = "https://hatscripts.github.io/circle-flags/flags/us.svg", + isCountrySupported = true, + primaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig(onClick = {}, text = stringReference("Confirm")), + secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig(onClick = {}, text = stringReference("Change")), + ), +) : ConfirmResidencyComponent { + + private val previewState: MutableStateFlow = MutableStateFlow(initialState) + + override fun dismiss() { + /* no-op */ + } + + @Composable + override fun BottomSheet() { + val state by previewState.collectAsStateWithLifecycle() + val bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + ConfirmResidencyBottomSheet( + config = bottomSheetConfig, + content = { modifier -> + ConfirmResidencyBottomSheetContent(model = state, modifier = modifier) + }, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampComponentModule.kt new file mode 100644 index 0000000000..6f27d391db --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampComponentModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.onramp.di + +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.component.OnrampComponent +import com.tangem.features.onramp.component.impl.DefaultConfirmResidencyComponent +import com.tangem.features.onramp.component.impl.DefaultOnrampComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampComponentModule { + + @Binds + @Singleton + fun bindOnrampComponentFactory(factory: DefaultOnrampComponent.Factory): OnrampComponent.Factory + + @Binds + @Singleton + fun bindConfirmResidencyComponentFactory( + factory: DefaultConfirmResidencyComponent.Factory, + ): ConfirmResidencyComponent.Factory +} \ 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/OnrampFeatureModule.kt similarity index 94% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/FeatureModule.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampFeatureModule.kt index 2c39321176..cac4df29b9 100644 --- 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/OnrampFeatureModule.kt @@ -11,7 +11,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object FeatureModule { +internal object OnrampFeatureModule { @Provides @Singleton diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampModelModule.kt new file mode 100644 index 0000000000..41f90f6d5a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampModelModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.onramp.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.model.OnrampModel +import com.tangem.features.onramp.model.ConfirmResidencyModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampModel::class) + fun provideOnrampModel(model: OnrampModel): Model + + @Binds + @IntoMap + @ClassKey(ConfirmResidencyModel::class) + fun provideConfirmResidencyModel(model: ConfirmResidencyModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyUM.kt new file mode 100644 index 0000000000..106df7e1bc --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyUM.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onramp.entity + +import com.tangem.core.ui.extensions.TextReference + +data class ConfirmResidencyUM( + val country: String, + val countryFlagUrl: String, + val isCountrySupported: Boolean, + val primaryButtonConfig: ActionButtonConfig, + val secondaryButtonConfig: ActionButtonConfig, +) { + data class ActionButtonConfig( + val onClick: () -> Unit, + val text: TextReference, + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampBottomSheetConfig.kt new file mode 100644 index 0000000000..81289b4cdf --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampBottomSheetConfig.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.entity + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class OnrampBottomSheetConfig { + + @Serializable + data object ConfirmResidency : OnrampBottomSheetConfig() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/ConfirmResidencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/ConfirmResidencyModel.kt new file mode 100644 index 0000000000..9457285bd3 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/ConfirmResidencyModel.kt @@ -0,0 +1,53 @@ +package com.tangem.features.onramp.model + +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.entity.ConfirmResidencyUM +import com.tangem.features.onramp.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@ComponentScoped +internal class ConfirmResidencyModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: ConfirmResidencyComponent.Params = paramsContainer.require() + val state: MutableStateFlow = MutableStateFlow( + ConfirmResidencyUM( + country = params.countryName, + countryFlagUrl = params.countryFlagUrl, + isCountrySupported = params.isOnrampSupported, + primaryButtonConfig = getPrimaryButtonConfig(), + secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig( + onClick = ::onChangeClick, + text = resourceReference(R.string.common_change), + ), + ), + ) + + private fun getPrimaryButtonConfig() = if (params.isOnrampSupported) { + ConfirmResidencyUM.ActionButtonConfig( + onClick = params.onDismiss, + text = resourceReference(R.string.common_confirm), + ) + } else { + ConfirmResidencyUM.ActionButtonConfig( + onClick = ::onCloseClick, + text = resourceReference(R.string.common_close), + ) + } + + @Suppress("EmptyFunctionBlock") + private fun onCloseClick() { + } + + private fun onChangeClick() { + // TODO: [REDACTED_JIRA] + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/OnrampModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/OnrampModel.kt new file mode 100644 index 0000000000..d1f35e1fca --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/OnrampModel.kt @@ -0,0 +1,38 @@ +package com.tangem.features.onramp.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.features.onramp.component.OnrampComponent +import com.tangem.features.onramp.entity.OnrampBottomSheetConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("MagicNumber") +@ComponentScoped +internal class OnrampModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + paramsContainer: ParamsContainer, +) : Model() { + + @Suppress("UnusedPrivateMember") + private val params: OnrampComponent.Params = paramsContainer.require() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + init { + modelScope.launch { + delay(1500) + bottomSheetNavigation.activate(OnrampBottomSheetConfig.ConfirmResidency) + } + } + + fun pop() { + router.pop() + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/ui/ResidenceBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/ui/ResidenceBottomSheet.kt new file mode 100644 index 0000000000..a950da23ab --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/ui/ResidenceBottomSheet.kt @@ -0,0 +1,165 @@ +package com.tangem.features.onramp.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.component.preview.PreviewConfirmResidencyComponent +import com.tangem.features.onramp.entity.ConfirmResidencyUM +import com.tangem.features.onramp.impl.R + +@Composable +internal fun ConfirmResidencyBottomSheet(config: TangemBottomSheetConfig, content: @Composable (Modifier) -> Unit) { + TangemBottomSheet( + config = config, + addBottomInsets = true, + containerColor = TangemTheme.colors.background.tertiary, + title = { _ -> Title() }, + content = { + val contentModifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(bottom = TangemTheme.dimens.spacing16) + .fillMaxWidth() + .wrapContentHeight() + + content(contentModifier) + }, + ) +} + +@Composable +private fun Title(modifier: Modifier = Modifier) { + TangemTopAppBar( + modifier = modifier, + title = resourceReference(R.string.onramp_residency_bottomsheet_title), + titleAlignment = Alignment.CenterHorizontally, + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ) +} + +@Composable +internal fun ConfirmResidencyBottomSheetContent(model: ConfirmResidencyUM, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + CountryContent( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing24), + name = model.country, + flagUrl = model.countryFlagUrl, + isCountrySupported = model.isCountrySupported, + ) + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = model.secondaryButtonConfig.text.resolveReference(), + onClick = model.secondaryButtonConfig.onClick, + ) + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing12), + text = model.primaryButtonConfig.text.resolveReference(), + onClick = model.primaryButtonConfig.onClick, + ) + } +} + +@Composable +private fun CountryContent(name: String, flagUrl: String, isCountrySupported: Boolean, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val flagSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() } + SubcomposeAsyncImage( + modifier = Modifier.size(TangemTheme.dimens.size36), + model = ImageRequest.Builder(LocalContext.current) + .size(size = flagSize) + .data(flagUrl) + .memoryCacheKey(flagUrl + flagSize) + .crossfade(true) + .allowHardware(false) + .build(), + loading = { CircleShimmer() }, + contentDescription = null, + ) + + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + text = name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), + text = stringResource( + id = if (isCountrySupported) { + R.string.onramp_residency_bottomsheet_country_subtitle + } else { + R.string.onramp_residency_bottomsheet_country_not_supported + }, + ), + color = if (isCountrySupported) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.warning, + style = TangemTheme.typography.body2, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_ConfirmResidencyBottomSheet( + @PreviewParameter(ConfirmResidencyComponentPreviewProvider::class) component: ConfirmResidencyComponent, +) { + TangemThemePreview { + component.BottomSheet() + } +} + +private class ConfirmResidencyComponentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PreviewConfirmResidencyComponent(), + PreviewConfirmResidencyComponent( + initialState = ConfirmResidencyUM( + country = "Russia", + countryFlagUrl = "https://hatscripts.github.io/circle-flags/flags/ru.svg", + isCountrySupported = false, + primaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig( + onClick = {}, + text = stringReference("Close"), + ), + secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig( + onClick = {}, + text = stringReference("Change"), + ), + ), + ), + ) +} \ No newline at end of file From 7834cc5a8736e5f65fc291efdfacb3bbcbba37a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Oct 2024 12:16:14 +0400 Subject: [PATCH 24/41] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 4 +-- .../java/com/tangem/tap/TangemApplication.kt | 8 +++--- .../tap/domain/twins/TwinCardsManager.kt | 26 ++++++------------ .../tap/proxy/redux/DaggerGraphState.kt | 4 +-- .../di/local/config/ConfigModule.kt | 13 ++++++++- .../issuers/DefaultIssuersConfigStorage.kt | 27 +++++++++++++++++++ .../config/issuers/IssuersConfigStorage.kt | 14 ++++++++++ .../local/config/issuers/models/Issuer.kt | 10 +++++++ 8 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/DefaultIssuersConfigStorage.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/IssuersConfigStorage.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/models/Issuer.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index a425711dd5..e9db6824f0 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -8,9 +8,9 @@ import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener -import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase @@ -51,7 +51,7 @@ interface ApplicationEntryPoint { fun getAppStateHolder(): AppStateHolder - fun getAssetLoader(): AssetLoader + fun getIssuersConfigStorage(): IssuersConfigStorage fun getFeatureTogglesManager(): FeatureTogglesManager diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 530337b2cd..36f91a010b 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -20,10 +20,10 @@ import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.createNetworkLoggingInterceptor -import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase @@ -86,8 +86,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val environmentConfigStorage: EnvironmentConfigStorage get() = entryPoint.getEnvironmentConfigStorage() - private val assetLoader: AssetLoader - get() = entryPoint.getAssetLoader() + private val issuersConfigStorage: IssuersConfigStorage + get() = entryPoint.getIssuersConfigStorage() private val featureTogglesManager: FeatureTogglesManager get() = entryPoint.getFeatureTogglesManager() @@ -269,7 +269,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, getCardInfoUseCase = getCardInfoUseCase, - assetLoader = assetLoader, + issuersConfigStorage = issuersConfigStorage, urlOpener = urlOpener, shareManager = shareManager, appRouter = appRouter, diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index 7f242bb845..beaf919a98 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -13,17 +13,19 @@ import com.tangem.tap.common.extensions.inject import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.tap.tangemSdkManager -import kotlinx.coroutines.flow.MutableStateFlow class TwinCardsManager(card: CardDTO) { + + private val issuersConfigStorage by lazy(mode = LazyThreadSafetyMode.NONE) { + store.inject(DaggerGraphState::issuersConfigStorage) + } + private val firstCardId: String = card.cardId private val publicKey: String = card.issuer.publicKey.toHexString() private var currentCardPublicKey: String? = null private var secondCardPublicKey: String? = null - private val issuerKeyPairFlow = MutableStateFlow(value = null) - suspend fun createFirstWallet(message: Message): CompletionResult { val response = tangemSdkManager.createFirstTwinWallet(cardId = firstCardId, initialMessage = message) @@ -70,23 +72,11 @@ class TwinCardsManager(card: CardDTO) { } private suspend fun getIssuerKeys(): KeyPair { - issuerKeyPairFlow.value?.let { return it } - - val assetLoader = store.inject(DaggerGraphState::assetLoader) - val issuer = assetLoader.loadList(fileName = ISSUERS_FILE_NAME) - .first { it.publicKey == publicKey } + val issuer = issuersConfigStorage.getConfig().first { it.publicKey == publicKey } return KeyPair( publicKey = issuer.publicKey.hexToBytes(), privateKey = issuer.privateKey.hexToBytes(), - ).also { - issuerKeyPairFlow.value = it - } + ) } - - private companion object { - const val ISSUERS_FILE_NAME = "tangem-app-config/issuers" - } -} - -private class Issuer(val privateKey: String, val publicKey: String) \ No newline at end of file +} \ No newline at end of file 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 8d6b257f74..0a0c716fc6 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 @@ -7,8 +7,8 @@ import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.email.EmailSender import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener -import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository @@ -79,7 +79,7 @@ data class DaggerGraphState( val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null, val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase? = null, val getCardInfoUseCase: GetCardInfoUseCase? = null, - val assetLoader: AssetLoader? = null, + val issuersConfigStorage: IssuersConfigStorage? = null, val stakingRouter: StakingRouter? = null, val urlOpener: UrlOpener? = null, val shareManager: ShareManager? = null, diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt index 109e138c0d..0ed4457201 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt @@ -4,6 +4,8 @@ import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.config.environment.DefaultEnvironmentConfigStorage import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage import com.tangem.datasource.local.config.providers.DefaultBlockchainProvidersStorage import com.tangem.datasource.local.config.testnet.DefaultTestnetTokensStorage @@ -30,7 +32,7 @@ internal object ConfigModule { @Provides @Singleton - fun providesTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage { + fun provideTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage { return DefaultTestnetTokensStorage(assetLoader) } @@ -42,4 +44,13 @@ internal object ConfigModule { runtimeStateStore = RuntimeStateStore(defaultValue = emptyMap()), ) } + + @Provides + @Singleton + fun provideIssuersConfigStorage(assetLoader: AssetLoader): IssuersConfigStorage { + return DefaultIssuersConfigStorage( + assetLoader = assetLoader, + runtimeStateStore = RuntimeStateStore(defaultValue = emptyList()), + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/DefaultIssuersConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/DefaultIssuersConfigStorage.kt new file mode 100644 index 0000000000..0d9de3ea5c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/DefaultIssuersConfigStorage.kt @@ -0,0 +1,27 @@ +package com.tangem.datasource.local.config.issuers + +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.local.config.issuers.models.Issuer +import com.tangem.datasource.local.datastore.RuntimeStateStore + +internal class DefaultIssuersConfigStorage( + private val assetLoader: AssetLoader, + private val runtimeStateStore: RuntimeStateStore>, +) : IssuersConfigStorage { + + override suspend fun getConfig(): List { + val cachedData = runtimeStateStore.get().value + + if (cachedData.isNotEmpty()) return cachedData + + val issuers = assetLoader.loadList(fileName = ISSUERS_FILE_NAME) + + runtimeStateStore.store(value = issuers) + + return issuers + } + + private companion object { + const val ISSUERS_FILE_NAME = "tangem-app-config/issuers" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/IssuersConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/IssuersConfigStorage.kt new file mode 100644 index 0000000000..ea2ac0708c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/IssuersConfigStorage.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.local.config.issuers + +import com.tangem.datasource.local.config.issuers.models.Issuer + +/** + * Storage for list of Twins [Issuer] + * +[REDACTED_AUTHOR] + */ +interface IssuersConfigStorage { + + /** Get config */ + suspend fun getConfig(): List +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/models/Issuer.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/models/Issuer.kt new file mode 100644 index 0000000000..a26754c4ec --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/models/Issuer.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.config.issuers.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Issuer( + @Json(name = "privateKey") val privateKey: String, + @Json(name = "publicKey") val publicKey: String, +) \ No newline at end of file From 51c6619a551a2a40c6711cf9d6676419e7676ef1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Oct 2024 14:03:55 +0400 Subject: [PATCH 25/41] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 12 -------- .../main/java/com/tangem/tap/MainActivity.kt | 12 -------- .../java/com/tangem/tap/TangemApplication.kt | 28 ++----------------- .../tap/common/redux/global/GlobalAction.kt | 3 -- .../common/redux/global/GlobalMiddleware.kt | 3 +- .../tap/common/redux/global/GlobalReducer.kt | 3 -- .../tap/common/redux/global/GlobalState.kt | 2 -- .../tap/proxy/redux/DaggerGraphAction.kt | 16 ----------- .../tap/proxy/redux/DaggerGraphReducer.kt | 8 ------ .../tap/proxy/redux/DaggerGraphState.kt | 24 ++-------------- 10 files changed, 5 insertions(+), 106 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index e9db6824f0..923313f69f 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -19,14 +19,11 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.GetCardInfoUseCase -import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository @@ -34,7 +31,6 @@ 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 import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder import dagger.hilt.EntryPoint @@ -61,18 +57,12 @@ interface ApplicationEntryPoint { fun getWalletConnect2Repository(): WalletConnect2Repository - fun getWalletConnectSessionsRepository(): WalletConnectSessionsRepository - fun getScanCardProcessor(): ScanCardProcessor fun getAppCurrencyRepository(): AppCurrencyRepository fun getWalletManagersFacade(): WalletManagersFacade - fun getNetworksRepository(): NetworksRepository - - fun getCurrenciesRepository(): CurrenciesRepository - fun getAppThemeModeRepository(): AppThemeModeRepository fun getBalanceHidingRepository(): BalanceHidingRepository @@ -103,8 +93,6 @@ interface ApplicationEntryPoint { fun getSendFeedbackEmailUseCase(): SendFeedbackEmailUseCase - fun getSaveBlockchainErrorUseCase(): SaveBlockchainErrorUseCase - fun getGetCardInfoUseCase(): GetCardInfoUseCase fun getUrlOpener(): UrlOpener diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 169f30bc6c..3109a05bcd 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -56,7 +56,6 @@ import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRou import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter -import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.operations.backup.BackupService @@ -113,9 +112,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lateinit var appStateHolder: AppStateHolder /** Router for opening tester menu */ - @Inject - lateinit var testerRouter: TesterRouter - @Inject lateinit var cardSdkOwner: CardSdkOwner @@ -309,17 +305,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac store.dispatch( DaggerGraphAction.SetActivityDependencies( - testerRouter = testerRouter, scanCardUseCase = scanCardUseCase, - walletRouter = walletRouter, walletConnectInteractor = walletConnectInteractor, - tokenDetailsRouter = tokenDetailsRouter, cardSdkConfigRepository = cardSdkConfigRepository, - sendRouter = sendRouter, - qrScanningRouter = qrScanningRouter, - emailSender = emailSender, - stakingRouter = stakingRouter, - pushNotificationsRouter = pushNotificationsRouter, ), ) } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 36f91a010b..c30a00c30d 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -33,14 +33,11 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.LogConfig import com.tangem.domain.feedback.GetCardInfoUseCase -import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository @@ -54,10 +51,8 @@ import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.tasks.product.DerivationsFinder -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState @@ -101,9 +96,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val walletConnect2Repository: WalletConnect2Repository get() = entryPoint.getWalletConnect2Repository() - private val walletConnectSessionsRepository: WalletConnectSessionsRepository - get() = entryPoint.getWalletConnectSessionsRepository() - private val scanCardProcessor: ScanCardProcessor get() = entryPoint.getScanCardProcessor() @@ -113,12 +105,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val walletManagersFacade: WalletManagersFacade get() = entryPoint.getWalletManagersFacade() - private val networksRepository: NetworksRepository - get() = entryPoint.getNetworksRepository() - - private val currenciesRepository: CurrenciesRepository - get() = entryPoint.getCurrenciesRepository() - private val appThemeModeRepository: AppThemeModeRepository get() = entryPoint.getAppThemeModeRepository() @@ -164,9 +150,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase get() = entryPoint.getSendFeedbackEmailUseCase() - private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase - get() = entryPoint.getSaveBlockchainErrorUseCase() - private val getCardInfoUseCase: GetCardInfoUseCase get() = entryPoint.getGetCardInfoUseCase() @@ -215,10 +198,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { runBlocking { featureTogglesManager.init() - val config = environmentConfigStorage.initialize() - store.dispatch(GlobalAction.SetConfigManager(environmentConfigStorage)) - - initWithConfigDependency(environmentConfig = config) + initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) } loadNativeLibraries() @@ -248,13 +228,10 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { networkConnectionManager = networkConnectionManager, cardScanningFeatureToggles = cardScanningFeatureToggles, walletConnectRepository = walletConnect2Repository, - walletConnectSessionsRepository = walletConnectSessionsRepository, scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, walletManagersFacade = walletManagersFacade, appStateHolder = appStateHolder, - networksRepository = networksRepository, - currenciesRepository = currenciesRepository, appThemeModeRepository = appThemeModeRepository, balanceHidingRepository = balanceHidingRepository, walletsRepository = walletsRepository, @@ -263,10 +240,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, generateWalletNameUseCase = generateWalletNameUseCase, cardRepository = cardRepository, - tangemSdkLogger = tangemSdkLogger, settingsRepository = settingsRepository, blockchainSDKFactory = blockchainSDKFactory, - saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, getCardInfoUseCase = getCardInfoUseCase, issuersConfigStorage = issuersConfigStorage, @@ -277,6 +252,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { homeFeatureToggles = homeFeatureToggles, getUserCountryUseCase = getUserCountryUseCase, onrampFeatureToggles = onrampFeatureToggles, + environmentConfigStorage = environmentConfigStorage, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index a9525dc46c..740f5bd460 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -2,7 +2,6 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog @@ -74,8 +73,6 @@ sealed class GlobalAction : Action { data class IsSignWithRing(val isSignWithRing: Boolean) : GlobalAction() - data class SetConfigManager(val environmentConfigStorage: EnvironmentConfigStorage) : GlobalAction() - object ExchangeManager : GlobalAction() { object Init : GlobalAction() { data class Success( diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index a0c16d69f6..636b4f40b1 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -56,8 +56,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) { restoreAppCurrency() } is GlobalAction.ExchangeManager.Init -> { - val appStateSafe = appState() ?: return - val config = appStateSafe.globalState.environmentConfigStorage?.getConfigSync() ?: return + val config = store.inject(DaggerGraphState::environmentConfigStorage).getConfigSync() scope.launch { val scanResponseProvider: () -> ScanResponse? = { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 6884603331..d55f99d21b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -47,9 +47,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde is GlobalAction.RestoreAppCurrency.Success -> { globalState.copy(appCurrency = action.appCurrency) } - is GlobalAction.SetConfigManager -> { - globalState.copy(environmentConfigStorage = action.environmentConfigStorage) - } is GlobalAction.UpdateWalletSignedHashes -> { val card = globalState.scanResponse?.card ?: return globalState val wallet = card.wallets diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 0317eaaca6..b75d7370d8 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,6 +1,5 @@ package com.tangem.tap.common.redux.global -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse @@ -16,7 +15,6 @@ data class GlobalState( val onboardingState: OnboardingState = OnboardingState(), val cardVerifiedOnline: Boolean = false, val tapWalletManager: TapWalletManager = TapWalletManager(), - val environmentConfigStorage: EnvironmentConfigStorage? = null, val appCurrency: AppCurrency = AppCurrency.Default, val scanCardFailsCounter: Int = 0, val dialog: StateDialog? = null, diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index 0f3c13fbf9..86116d6849 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -1,31 +1,15 @@ package com.tangem.tap.proxy.redux -import com.tangem.core.navigation.email.EmailSender import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter -import com.tangem.features.send.api.navigation.SendRouter -import com.tangem.features.staking.api.navigation.StakingRouter -import com.tangem.features.tester.api.TesterRouter -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter -import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import org.rekotlin.Action sealed interface DaggerGraphAction : Action { data class SetActivityDependencies( - val testerRouter: TesterRouter, val scanCardUseCase: ScanCardUseCase, - val walletRouter: WalletRouter, val walletConnectInteractor: WalletConnectInteractor, - val tokenDetailsRouter: TokenDetailsRouter, val cardSdkConfigRepository: CardSdkConfigRepository, - val sendRouter: SendRouter, - val qrScanningRouter: QrScanningRouter, - val emailSender: EmailSender, - val stakingRouter: StakingRouter, - val pushNotificationsRouter: PushNotificationsRouter, ) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index 2a20bab5b6..76802dd20d 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -13,17 +13,9 @@ object DaggerGraphReducer { private fun internalReduce(action: DaggerGraphAction, state: AppState): DaggerGraphState { return when (action) { is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy( - testerRouter = action.testerRouter, scanCardUseCase = action.scanCardUseCase, - walletRouter = action.walletRouter, walletConnectInteractor = action.walletConnectInteractor, - tokenDetailsRouter = action.tokenDetailsRouter, cardSdkConfigRepository = action.cardSdkConfigRepository, - sendRouter = action.sendRouter, - qrScanningRouter = action.qrScanningRouter, - emailSender = action.emailSender, - stakingRouter = action.stakingRouter, - pushNotificationsRouter = action.pushNotificationsRouter, ) } } 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 0a0c716fc6..fe6a63998e 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 @@ -1,13 +1,12 @@ package com.tangem.tap.proxy.redux -import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.signer.TransactionSignerFactory import com.tangem.common.routing.AppRouter -import com.tangem.core.navigation.email.EmailSender import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -17,44 +16,31 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.GetCardInfoUseCase -import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository 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.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 -import com.tangem.features.tester.api.TesterRouter -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter -import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.StateType data class DaggerGraphState( - val testerRouter: TesterRouter? = null, val networkConnectionManager: NetworkConnectionManager? = null, val cardScanningFeatureToggles: CardScanningFeatureToggles? = null, val scanCardUseCase: ScanCardUseCase? = null, - val walletRouter: WalletRouter? = null, val walletConnectRepository: LegacyWalletConnectRepository? = null, - val walletConnectSessionsRepository: WalletConnectSessionsRepository? = null, val walletConnectInteractor: WalletConnectInteractor? = null, - val tokenDetailsRouter: TokenDetailsRouter? = null, val scanCardProcessor: ScanCardProcessor? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, @@ -63,20 +49,13 @@ data class DaggerGraphState( val appThemeModeRepository: AppThemeModeRepository? = null, val balanceHidingRepository: BalanceHidingRepository? = null, val walletsRepository: WalletsRepository? = null, - val networksRepository: NetworksRepository? = null, - val sendRouter: SendRouter? = null, - val qrScanningRouter: QrScanningRouter? = null, - val currenciesRepository: CurrenciesRepository? = null, val generalUserWalletsListManager: UserWalletsListManager? = null, val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null, val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, val generateWalletNameUseCase: GenerateWalletNameUseCase? = null, val cardRepository: CardRepository? = null, - val tangemSdkLogger: TangemSdkLogger? = null, val settingsRepository: SettingsRepository? = null, val blockchainSDKFactory: BlockchainSDKFactory? = null, - val emailSender: EmailSender? = null, - val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null, val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase? = null, val getCardInfoUseCase: GetCardInfoUseCase? = null, val issuersConfigStorage: IssuersConfigStorage? = null, @@ -89,4 +68,5 @@ data class DaggerGraphState( val homeFeatureToggles: HomeFeatureToggles? = null, val getUserCountryUseCase: GetUserCountryUseCase? = null, val onrampFeatureToggles: OnrampFeatureToggles? = null, + val environmentConfigStorage: EnvironmentConfigStorage? = null, ) : StateType \ No newline at end of file From dab5fb51b66abe978596251ee5284b7413313201 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Oct 2024 14:20:31 +0400 Subject: [PATCH 26/41] Updated on 2026-08-14 --- .../assets/configs/feature_toggles_config.json | 4 ++++ .../featuretoggles/WalletFeatureToggles.kt | 11 +++++++++++ .../wallet/di/WalletFeatureTogglesModule.kt | 18 ++++++++++++++++++ .../DefaultWalletFeatureToggles.kt | 13 +++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureTogglesModule.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt 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 b906e0ce74..3c29124728 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -26,5 +26,9 @@ { "name": "ONRAMP_ENABLED", "version": "undefined" + }, + { + "name": "MAIN_ACTION_BUTTONS_ENABLED", + "version": "undefined" } ] diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt new file mode 100644 index 0000000000..97d2d5800f --- /dev/null +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.wallet.featuretoggles + +/** + * Wallet feature toggles + * +[REDACTED_AUTHOR] + */ +interface WalletFeatureToggles { + + val isMainActionButtonsEnabled: Boolean +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureTogglesModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureTogglesModule.kt new file mode 100644 index 0000000000..aa51974608 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureTogglesModule.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.di + +import com.tangem.feature.wallet.featuretoggles.DefaultWalletFeatureToggles +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface WalletFeatureTogglesModule { + + @Singleton + @Binds + fun bindWalletFeatureToggles(toggles: DefaultWalletFeatureToggles): WalletFeatureToggles +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt new file mode 100644 index 0000000000..cda1776fc1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.wallet.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import javax.inject.Inject + +internal class DefaultWalletFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) : WalletFeatureToggles { + + override val isMainActionButtonsEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "MAIN_ACTION_BUTTONS_ENABLED") +} \ No newline at end of file From 967d5520a1749489a6b47e61ae37aa2dd23f5e61 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Oct 2024 17:49:53 +0300 Subject: [PATCH 27/41] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 57 ++++----- .../datasource/api/stakekit/StakeKitApi.kt | 18 ++- .../{EnterActionResponse.kt => ActionDTO.kt} | 2 +- .../models/response/GetActionsResponse.kt | 16 +++ ...ceStoreModule.kt => StakingStoreModule.kt} | 17 ++- .../datasource/di/StakingTokensStoreModule.kt | 20 --- .../local/token/DefaultStakingActionsStore.kt | 34 +++++ .../local/token/DefaultStakingYieldsStore.kt | 4 +- .../local/token/StakingActionsStore.kt | 13 ++ .../staking/DefaultStakingActionRepository.kt | 30 +++++ ...aultStakingPendingTransactionRepository.kt | 32 ----- .../data/staking/DefaultStakingRepository.kt | 44 ++++++- .../PendingTransactionItemConverter.kt | 23 ---- .../action/ActionStatusConverter.kt | 17 ++- .../action/EnterActionResponseConverter.kt | 6 +- .../data/staking/di/StakingDataModule.kt | 18 +-- .../staking/model/PendingTransaction.kt | 16 --- .../domain/staking/model/SubmitHashData.kt | 1 - .../staking/model/stakekit/YieldBalance.kt | 8 -- .../domain/staking/FetchActionsUseCase.kt | 41 ++++++ .../domain/staking/GetActionsUseCase.kt | 34 +++++ .../GetStakingPendingTransactionsUseCase.kt | 25 ---- .../InvalidatePendingTransactionsUseCase.kt | 119 ++++++++---------- .../staking/SavePendingTransactionUseCase.kt | 28 ----- .../repositories/StakingActionRepository.kt | 13 ++ .../StakingPendingTransactionRepository.kt | 14 --- .../staking/repositories/StakingRepository.kt | 9 ++ .../tokens/error/CurrencyStatusError.kt | 2 +- .../repository/MockStakingRepository.kt | 9 ++ .../impl/presentation/state/StakingUiState.kt | 2 - .../state/helpers/StakingBalanceUpdater.kt | 31 ++++- .../state/helpers/StakingTransactionSender.kt | 39 +----- .../ConfirmationStatePreviewData.kt | 1 - .../SetConfirmationStateInitTransformer.kt | 1 - ...etPossiblePendingTransactionTransformer.kt | 40 ------ .../viewmodel/StakingViewModel.kt | 71 ++++++----- .../viewmodels/TokenDetailsViewModel.kt | 5 +- 37 files changed, 452 insertions(+), 408 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/{EnterActionResponse.kt => ActionDTO.kt} (97%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/GetActionsResponse.kt rename core/datasource/src/main/java/com/tangem/datasource/di/{StakingBalanceStoreModule.kt => StakingStoreModule.kt} (54%) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingActionsStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/StakingActionsStore.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/DefaultStakingActionRepository.kt delete mode 100644 data/staking/src/main/java/com/tangem/data/staking/DefaultStakingPendingTransactionRepository.kt delete mode 100644 data/staking/src/main/java/com/tangem/data/staking/PendingTransactionItemConverter.kt delete mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/PendingTransaction.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/GetActionsUseCase.kt delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/GetStakingPendingTransactionsUseCase.kt delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/SavePendingTransactionUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingActionRepository.kt delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingPendingTransactionRepository.kt delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetPossiblePendingTransactionTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index be10ba1059..53e9546b1b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -1,10 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.staking.repositories.StakingTransactionHashRepository +import com.tangem.domain.staking.repositories.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -51,6 +48,32 @@ internal object StakingDomainModule { ) } + @Provides + @Singleton + fun provideFetchActionsUseCase( + stakingRepository: StakingRepository, + stakingActionRepository: StakingActionRepository, + stakingErrorResolver: StakingErrorResolver, + ): FetchActionsUseCase { + return FetchActionsUseCase( + stakingRepository = stakingRepository, + stakingActionRepository = stakingActionRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } + + @Provides + @Singleton + fun provideGetActionsUseCase( + stakingActionRepository: StakingActionRepository, + stakingErrorResolver: StakingErrorResolver, + ): GetActionsUseCase { + return GetActionsUseCase( + stakingActionRepository = stakingActionRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } + @Provides @Singleton fun provideGetStakingTokensUseCase( @@ -123,38 +146,12 @@ internal object StakingDomainModule { ) } - @Provides - @Singleton - fun provideSavePendingTransactionUseCase( - stakingPendingTransactionRepository: StakingPendingTransactionRepository, - stakingErrorResolver: StakingErrorResolver, - ): SavePendingTransactionUseCase { - return SavePendingTransactionUseCase( - stakingPendingTransactionRepository = stakingPendingTransactionRepository, - stakingErrorResolver = stakingErrorResolver, - ) - } - @Provides @Singleton fun provideInvalidatePendingTransactionsUseCase( - stakingPendingTransactionRepository: StakingPendingTransactionRepository, stakingErrorResolver: StakingErrorResolver, ): InvalidatePendingTransactionsUseCase { return InvalidatePendingTransactionsUseCase( - stakingPendingTransactionRepository = stakingPendingTransactionRepository, - stakingErrorResolver = stakingErrorResolver, - ) - } - - @Provides - @Singleton - fun provideGetPendingTransactionsUseCase( - stakingPendingTransactionRepository: StakingPendingTransactionRepository, - stakingErrorResolver: StakingErrorResolver, - ): GetStakingPendingTransactionsUseCase { - return GetStakingPendingTransactionsUseCase( - stakingPendingTransactionRepository = stakingPendingTransactionRepository, stakingErrorResolver = stakingErrorResolver, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 487f53ea0e..745fa5fe39 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -3,7 +3,8 @@ package com.tangem.datasource.api.stakekit import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse -import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse +import com.tangem.datasource.api.stakekit.models.response.ActionDTO +import com.tangem.datasource.api.stakekit.models.response.GetActionsResponse import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO @@ -35,14 +36,23 @@ interface StakeKitApi { @Body body: YieldBalanceRequestBody, ): ApiResponse> + @GET("actions") + suspend fun getActions( + @Query("walletAddress") walletAddress: String, + @Query("network") network: String, + @Query("status") status: String, + @Query("sort") sort: String = "createdAtDesc", + @Query("limit") limit: Int = 50, + ): ApiResponse + @POST("actions/enter") - suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse + suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse @POST("actions/exit") - suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse + suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse @POST("actions/pending") - suspend fun createPendingAction(@Body body: PendingActionRequestBody): ApiResponse + suspend fun createPendingAction(@Body body: PendingActionRequestBody): ApiResponse @POST("actions/enter/estimate-gas") suspend fun estimateGasOnEnter(@Body body: ActionRequestBody): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/ActionDTO.kt similarity index 97% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/ActionDTO.kt index 9413c43398..87382ee7ef 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/ActionDTO.kt @@ -9,7 +9,7 @@ import org.joda.time.DateTime import java.math.BigDecimal @JsonClass(generateAdapter = true) -data class EnterActionResponse( +data class ActionDTO( @Json(name = "id") val id: String, @Json(name = "integrationId") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/GetActionsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/GetActionsResponse.kt new file mode 100644 index 0000000000..0e116d42be --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/GetActionsResponse.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.stakekit.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetActionsResponse( + @Json(name = "data") + val data: List, + @Json(name = "hasNextPage") + val hasNextPage: Boolean, + @Json(name = "limit") + val limit: Int, + @Json(name = "page") + val page: Int, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt similarity index 54% rename from core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt rename to core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt index 21d78318c1..3dda78cc57 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt @@ -1,8 +1,9 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.token.* import com.tangem.datasource.local.token.DefaultStakingBalanceStore -import com.tangem.datasource.local.token.StakingBalanceStore +import com.tangem.datasource.local.token.DefaultStakingYieldsStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,11 +12,23 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object StakingBalanceStoreModule { +internal object StakingStoreModule { + + @Provides + @Singleton + fun provideStakingTokensStore(): StakingYieldsStore { + return DefaultStakingYieldsStore() + } @Provides @Singleton fun provideStakingBalanceStore(): StakingBalanceStore { return DefaultStakingBalanceStore(dataStore = RuntimeDataStore()) } + + @Provides + @Singleton + fun provideStakingActionsStore(): StakingActionsStore { + return DefaultStakingActionsStore(dataStore = RuntimeDataStore()) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt deleted file mode 100644 index d55c4066ec..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.datasource.local.token.DefaultStakingYieldsStore -import com.tangem.datasource.local.token.StakingYieldsStore -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 StakingTokensStoreModule { - - @Provides - @Singleton - fun provideStakingTokensStore(): StakingYieldsStore { - return DefaultStakingYieldsStore() - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingActionsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingActionsStore.kt new file mode 100644 index 0000000000..39f499ca68 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingActionsStore.kt @@ -0,0 +1,34 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +internal class DefaultStakingActionsStore( + private val dataStore: StringKeyDataStore>, +) : StakingActionsStore { + + private val mutex = Mutex() + + override fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> { + return dataStore.get(composeKey(userWalletId, cryptoCurrencyId)) + } + + override suspend fun store( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + items: List, + ) { + mutex.withLock { + dataStore.store(composeKey(userWalletId, cryptoCurrencyId), items) + } + } + + private fun composeKey(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): String { + return userWalletId.stringValue + cryptoCurrencyId.value + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt index db8de27d38..d30a83e4c3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt @@ -4,13 +4,13 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO internal class DefaultStakingYieldsStore : StakingYieldsStore { - private var yields = mutableListOf() + private var yields = listOf() override fun get(): List { return yields } override fun store(items: List) { - yields = items.toMutableList() + yields = items } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingActionsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingActionsStore.kt new file mode 100644 index 0000000000..aa6de02da4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingActionsStore.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.local.token + +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +interface StakingActionsStore { + + fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> + + suspend fun store(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, items: List) +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingActionRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingActionRepository.kt new file mode 100644 index 0000000000..888f6a342c --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingActionRepository.kt @@ -0,0 +1,30 @@ +package com.tangem.data.staking + +import com.tangem.datasource.local.token.StakingActionsStore +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.repositories.StakingActionRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext + +internal class DefaultStakingActionRepository( + private val stakingActionsStore: StakingActionsStore, + private val dispatchers: CoroutineDispatcherProvider, +) : StakingActionRepository { + + override suspend fun store( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + actions: List, + ) { + withContext(dispatchers.io) { + stakingActionsStore.store(userWalletId, cryptoCurrencyId, actions) + } + } + + override fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> { + return stakingActionsStore.get(userWalletId, cryptoCurrencyId) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingPendingTransactionRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingPendingTransactionRepository.kt deleted file mode 100644 index 008ea7161a..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingPendingTransactionRepository.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.data.staking - -import com.tangem.domain.staking.model.PendingTransaction -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.wallets.models.UserWalletId -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArrayList - -internal class DefaultStakingPendingTransactionRepository : StakingPendingTransactionRepository { - - private val pendingTransactionsMap = ConcurrentHashMap>() - - override fun saveTransaction(userWalletId: UserWalletId, transaction: PendingTransaction) { - val transactions = pendingTransactionsMap.computeIfAbsent(userWalletId) { CopyOnWriteArrayList() } - transactions.add(transaction) - } - - override fun removeTransactions(userWalletId: UserWalletId, transactions: Set) { - pendingTransactionsMap[userWalletId]?.removeAll(transactions) - } - - override fun getTransactionsWithBalanceItems( - userWalletId: UserWalletId, - ): List> { - return pendingTransactionsMap[userWalletId]?.mapNotNull { pendingTransaction: PendingTransaction -> - PendingTransactionItemConverter.convert(pendingTransaction)?.let { balanceItem -> - pendingTransaction to balanceItem - } - } ?: emptyList() - } -} \ 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 ce9296103a..e57c15dd67 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 @@ -25,19 +25,19 @@ import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConv import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.request.* +import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO 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.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.NetworkType -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceList +import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate @@ -102,6 +102,8 @@ internal class DefaultStakingRepository( private val yieldBalanceListConverter = YieldBalanceListConverter(yieldBalanceConverter) private val tronStakeKitTransactionAdapter by lazy { moshi.adapter(TronStakeKitTransaction::class.java) } + private val networkTypeAdapter by lazy { moshi.adapter(NetworkTypeDTO::class.java) } + private val stakingActionStatusAdapter by lazy { moshi.adapter(StakingActionStatusDTO::class.java) } override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) { rawNetworkId.plus(rawCurrencyId) @@ -140,6 +142,40 @@ internal class DefaultStakingRepository( } } + override suspend fun getActions( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + networkType: NetworkType, + stakingActionStatus: StakingActionStatus, + ): List { + return withContext(dispatchers.io) { + val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() + + val networkTypeDto = networkTypeConverter.convertBack(networkType) + val networkTypeString = networkTypeDto.extractJsonName() + + val actionStatusDTO = actionStatusConverter.convertBack(stakingActionStatus) + val actionStatusString = actionStatusDTO.extractJsonName() + + enterActionResponseConverter.convertListIgnoreErrors( + input = stakeKitApi.getActions( + walletAddress = address, + network = networkTypeString, + status = actionStatusString, + ).getOrThrow().data, + onError = { Timber.e("Error converting staking actions list: $it") }, + ) + } + } + + private fun NetworkTypeDTO.extractJsonName(): String { + return networkTypeAdapter.toJson(this).replace("\"", "") + } + + private fun StakingActionStatusDTO.extractJsonName(): String { + return stakingActionStatusAdapter.toJson(this).replace("\"", "") + } + override suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo { return withContext(dispatchers.io) { val yield = getYield(cryptoCurrencyId, symbol) diff --git a/data/staking/src/main/java/com/tangem/data/staking/PendingTransactionItemConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/PendingTransactionItemConverter.kt deleted file mode 100644 index 026c1435bd..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/PendingTransactionItemConverter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.data.staking - -import com.tangem.domain.staking.model.PendingTransaction -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.utils.converter.Converter -import org.joda.time.DateTime - -internal object PendingTransactionItemConverter : Converter { - - override fun convert(value: PendingTransaction): BalanceItem? { - return BalanceItem( - groupId = value.groupId ?: return null, - token = value.token, - type = value.type, - amount = value.amount, - rawCurrencyId = value.rawCurrencyId, - validatorAddress = value.validator?.address, - date = DateTime.now(), - pendingActions = emptyList(), - isPending = true, - ) - } -} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt index 4bc04836a3..0a4ce1e6ff 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt @@ -2,9 +2,10 @@ package com.tangem.data.staking.converters.action import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter + +class ActionStatusConverter : TwoWayConverter { -class ActionStatusConverter : Converter { override fun convert(value: StakingActionStatusDTO): StakingActionStatus { return when (value) { StakingActionStatusDTO.CANCELED -> StakingActionStatus.CANCELED @@ -16,4 +17,16 @@ class ActionStatusConverter : Converter StakingActionStatus.UNKNOWN } } + + override fun convertBack(value: StakingActionStatus): StakingActionStatusDTO { + return when (value) { + StakingActionStatus.CANCELED -> StakingActionStatusDTO.CANCELED + StakingActionStatus.CREATED -> StakingActionStatusDTO.CREATED + StakingActionStatus.WAITING_FOR_NEXT -> StakingActionStatusDTO.WAITING_FOR_NEXT + StakingActionStatus.PROCESSING -> StakingActionStatusDTO.PROCESSING + StakingActionStatus.FAILED -> StakingActionStatusDTO.FAILED + StakingActionStatus.SUCCESS -> StakingActionStatusDTO.SUCCESS + else -> StakingActionStatusDTO.UNKNOWN + } + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt index d06ab798d2..b46fd75f85 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters.action import com.tangem.data.staking.converters.transaction.StakingTransactionConverter -import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse +import com.tangem.datasource.api.stakekit.models.response.ActionDTO import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.utils.converter.Converter @@ -9,9 +9,9 @@ class EnterActionResponseConverter( private val actionStatusConverter: ActionStatusConverter, private val stakingActionTypeConverter: StakingActionTypeConverter, private val transactionConverter: StakingTransactionConverter, -) : Converter { +) : Converter { - override fun convert(value: EnterActionResponse): StakingAction { + override fun convert(value: ActionDTO): StakingAction { return StakingAction( id = value.id, integrationId = value.integrationId, diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index e440b5c172..2226847178 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -2,8 +2,8 @@ package com.tangem.data.staking.di import com.squareup.moshi.Moshi import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.staking.* import com.tangem.data.staking.DefaultStakingErrorResolver -import com.tangem.data.staking.DefaultStakingPendingTransactionRepository import com.tangem.data.staking.DefaultStakingRepository import com.tangem.data.staking.DefaultStakingTransactionHashRepository import com.tangem.data.staking.converters.error.StakeKitErrorConverter @@ -11,12 +11,10 @@ import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.StakingActionsStore import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.staking.repositories.StakingTransactionHashRepository +import com.tangem.domain.staking.repositories.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles @@ -73,8 +71,14 @@ internal object StakingDataModule { @Provides @Singleton - fun provideStakingPendingTransactionRepository(): StakingPendingTransactionRepository { - return DefaultStakingPendingTransactionRepository() + fun provideStakingActionRepository( + stakingActionsStore: StakingActionsStore, + dispatchers: CoroutineDispatcherProvider, + ): StakingActionRepository { + return DefaultStakingActionRepository( + stakingActionsStore = stakingActionsStore, + dispatchers = dispatchers, + ) } @Provides diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/PendingTransaction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/PendingTransaction.kt deleted file mode 100644 index 408de7dc0d..0000000000 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/PendingTransaction.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.domain.staking.model - -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.Token -import com.tangem.domain.staking.model.stakekit.Yield -import java.math.BigDecimal - -data class PendingTransaction( - val groupId: String?, - val token: Token, - val type: BalanceType, - val amount: BigDecimal, - val rawCurrencyId: String?, - val validator: Yield.Validator?, - val balancesId: Int, -) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/SubmitHashData.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/SubmitHashData.kt index 684264334d..662c27c5c8 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/SubmitHashData.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/SubmitHashData.kt @@ -3,5 +3,4 @@ package com.tangem.domain.staking.model data class SubmitHashData( val transactionHash: String, val transactionId: String, - val pendingTransaction: PendingTransaction, ) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index d1f0f3a3f3..ef9946cdb7 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -32,14 +32,6 @@ sealed class YieldBalance { .distinctBy { it.validatorAddress } .size } - - fun getBalancesUniqueId(): Int { - // need to exclude rewards because their amount may change frequently - return balance.items - .filter { it.type != BalanceType.REWARDS } - .map { it.amount.toString() + it.type.toString() + it.groupId } - .hashCode() - } } data object Empty : YieldBalance() diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt new file mode 100644 index 0000000000..1e437878e6 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus +import com.tangem.domain.staking.repositories.StakingActionRepository +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for getting pending actions list. + */ +class FetchActionsUseCase( + private val stakingRepository: StakingRepository, + private val stakingActionRepository: StakingActionRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + networkType: NetworkType, + stakingActionStatus: StakingActionStatus, + ): Either { + return Either + .catch { + val actions = stakingRepository.getActions( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + networkType = networkType, + stakingActionStatus = stakingActionStatus, + ) + + stakingActionRepository.store(userWalletId, cryptoCurrency.id, actions) + } + .mapLeft { stakingErrorResolver.resolve(it) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionsUseCase.kt new file mode 100644 index 0000000000..3190d22711 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionsUseCase.kt @@ -0,0 +1,34 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.repositories.StakingActionRepository +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +/** + * Use case for getting pending actions list. + */ +class GetActionsUseCase( + private val stakingActionRepository: StakingActionRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ): EitherFlow> { + return stakingActionRepository.get( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrencyId, + ).map, Either>> { it.right() } + .catch { emit(stakingErrorResolver.resolve(it).left()) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingPendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingPendingTransactionsUseCase.kt deleted file mode 100644 index 15557232b0..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingPendingTransactionsUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.domain.staking - -import arrow.core.Either -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.wallets.models.UserWalletId - -/** - * Use case for getting saved staking pending transactions. - */ -class GetStakingPendingTransactionsUseCase( - private val stakingPendingTransactionRepository: StakingPendingTransactionRepository, - private val stakingErrorResolver: StakingErrorResolver, -) { - - operator fun invoke(userWalletId: UserWalletId): Either> { - return Either.catch { - stakingPendingTransactionRepository.getTransactionsWithBalanceItems(userWalletId).map { it.second } - }.mapLeft { - stakingErrorResolver.resolve(it) - } - } -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 2c07ae8ea0..1b0f6b9128 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -1,104 +1,85 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.PendingTransaction import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.wallets.models.UserWalletId -import org.joda.time.DateTime -import java.math.BigDecimal import java.util.UUID class InvalidatePendingTransactionsUseCase( - private val stakingPendingTransactionRepository: StakingPendingTransactionRepository, private val stakingErrorResolver: StakingErrorResolver, ) { operator fun invoke( - userWalletId: UserWalletId, balanceItems: List, - balancesId: Int, + processingActions: List, ): Either> { return Either.catch { - val (balancesToDisplay, transactionsToRemove) = mergeRealAndPendingTransactions( - realData = balanceItems, - newBalancesId = balancesId, - pendingData = stakingPendingTransactionRepository.getTransactionsWithBalanceItems(userWalletId), + val balancesToDisplay = mergeBalancesAndProcessingActions( + realBalances = balanceItems, + processingActions = processingActions, ) - - stakingPendingTransactionRepository.removeTransactions(userWalletId, transactionsToRemove.toSet()) - balancesToDisplay }.mapLeft { stakingErrorResolver.resolve(it) } } - private fun mergeRealAndPendingTransactions( - realData: List, - newBalancesId: Int, - pendingData: List>, - ): Pair, List> { - val balances = realData.groupBy { BalanceIdentity(it.groupId, it.type, it.amount, it.date) } - .mapValues { it.value.toMutableList() } - .toMutableMap() + private fun mergeBalancesAndProcessingActions( + realBalances: List, + processingActions: List, + ): List { + val balances = realBalances.toMutableList() - val transactionsToRemove = mutableListOf() - - pendingData.forEach { (pendingTransaction, balanceItem) -> - val key = BalanceIdentity(balanceItem.groupId, balanceItem.type, balanceItem.amount, balanceItem.date) - val oldBalancesId = pendingTransaction.balancesId - - when { - newBalancesId != oldBalancesId -> { - transactionsToRemove.add(pendingTransaction) + processingActions.forEach { action -> + when (action.type) { + StakingActionType.STAKE, StakingActionType.VOTE, StakingActionType.VOTE_LOCKED -> { + processEnterAction(balances, action) } - balances.containsKey(key) -> { - val removed = balances[key]?.removeIf { !it.isPending } ?: false - if (removed) { - balances[key]?.add(balanceItem) - } + StakingActionType.WITHDRAW -> { + modifyBalancesByStatus(balances, action, BalanceType.UNSTAKED) + } + StakingActionType.UNLOCK_LOCKED -> { + modifyBalancesByStatus(balances, action, BalanceType.LOCKED) + } + StakingActionType.UNSTAKE -> { + modifyBalancesByStatus(balances, action, BalanceType.STAKED) } else -> { - val groupId = UUID.randomUUID().toString() - val now = DateTime.now() - - balances[BalanceIdentity(groupId, BalanceType.STAKED, pendingTransaction.amount, now)] = - mutableListOf( - BalanceItem( - groupId = groupId, - type = pendingTransaction.type, - amount = pendingTransaction.amount, - rawCurrencyId = pendingTransaction.rawCurrencyId, - validatorAddress = pendingTransaction.validator?.address, - date = null, - pendingActions = emptyList(), - token = pendingTransaction.token, - isPending = true, - ), - ) - - balances.entries.find { - it.key.type == BalanceType.STAKED && - it.key.amount == pendingTransaction.amount && - !it.value.any { it.isPending } - } - ?.key - ?.let { balances.remove(it) } + // intentionally do nothing } } } - return balances.values.flatten() to transactionsToRemove + return balances } - private data class BalanceIdentity( - val groupId: String, - val type: BalanceType, - val amount: BigDecimal, - val date: DateTime?, - ) + private fun processEnterAction(balances: MutableList, action: StakingAction) { + balances.add( + BalanceItem( + groupId = UUID.randomUUID().toString(), + token = balances[0].token, + type = BalanceType.STAKED, + amount = action.amount, + rawCurrencyId = null, + validatorAddress = action.validatorAddress ?: action.validatorAddresses?.get(0) ?: "", + date = null, + pendingActions = emptyList(), + isPending = true, + ), + ) + } + + private fun modifyBalancesByStatus(balances: MutableList, action: StakingAction, type: BalanceType) { + val index = balances.indexOfFirst { + !it.isPending && it.amount == action.amount && it.type == type + } + + if (index != -1) { + balances[index] = balances[index].copy(isPending = true) + } + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SavePendingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SavePendingTransactionUseCase.kt deleted file mode 100644 index ef80e06e64..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SavePendingTransactionUseCase.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.domain.staking - -import arrow.core.Either -import com.tangem.domain.staking.model.PendingTransaction -import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.wallets.models.UserWalletId - -/** - * Use case for saving hash that failed to submit during staking confirmation - */ -class SavePendingTransactionUseCase( - private val stakingPendingTransactionRepository: StakingPendingTransactionRepository, - private val stakingErrorResolver: StakingErrorResolver, -) { - - operator fun invoke(userWalletId: UserWalletId, transaction: PendingTransaction): Either { - return Either.catch { - stakingPendingTransactionRepository.saveTransaction( - userWalletId = userWalletId, - transaction = transaction, - ) - }.mapLeft { - stakingErrorResolver.resolve(it) - } - } -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingActionRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingActionRepository.kt new file mode 100644 index 0000000000..142c9d08f6 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingActionRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.staking.repositories + +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +interface StakingActionRepository { + + suspend fun store(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, actions: List) + + fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingPendingTransactionRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingPendingTransactionRepository.kt deleted file mode 100644 index 708b8e1352..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingPendingTransactionRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.staking.repositories - -import com.tangem.domain.staking.model.PendingTransaction -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.wallets.models.UserWalletId - -interface StakingPendingTransactionRepository { - - fun getTransactionsWithBalanceItems(userWalletId: UserWalletId): List> - - fun saveTransaction(userWalletId: UserWalletId, transaction: PendingTransaction) - - fun removeTransactions(userWalletId: UserWalletId, transactions: Set) -} \ No newline at end of file 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 568683be77..e68adf73a9 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 @@ -6,10 +6,12 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.stakekit.NetworkType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction @@ -33,6 +35,13 @@ interface StakingRepository { fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): StakingAvailability + suspend fun getActions( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + networkType: NetworkType, + stakingActionStatus: StakingActionStatus, + ): List + suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt index c1f5dfaac8..624325cda9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt @@ -2,7 +2,7 @@ package com.tangem.domain.tokens.error sealed class CurrencyStatusError { - object UnableToCreateCurrency : CurrencyStatusError() + data object UnableToCreateCurrency : CurrencyStatusError() data class DataError(val cause: Throwable) : CurrencyStatusError() } \ 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 0dc93edf8d..fa3e6119d7 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 @@ -117,6 +117,15 @@ class MockStakingRepository : StakingRepository { cryptoCurrency: CryptoCurrency, ): StakingAvailability = StakingAvailability.Unavailable + override suspend fun getActions( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + networkType: NetworkType, + stakingActionStatus: StakingActionStatus, + ): List { + return emptyList() + } + override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, 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 56aa176637..cdc4967df2 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 @@ -9,7 +9,6 @@ import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent 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 @@ -120,7 +119,6 @@ internal sealed class StakingStates { val transactionDoneState: TransactionDoneState, val isApprovalNeeded: Boolean, val reduceAmountBy: BigDecimal?, - val possiblePendingTransaction: PendingTransaction?, ) : ConfirmationState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index 55446f2303..aa8342846e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -1,6 +1,9 @@ package com.tangem.features.staking.impl.presentation.state.helpers import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase +import com.tangem.domain.staking.FetchActionsUseCase +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -20,16 +23,21 @@ internal class StakingBalanceUpdater @AssistedInject constructor( private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val fetchActionsUseCase: FetchActionsUseCase, @DelayedWork private val coroutineScope: CoroutineScope, @Assisted private val userWallet: UserWallet, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, + @Assisted private val yield: Yield, ) { fun scheduleUpdates() { coroutineScope.launch { listOf( // we should update network to find pending tx after 1 sec async { - fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrencyStatus.currency.network)) + fetchPendingTransactionsUseCase( + userWalletId = userWallet.walletId, + networks = setOf(cryptoCurrencyStatus.currency.network), + ) }, // we should update tx history and network for new balances async { @@ -41,6 +49,9 @@ internal class StakingBalanceUpdater @AssistedInject constructor( async { updateNetworkStatuses() }, + async { + updateProcessingActions() + }, ).awaitAll() } } @@ -54,6 +65,9 @@ internal class StakingBalanceUpdater @AssistedInject constructor( async { updateNetworkStatuses(delay = 0) }, + async { + updateProcessingActions() + }, ).awaitAll() } } @@ -91,9 +105,22 @@ internal class StakingBalanceUpdater @AssistedInject constructor( } } + private suspend fun updateProcessingActions() { + fetchActionsUseCase( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + networkType = yield.token.network, + stakingActionStatus = StakingActionStatus.PROCESSING, + ) + } + @AssistedFactory interface Factory { - fun create(cryptoCurrencyStatus: CryptoCurrencyStatus, userWallet: UserWallet): StakingBalanceUpdater + fun create( + cryptoCurrencyStatus: CryptoCurrencyStatus, + userWallet: UserWallet, + yield: Yield, + ): StakingBalanceUpdater } private companion object { 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 2ac221975e..5e5538c4e1 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 @@ -6,7 +6,6 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.staking.* -import com.tangem.domain.staking.model.PendingTransaction import com.tangem.domain.staking.model.SubmitHashData import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType @@ -36,7 +35,6 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import timber.log.Timber import java.math.BigDecimal -import java.util.UUID @Suppress("LongParameterList") internal class StakingTransactionSender @AssistedInject constructor( @@ -48,7 +46,6 @@ internal class StakingTransactionSender @AssistedInject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val submitHashUseCase: SubmitHashUseCase, private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, - private val savePendingTransactionUseCase: SavePendingTransactionUseCase, private val analyticsEventHandler: AnalyticsEventHandler, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val userWallet: UserWallet, @@ -57,7 +54,7 @@ internal class StakingTransactionSender @AssistedInject constructor( ) { private val balanceUpdater: StakingBalanceUpdater - get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet) + get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, yield) suspend fun constructAndSendTransactions( onConstructSuccess: (List) -> Unit, @@ -95,16 +92,11 @@ internal class StakingTransactionSender @AssistedInject constructor( sendStakingTransaction( fullTransactionsData = fullTransactionsData, - possiblePendingTransaction = composePendingTransaction(confirmationState), onSendSuccess = onSendSuccess, onSendError = onSendError, ) } - private fun composePendingTransaction(confirmationState: StakingStates.ConfirmationState.Data): PendingTransaction { - return confirmationState.possiblePendingTransaction ?: composeStakeTransaction() - } - private suspend fun getStakingTransactions( state: StakingUiState, confirmationState: StakingStates.ConfirmationState.Data, @@ -214,7 +206,6 @@ internal class StakingTransactionSender @AssistedInject constructor( private suspend fun sendStakingTransaction( fullTransactionsData: List, - possiblePendingTransaction: PendingTransaction, onSendSuccess: (txUrl: String) -> Unit, onSendError: (SendTransactionError?) -> Unit, ) { @@ -230,7 +221,6 @@ internal class StakingTransactionSender @AssistedInject constructor( submitHash( transactions = fullTransactionsData.map { it.stakeKitTransaction }, transactionHashes = transactionHashes, - pendingTransaction = possiblePendingTransaction, ) val txUrl = getExplorerTransactionUrlUseCase( txHash = transactionHashes.last(), @@ -243,11 +233,7 @@ internal class StakingTransactionSender @AssistedInject constructor( ) } - private suspend fun submitHash( - transactions: List, - transactionHashes: List, - pendingTransaction: PendingTransaction, - ) { + private suspend fun submitHash(transactions: List, transactionHashes: List) { transactions .zip(transactionHashes) .forEach { (transaction, transactionHash) -> @@ -255,7 +241,6 @@ internal class StakingTransactionSender @AssistedInject constructor( SubmitHashData( transactionId = transaction.id, transactionHash = transactionHash, - pendingTransaction = pendingTransaction, ), ) .onLeft { error -> @@ -271,9 +256,6 @@ internal class StakingTransactionSender @AssistedInject constructor( ) }.onRight { Timber.d("Successful hash submission") - if (transaction.type != StakingTransactionType.FREEZE_ENERGY) { - savePendingTransactionUseCase.invoke(userWallet.walletId, pendingTransaction) - } } } } @@ -292,23 +274,6 @@ internal class StakingTransactionSender @AssistedInject constructor( ) } - private fun composeStakeTransaction(): PendingTransaction { - val state = stateController.value - - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data - val token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId) - - return PendingTransaction( - groupId = UUID.randomUUID().toString(), - token = token, - type = BalanceType.STAKED, - amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO, - rawCurrencyId = cryptoCurrencyStatus.currency.id.rawCurrencyId, - validator = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenValidator, - balancesId = yieldBalance?.getBalancesUniqueId() ?: 0, - ) - } - private data class FullTransactionData( val stakeKitTransaction: StakingTransaction, val tangemTransaction: TransactionData.Compiled, 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 016a6a0625..dbcf70f6bb 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 @@ -47,6 +47,5 @@ internal object ConfirmationStatePreviewData { isApprovalNeeded = false, reduceAmountBy = null, pendingActions = null, - 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/SetConfirmationStateInitTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt index a37a92cdcd..8387933908 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt @@ -63,7 +63,6 @@ internal class SetConfirmationStateInitTransformer( reduceAmountBy = null, pendingAction = pendingAction, pendingActions = pendingActions.takeIf { isComposePendingActions }, - possiblePendingTransaction = null, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetPossiblePendingTransactionTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetPossiblePendingTransactionTransformer.kt deleted file mode 100644 index 26508e49b5..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetPossiblePendingTransactionTransformer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.transformers - -import com.tangem.domain.staking.model.PendingTransaction -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.staking.impl.presentation.state.BalanceState -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 class SetPossiblePendingTransactionTransformer( - private val yield: Yield, - private val balanceState: BalanceState, - private val cryptoCurrencyStatus: CryptoCurrencyStatus, -) : Transformer { - - override fun transform(prevState: StakingUiState): StakingUiState { - val possibleConfirmationState = - prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data - - val balancesId = yieldBalance?.getBalancesUniqueId() ?: 0 - val token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId) - - return prevState.copy( - confirmationState = possibleConfirmationState.copy( - possiblePendingTransaction = PendingTransaction( - groupId = balanceState.groupId, - token = token, - type = balanceState.type, - amount = balanceState.cryptoDecimal, - rawCurrencyId = balanceState.rawCurrencyId, - validator = balanceState.validator, - balancesId = balancesId, - ), - ), - ) - } -} \ No newline at end of file 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 6639fdf7cb..6850137ee4 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 @@ -25,14 +25,13 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.staking.InvalidatePendingTransactionsUseCase -import com.tangem.domain.staking.IsAnyTokenStakedUseCase -import com.tangem.domain.staking.IsApproveNeededUseCase +import com.tangem.domain.staking.* import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.tokens.* @@ -74,7 +73,6 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import java.math.BigDecimal import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -108,6 +106,7 @@ internal class StakingViewModel @Inject constructor( private val stakingBalanceUpdater: StakingBalanceUpdater.Factory, private val analyticsEventHandler: AnalyticsEventHandler, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val getActionsUseCase: GetActionsUseCase, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -131,6 +130,7 @@ internal class StakingViewModel @Inject constructor( ?: error("This screen can't be opened without `Yield`") private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var processingActions: List = emptyList() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null private var innerRouter: InnerStakingRouter by Delegates.notNull() @@ -141,9 +141,8 @@ internal class StakingViewModel @Inject constructor( get() { val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data return invalidatePendingTransactionsUseCase( - userWalletId = userWallet.walletId, balanceItems = yieldBalance?.balance?.items ?: emptyList(), - balancesId = yieldBalance?.getBalancesUniqueId() ?: 0, + processingActions = processingActions, ).getOrElse { emptyList() } } @@ -153,6 +152,7 @@ internal class StakingViewModel @Inject constructor( stakingBalanceUpdater.create( cryptoCurrencyStatus, userWallet, + yield, ) } @@ -215,11 +215,6 @@ internal class StakingViewModel @Inject constructor( override fun onNextClick(balanceState: BalanceState?) { stakingStateRouter.onNextClick() - if (balanceState != null && (isInitState() || isAssentState())) { - stateController.update( - SetPossiblePendingTransactionTransformer(yield, balanceState, cryptoCurrencyStatus), - ) - } } override fun getFee() { @@ -281,7 +276,6 @@ internal class StakingViewModel @Inject constructor( transactionsInProgress.addAll(constructedTransactions) }, onConstructError = { error -> - Timber.e(error.toString()) analyticsEventHandler.send( StakingAnalyticsEvents.StakekitError( token = value.cryptoCurrencySymbol, @@ -297,7 +291,6 @@ internal class StakingViewModel @Inject constructor( stateController.update(SetConfirmationStateCompletedTransformer(txUrl)) }, onSendError = { error -> - Timber.e(error.toString()) analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol)) stakingEventFactory.createSendTransactionErrorAlert(error) stateController.update(SetConfirmationStateResetAssentTransformer) @@ -527,7 +520,6 @@ internal class StakingViewModel @Inject constructor( userWalletId = userWalletId, ).fold( ifLeft = { error -> - Timber.e(error.toString()) analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol)) stateController.update( SetConfirmationStateAssentApprovalTransformer( @@ -549,7 +541,6 @@ internal class StakingViewModel @Inject constructor( network = tokenCryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error.toString()) analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol)) stateController.update( SetConfirmationStateAssentApprovalTransformer( @@ -760,7 +751,6 @@ internal class StakingViewModel @Inject constructor( userWallet = wallet }, ifLeft = { - Timber.e(it.toString()) stakingEventFactory.createGenericErrorAlert(it.toString()) stateController.update(SetConfirmationStateResetAssentTransformer) }, @@ -790,10 +780,10 @@ internal class StakingViewModel @Inject constructor( setupApprovalNeeded() setupIsAnyTokenStaked() checkIfSubtractAvailable() + subscribeOnActionsUpdates() subscribeOnStepChanges() }, ifLeft = { error -> - Timber.e(error.toString()) stakingEventFactory.createGenericErrorAlert(error.toString()) stateController.update(SetConfirmationStateResetAssentTransformer) }, @@ -831,18 +821,7 @@ internal class StakingViewModel @Inject constructor( .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, - ) + updateInitialData() onRefreshSwipe(isRefreshing = false) } isAssentState() -> { @@ -862,6 +841,40 @@ internal class StakingViewModel @Inject constructor( .launchIn(viewModelScope) } + private fun subscribeOnActionsUpdates() { + getActionsUseCase( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrencyId, + ) + .conflate() + .distinctUntilChanged() + .onEach { result -> + result.getOrNull()?.let { actions -> + processingActions = actions + if (isInitState()) { + updateInitialData() + } + } + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + + private fun updateInitialData() { + stateController.updateAll( + SetInitialDataStateTransformer( + clickIntents = this@StakingViewModel, + yield = yield, + isAnyTokenStaked = isAnyTokenStaked, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider { appCurrency }, + balancesToShowProvider = Provider { balancesToShow }, + ), + SetConfirmationStateEmptyTransformer, + ) + } + private fun prepareForConfirmation( pendingActions: ImmutableList = persistentListOf(), pendingAction: PendingAction? = pendingActions.firstOrNull(), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 1b91c07eab..a63d88e92a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -32,7 +32,6 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase -import com.tangem.domain.staking.GetStakingPendingTransactionsUseCase import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.GetStakingIntegrationIdUseCase @@ -121,7 +120,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val swapTransactionStatusStore: SwapTransactionStatusStore, private val isDemoCardUseCase: IsDemoCardUseCase, private val associateAssetUseCase: AssociateAssetUseCase, - private val getStakingPendingTransactionsUseCase: GetStakingPendingTransactionsUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, private val vibratorHapticManager: VibratorHapticManager, @@ -151,8 +149,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val warningsJobHolder = JobHolder() private val swapTxJobHolder = JobHolder() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - private val stakingPendingBalances: List - get() = getStakingPendingTransactionsUseCase(userWalletId).getOrElse { emptyList() } + private val stakingPendingBalances: List = emptyList() // TODO staking private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var stakingEntryInfo: StakingEntryInfo? = null From fea0ea58905422653ca80e986a85f5557bed1f11 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 10:45:08 +0300 Subject: [PATCH 28/41] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 - .../tokens/GetCryptoCurrencyActionsUseCase.kt | 4 +- .../features/markets/MarketsFeatureToggles.kt | 5 - .../markets/DefaultMarketsFeatureToggles.kt | 11 -- .../features/markets/di/FeatureModule.kt | 20 ---- .../presentation/TokenDetailsFragment.kt | 38 +++---- .../wallet/presentation/WalletFragment.kt | 24 ++-- .../router/DefaultWalletRouter.kt | 2 +- .../presentation/router/InnerWalletRouter.kt | 2 +- .../presentation/wallet/ui/WalletScreen.kt | 105 +++++------------- .../wallet/viewmodels/WalletViewModel.kt | 4 +- 11 files changed, 57 insertions(+), 161 deletions(-) delete mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 5245e5a7ec..c28ded59d7 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -7,7 +7,6 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.domain.api.SwapRepository -import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -234,7 +233,6 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, stakingFeatureToggles: StakingFeatureToggles, - marketsFeatureToggles: MarketsFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( @@ -246,7 +244,6 @@ internal object TokensDomainModule { networksRepository = networksRepository, stakingRepository = stakingRepository, stakingFeatureToggles = stakingFeatureToggles, - marketsFeatureToggles = marketsFeatureToggles, dispatchers = dispatchers, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index c1ac067bff..ad321616d3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -12,7 +12,6 @@ import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero @@ -34,7 +33,6 @@ class GetCryptoCurrencyActionsUseCase( private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val stakingFeatureToggles: StakingFeatureToggles, - private val marketsFeatureToggles: MarketsFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -116,7 +114,7 @@ class GetCryptoCurrencyActionsUseCase( // markets // not a custom token - if (marketsFeatureToggles.isFeatureEnabled && cryptoCurrencyStatus.currency.id.rawCurrencyId != null) { + if (cryptoCurrencyStatus.currency.id.rawCurrencyId != null) { activeList.add(TokenActionsState.ActionState.Analytics(ScenarioUnavailabilityReason.None)) } diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt deleted file mode 100644 index 34758a7b41..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.markets - -interface MarketsFeatureToggles { - val isFeatureEnabled: Boolean -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt deleted file mode 100644 index 75062eddef..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.features.markets - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager - -internal class DefaultMarketsFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : MarketsFeatureToggles { - - override val isFeatureEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("MARKETS_ENABLED") -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt deleted file mode 100644 index a2085dfe77..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.features.markets.DefaultMarketsFeatureToggles -import com.tangem.features.markets.MarketsFeatureToggles -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): MarketsFeatureToggles = - DefaultMarketsFeatureToggles(featureTogglesManager = featureTogglesManager) -} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index 3d8785e062..5b11ac88dd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -20,7 +20,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel -import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -36,9 +35,6 @@ internal class TokenDetailsFragment : ComposeFragment() { @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter - @Inject - internal lateinit var marketsFeatureToggles: MarketsFeatureToggles - @Inject internal lateinit var coroutineDispatcherProvider: CoroutineDispatcherProvider @@ -61,27 +57,25 @@ internal class TokenDetailsFragment : ComposeFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (marketsFeatureToggles.isFeatureEnabled) { - val cryptoCurrency: CryptoCurrency = arguments - ?.getBundle(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY) - ?.unbundle(CryptoCurrency.serializer()) - ?: error("Token Details screen can't be opened without `CryptoCurrency`") + val cryptoCurrency: CryptoCurrency = arguments + ?.getBundle(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY) + ?.unbundle(CryptoCurrency.serializer()) + ?: error("Token Details screen can't be opened without `CryptoCurrency`") - val param = cryptoCurrency.toParam() ?: return + val param = cryptoCurrency.toParam() ?: return - val appContext = DefaultAppComponentContext( - componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), - messageHandler = uiDependencies.eventMessageHandler, - dispatchers = coroutineDispatcherProvider, - hiltComponentBuilder = componentBuilder, - replaceRouter = appRouter.asRouter(), - ) + val appContext = DefaultAppComponentContext( + componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), + messageHandler = uiDependencies.eventMessageHandler, + dispatchers = coroutineDispatcherProvider, + hiltComponentBuilder = componentBuilder, + replaceRouter = appRouter.asRouter(), + ) - tokenMarketBlockComponent = tokenMarketBlockComponentFactory.create( - appComponentContext = appContext, - params = param, - ) - } + tokenMarketBlockComponent = tokenMarketBlockComponentFactory.create( + appComponentContext = appContext, + params = param, + ) } private fun CryptoCurrency.toParam(): TokenMarketBlockComponent.Params? { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 74b81ff310..ba3c83a69c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -12,7 +12,6 @@ import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -43,13 +42,10 @@ internal class WalletFragment : ComposeFragment() { @Inject internal lateinit var componentBuilder: DecomposeComponent.Builder - @Inject - internal lateinit var marketsFeatureToggles: MarketsFeatureToggles - @Inject internal lateinit var appRouter: AppRouter - private var marketsEntryComponent: MarketsEntryComponent? = null + private lateinit var marketsEntryComponent: MarketsEntryComponent private val _walletRouter: InnerWalletRouter get() = requireNotNull(walletRouter as? InnerWalletRouter) { @@ -59,17 +55,15 @@ internal class WalletFragment : ComposeFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (marketsFeatureToggles.isFeatureEnabled) { - val appContext = DefaultAppComponentContext( - componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), - messageHandler = uiDependencies.eventMessageHandler, - dispatchers = coroutineDispatcherProvider, - hiltComponentBuilder = componentBuilder, - replaceRouter = appRouter.asRouter(), - ) + val appContext = DefaultAppComponentContext( + componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), + messageHandler = uiDependencies.eventMessageHandler, + dispatchers = coroutineDispatcherProvider, + hiltComponentBuilder = componentBuilder, + replaceRouter = appRouter.asRouter(), + ) - marketsEntryComponent = marketsEntryComponentFactory.create(appContext) - } + marketsEntryComponent = marketsEntryComponentFactory.create(appContext) } @Composable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index d08f24beca..a2e325f621 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -42,7 +42,7 @@ internal class DefaultWalletRouter( override fun getEntryFragment(): Fragment = WalletFragment.create() @Composable - override fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent?) { + override fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent) { this.onFinish = onFinish NavHost( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 40ebf62fed..344887162c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -24,7 +24,7 @@ internal interface InnerWalletRouter : WalletRouter { * @param onFinish finish activity callback */ @Composable - fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent?) + fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent) /** Pop back stack */ fun popBackStack() 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 761df41a51..8dc7d7fd8f 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 @@ -45,7 +45,6 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -65,7 +64,6 @@ import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TestTags -import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.core.ui.utils.lineTo import com.tangem.core.ui.utils.moveTo import com.tangem.core.ui.utils.toPx @@ -93,7 +91,7 @@ import kotlinx.coroutines.launch import kotlin.math.roundToInt @Composable -internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent?) { +internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent) { BackHandler(onBack = state.onBackClick) // It means that screen is still initializing @@ -137,7 +135,7 @@ private fun WalletContent( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, - marketsEntryComponent: MarketsEntryComponent?, + marketsEntryComponent: MarketsEntryComponent, alertConfig: WalletAlertState?, onAutoScrollReset: () -> Unit, ) { @@ -239,82 +237,26 @@ private fun WalletContent( ) } - if (marketsEntryComponent != null) { - val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } + val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } - var headerSize by remember { mutableStateOf(0.dp) } + var headerSize by remember { mutableStateOf(0.dp) } - BaseScaffoldWithMarkets( - state = state, - listState = listState, - selectedWallet = selectedWallet, - snackbarHostState = snackbarHostState, - bottomSheetHeaderHeightProvider = { headerSize }, - alertConfig = alertConfig, - onBottomSheetStateChange = { bottomSheetState.value = it }, - bottomSheetContent = { - marketsEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = { headerSize = it }, - modifier = Modifier, - ) - }, - content = scaffoldContent, - ) - } else { - BaseScaffold( - state = state, - selectedWallet = selectedWallet, - snackbarHostState = snackbarHostState, - content = { scaffoldContent(null) }, - ) - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun BaseScaffold( - state: WalletScreenState, - selectedWallet: WalletState, - snackbarHostState: SnackbarHostState, - content: @Composable () -> Unit, -) { - Scaffold( - topBar = { WalletTopBar(config = state.topBarConfig) }, - contentWindowInsets = WindowInsetsZero, - snackbarHost = { - WalletSnackbarHost( - snackbarHostState = snackbarHostState, - event = state.event, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), + BaseScaffoldWithMarkets( + state = state, + listState = listState, + selectedWallet = selectedWallet, + snackbarHostState = snackbarHostState, + bottomSheetHeaderHeightProvider = { headerSize }, + alertConfig = alertConfig, + onBottomSheetStateChange = { bottomSheetState.value = it }, + bottomSheetContent = { + marketsEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = Modifier, ) }, - floatingActionButtonPosition = FabPosition.Center, - containerColor = TangemTheme.colors.background.secondary, - content = { - val pullRefreshState = rememberPullRefreshState( - refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = { - selectedWallet.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) - }, - ) - - Box( - modifier = Modifier - .pullRefresh(pullRefreshState) - .padding(it), - ) { - content() - - WalletPullToRefreshIndicator( - isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) - - BottomFade(Modifier.align(Alignment.BottomCenter)) - } - }, + content = scaffoldContent, ) } @@ -771,7 +713,16 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, - marketsEntryComponent = null, + marketsEntryComponent = object : MarketsEntryComponent { + @Composable + override fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) { + Text("Markets Content") + } + }, ) } } 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 14bb262aa5..fc787ace14 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 @@ -29,7 +29,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents -import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.utils.Provider @@ -68,7 +67,6 @@ internal class WalletViewModel @Inject constructor( private val walletNameMigrationUseCase: WalletNameMigrationUseCase, private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, - private val marketsFeatureToggles: MarketsFeatureToggles, private val walletImageResolver: WalletImageResolver, private val tokenListStore: MultiWalletTokenListStore, analyticsEventsHandler: AnalyticsEventHandler, @@ -130,7 +128,7 @@ internal class WalletViewModel @Inject constructor( viewModelScope.launch { withContext(dispatchers.io) { delay(timeMillis = 1_800) } - if (marketsFeatureToggles.isFeatureEnabled && shouldShowMarketsTooltipUseCase()) { + if (shouldShowMarketsTooltipUseCase()) { stateHolder.update { it.copy(showMarketsOnboarding = true) } From 142ba20e3512d988607798e06309b69ba88539e6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 13:22:03 +0400 Subject: [PATCH 29/41] Updated on 2026-08-14 --- .../core/ui/decorations/RoundedDecorations.kt | 63 ++++++++++--------- .../DefaultSelectTokenComponent.kt | 46 ++++++++++++++ .../selecttoken/SelectTokenComponent.kt | 32 ++++++++++ .../di/SelectTokenComponentModule.kt | 18 ++++++ .../selecttoken/ui/SelectToken.kt | 45 +++++++++++++ .../tokenlist/DefaultTokenListComponent.kt | 5 +- .../tokenlist/TokenListComponent.kt | 15 +++-- .../presentation/tokenlist/ui/TokenList.kt | 11 ++-- .../tokenlist/utils/SearchTokensManager.kt | 6 +- .../multicurrency/MultiCurrencyContent.kt | 1 + .../multicurrency/MultiCurrencyContentItem.kt | 13 +--- 11 files changed, 203 insertions(+), 52 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/DefaultSelectTokenComponent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/SelectTokenComponent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/di/SelectTokenComponentModule.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/ui/SelectToken.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt index dabc6f97e2..02615d9faa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt @@ -1,11 +1,13 @@ package com.tangem.core.ui.decorations +import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import com.tangem.core.ui.res.TangemTheme @@ -15,46 +17,49 @@ fun Modifier.roundedShapeItemDecoration( lastIndex: Int, addDefaultPadding: Boolean = true, radius: Dp = TangemTheme.dimens.radius16, + backgroundColor: Color? = null, ): Modifier = composed { val modifier = if (addDefaultPadding) this.padding(horizontal = TangemTheme.dimens.spacing16) else this + + val applyTopPadding: @Composable Modifier.() -> Modifier = { + if (addDefaultPadding) { + padding(top = TangemTheme.dimens.spacing12) + } else { + this + } + } + + val applyShape: Modifier.(shape: RoundedCornerShape?) -> Modifier = { shape -> + if (backgroundColor != null) { + if (shape != null) { + background(color = backgroundColor, shape = shape) + } else { + background(color = backgroundColor) + } + } else { + if (shape != null) { + clip(shape = shape) + } else { + this + } + } + } + val isSingleItem = currentIndex == 0 && lastIndex == 0 when { isSingleItem -> { modifier - .then( - if (addDefaultPadding) { - Modifier.padding(top = TangemTheme.dimens.spacing12) - } else { - Modifier - }, - ) - .clip(shape = RoundedCornerShape(radius)) + .applyTopPadding() + .applyShape(RoundedCornerShape(radius)) } currentIndex == 0 -> { modifier - .then( - if (addDefaultPadding) { - Modifier.padding(top = TangemTheme.dimens.spacing12) - } else { - Modifier - }, - ) - .clip( - shape = RoundedCornerShape( - topStart = radius, - topEnd = radius, - ), - ) + .applyTopPadding() + .applyShape(RoundedCornerShape(topStart = radius, topEnd = radius)) } currentIndex == lastIndex -> { - modifier - .clip( - shape = RoundedCornerShape( - bottomStart = radius, - bottomEnd = radius, - ), - ) + modifier.applyShape(RoundedCornerShape(bottomStart = radius, bottomEnd = radius)) } - else -> modifier + else -> modifier.applyShape(null) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/DefaultSelectTokenComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/DefaultSelectTokenComponent.kt new file mode 100644 index 0000000000..680be64c0b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/DefaultSelectTokenComponent.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.selecttoken + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.feature.wallet.presentation.selecttoken.ui.SelectToken +import com.tangem.feature.wallet.presentation.tokenlist.TokenListComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultSelectTokenComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + tokenListComponentFactory: TokenListComponent.Factory, + @Assisted private val params: SelectTokenComponent.Params, +) : AppComponentContext by appComponentContext, SelectTokenComponent { + + private val tokenListComponent: TokenListComponent = tokenListComponentFactory.create( + context = child(key = "token_list"), + params = TokenListComponent.Params( + hasSearchBar = params.hasSearchBar, + userWalletId = params.userWalletId, + onTokenClick = params.onTokenClick, + ), + ) + + @Composable + override fun Content(modifier: Modifier) { + SelectToken( + titleResId = params.titleResId, + onBackClick = router::pop, + tokenListComponent = tokenListComponent, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : SelectTokenComponent.Factory { + + override fun create( + context: AppComponentContext, + params: SelectTokenComponent.Params, + ): DefaultSelectTokenComponent + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/SelectTokenComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/SelectTokenComponent.kt new file mode 100644 index 0000000000..ff36917781 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/SelectTokenComponent.kt @@ -0,0 +1,32 @@ +package com.tangem.feature.wallet.presentation.selecttoken + +import androidx.annotation.StringRes +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Select token component + * +[REDACTED_AUTHOR] + */ +interface SelectTokenComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + /** + * Params + * + * @property hasSearchBar flag that indicates if search bar should be shown + * @property userWalletId id of multi-currency wallet + * @property titleResId resource id of title + * @property onTokenClick callback for token click + */ + data class Params( + val hasSearchBar: Boolean, + val userWalletId: UserWalletId, + @StringRes val titleResId: Int, + val onTokenClick: (CryptoCurrencyStatus) -> Unit, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/di/SelectTokenComponentModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/di/SelectTokenComponentModule.kt new file mode 100644 index 0000000000..620bf54e1b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/di/SelectTokenComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.presentation.selecttoken.di + +import com.tangem.feature.wallet.presentation.selecttoken.DefaultSelectTokenComponent +import com.tangem.feature.wallet.presentation.selecttoken.SelectTokenComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface SelectTokenComponentModule { + + @Binds + @Singleton + fun bindSelectTokenComponentFactory(factory: DefaultSelectTokenComponent.Factory): SelectTokenComponent.Factory +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/ui/SelectToken.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/ui/SelectToken.kt new file mode 100644 index 0000000000..56c6872198 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/ui/SelectToken.kt @@ -0,0 +1,45 @@ +package com.tangem.feature.wallet.presentation.selecttoken.ui + +import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.tokenlist.TokenListComponent + +@Composable +internal fun SelectToken( + @StringRes titleResId: Int, + onBackClick: () -> Unit, + tokenListComponent: TokenListComponent, + modifier: Modifier = Modifier, +) { + BackHandler(onBack = onBackClick) + + Column( + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .imePadding() + .systemBarsPadding(), + ) { + AppBarWithBackButton( + onBackClick = onBackClick, + text = stringResource(id = titleResId), + iconRes = R.drawable.ic_close_24, + ) + + tokenListComponent.Content( + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp), + modifier = Modifier, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt index c74231c846..86dfa0a67f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.tokenlist +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -22,10 +23,10 @@ internal class DefaultTokenListComponent @AssistedInject constructor( private val model: TokenListModel = getOrCreateModel(params) @Composable - override fun Content(modifier: Modifier) { + override fun Content(contentPadding: PaddingValues, modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() - TokenList(state = state, modifier = modifier) + TokenList(state = state, contentPadding = contentPadding, modifier = modifier) } @AssistedFactory diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt index 3c271c0894..4bb85444e3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt @@ -1,14 +1,19 @@ package com.tangem.feature.wallet.presentation.tokenlist +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId /** Token list component that present list of token for multi-currency wallet */ @Stable -internal interface TokenListComponent : ComposableContentComponent { +internal interface TokenListComponent { + + @Composable + fun Content(contentPadding: PaddingValues, modifier: Modifier) /** Component factory */ interface Factory : ComponentFactory @@ -16,9 +21,9 @@ internal interface TokenListComponent : ComposableContentComponent { /** * Params * - * @property hasSearchBar flag that indicates if search bar should be shown - * @property userWalletId id of multi-currency wallet - * @property onTokenClick callback for token click + * @property hasSearchBar flag that indicates if search bar should be shown + * @property userWalletId id of multi-currency wallet + * @property onTokenClick callback for token click */ data class Params( val hasSearchBar: Boolean, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt index e780c03e44..68a102e845 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt @@ -1,8 +1,8 @@ package com.tangem.feature.wallet.presentation.tokenlist.ui import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable @@ -26,8 +26,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency [REDACTED_AUTHOR] */ @Composable -internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { - LazyColumn(modifier = modifier) { +internal fun TokenList(state: TokenListUM, contentPadding: PaddingValues, modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier, contentPadding = contentPadding) { itemsIndexed( items = state.items, key = { _, item -> item.id }, @@ -42,6 +42,7 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { currentIndex = index, lastIndex = state.items.lastIndex, addDefaultPadding = false, + backgroundColor = TangemTheme.colors.background.primary, ), ) }, @@ -55,10 +56,10 @@ private fun Preview_TokenList(@PreviewParameter(PreviewTokenListUMProvider::clas TangemThemePreview { TokenList( state = state, + contentPadding = PaddingValues(all = 16.dp), modifier = Modifier .fillMaxWidth() - .background(color = TangemTheme.colors.background.secondary) - .padding(16.dp), + .background(color = TangemTheme.colors.background.secondary), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt index 98258cca19..ff57e2818c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt @@ -23,7 +23,11 @@ internal class SearchTokensManager @Inject constructor() { suspend fun update(value: String) { coroutineScope { - withDebounce(jobHolder) { _query.value = value } + if (value.isEmpty()) { + _query.value = value + } else { + withDebounce(jobHolder) { _query.value = value } + } } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index e0563a4400..14c3dc4b41 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -64,6 +64,7 @@ private fun LazyListScope.contentItems( modifier = modifier.roundedShapeItemDecoration( currentIndex = index, lastIndex = items.lastIndex, + backgroundColor = TangemTheme.colors.background.primary, ), ) }, 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 0b8e4ea13b..7f26ab0399 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,6 +1,5 @@ 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 @@ -8,7 +7,6 @@ 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 import com.tangem.feature.wallet.presentation.common.component.NetworkTitleItem import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState @@ -26,24 +24,19 @@ internal fun MultiCurrencyContentItem( isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { - val modifierWithBackground = modifier.background(color = TangemTheme.colors.background.primary) - when (state) { is TokensListItemState.NetworkGroupTitle -> { - NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifierWithBackground) + NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifier) } is TokensListItemState.Token -> { TokenItem( state = state.state, isBalanceHidden = isBalanceHidden, - modifier = modifierWithBackground, + modifier = modifier, ) } is TokensListItemState.SearchBar -> { - SearchBar( - state = state.searchBarUM, - modifier = modifierWithBackground.padding(all = 12.dp), - ) + SearchBar(state = state.searchBarUM, modifier = modifier.padding(all = 12.dp)) } } } \ No newline at end of file From 9230cc46946f8bf2b11149c8e64c09c64daf31cb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 14:08:36 +0300 Subject: [PATCH 30/41] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 4 ++ features/onboarding-legacy/.gitignore | 1 + features/onboarding-legacy/build.gradle.kts | 64 +++++++++++++++++++ .../onboarding/legacy/di/FeatureModule.kt | 18 ++++++ .../DefaultOnboardingLegacyFeatureToggles.kt | 11 ++++ .../OnboardingLegacyFeatureToggles.kt | 5 ++ settings.gradle.kts | 1 + 7 files changed, 104 insertions(+) create mode 100644 features/onboarding-legacy/.gitignore create mode 100644 features/onboarding-legacy/build.gradle.kts create mode 100644 features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/di/FeatureModule.kt create mode 100644 features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/DefaultOnboardingLegacyFeatureToggles.kt create mode 100644 features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/OnboardingLegacyFeatureToggles.kt 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 3c29124728..5191b7778d 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -30,5 +30,9 @@ { "name": "MAIN_ACTION_BUTTONS_ENABLED", "version": "undefined" + }, + { + "name": "ONBOARDING_CODE_REFACTORING_ENABLED", + "version": "undefined" } ] diff --git a/features/onboarding-legacy/.gitignore b/features/onboarding-legacy/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/onboarding-legacy/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/onboarding-legacy/build.gradle.kts b/features/onboarding-legacy/build.gradle.kts new file mode 100644 index 0000000000..d82dd86a7d --- /dev/null +++ b/features/onboarding-legacy/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.feature.onboarding.legacy" +} + +dependencies { + /** Core modules */ + implementation(project(":common")) + implementation(project(":domain:models")) + implementation(project(":core:featuretoggles")) + implementation(project(":core:datasource")) + implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) + implementation(project(":core:utils")) + implementation(project(":core:ui")) + implementation(project(":core:res")) + implementation(project(":data:common")) + + /** Tangem libraries */ + implementation(deps.tangem.card.core) + implementation(deps.tangem.card.android) { + exclude(module = "joda-time") + } + + /** AndroidX libraries */ + implementation(deps.androidx.core.ktx) + implementation(deps.androidx.appCompat) + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.constraintLayout) + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.runtime.ktx) + implementation(deps.lifecycle.common.java8) + implementation(deps.lifecycle.viewModel.ktx) + + /** Compose libraries */ + implementation(deps.compose.material) + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.compose.shimmer) + + implementation(deps.tangem.card.core) + + /** Other libraries */ + implementation(deps.compose.shimmer) + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/di/FeatureModule.kt b/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/di/FeatureModule.kt new file mode 100644 index 0000000000..58798b2583 --- /dev/null +++ b/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/di/FeatureModule.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.onboarding.legacy.di + +import com.tangem.feature.onboarding.legacy.featuretoggle.DefaultOnboardingLegacyFeatureToggles +import com.tangem.feature.onboarding.legacy.featuretoggle.OnboardingLegacyFeatureToggles +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface FeatureModule { + + @Singleton + @Binds + fun provideFeatureToggles(featureToggles: DefaultOnboardingLegacyFeatureToggles): OnboardingLegacyFeatureToggles +} \ No newline at end of file diff --git a/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/DefaultOnboardingLegacyFeatureToggles.kt b/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/DefaultOnboardingLegacyFeatureToggles.kt new file mode 100644 index 0000000000..d46fdf1c92 --- /dev/null +++ b/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/DefaultOnboardingLegacyFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.onboarding.legacy.featuretoggle + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import javax.inject.Inject + +internal class DefaultOnboardingLegacyFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) : OnboardingLegacyFeatureToggles { + override val isRewrittenOnboardingCodeInUse: Boolean + get() = featureTogglesManager.isFeatureEnabled("ONBOARDING_CODE_REFACTORING_ENABLED") +} \ No newline at end of file diff --git a/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/OnboardingLegacyFeatureToggles.kt b/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/OnboardingLegacyFeatureToggles.kt new file mode 100644 index 0000000000..aa692ec61e --- /dev/null +++ b/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/OnboardingLegacyFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.feature.onboarding.legacy.featuretoggle + +interface OnboardingLegacyFeatureToggles { + val isRewrittenOnboardingCodeInUse: Boolean +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f3b70b2d8b..2876b342f6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -151,6 +151,7 @@ include(":libs:visa") // region Feature modules include(":features:onboarding") +include(":features:onboarding-legacy") include(":features:referral:data") include(":features:referral:domain") From 41131e5b73af4134e5d6fa535af09e987a59beec Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 15:36:35 +0400 Subject: [PATCH 31/41] Updated on 2026-08-14 --- .../ui/components/tokenlist/TokenListItem.kt | 23 +++++----- .../tokenlist/internal}/NetworkTitleItem.kt | 19 +++++++-- .../tokenlist/state/TokensListItemUM.kt | 42 +++++++++++++++++++ .../common/preview/WalletScreenPreviewData.kt | 17 ++++---- .../organizetokens/OrganizeTokensScreen.kt | 2 +- .../tokenlist/entity/TokenListUM.kt | 10 ++--- .../tokenlist/entity/TokenListUMController.kt | 4 +- .../UpdateTokenItemsTransformer.kt | 8 ++-- .../presentation/tokenlist/ui/TokenList.kt | 4 +- .../ui/preview/PreviewTokenListUMProvider.kt | 10 ++--- .../state/model/WalletTokensListState.kt | 29 +++---------- .../converter/TokenListStateConverter.kt | 14 +++---- .../multicurrency/MultiCurrencyContent.kt | 11 +++-- 13 files changed, 114 insertions(+), 79 deletions(-) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt => core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt (56%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component => core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal}/NetworkTitleItem.kt (89%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt similarity index 56% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index 7f26ab0399..56b0ce8bf1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency +package com.tangem.core.ui.components.tokenlist import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -6,36 +6,33 @@ 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.components.tokenlist.internal.NetworkTitleItem +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resolveReference -import com.tangem.feature.wallet.presentation.common.component.NetworkTitleItem -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState /** * Multi-currency content item * - * @param state item state - * @param modifier modifier + * @param state component UI model + * @param isBalanceHidden flag that shows/hides balance + * @param modifier modifier * [REDACTED_AUTHOR] */ @Composable -internal fun MultiCurrencyContentItem( - state: TokensListItemState, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { +fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { when (state) { - is TokensListItemState.NetworkGroupTitle -> { + is TokensListItemUM.NetworkGroupTitle -> { NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifier) } - is TokensListItemState.Token -> { + is TokensListItemUM.Token -> { TokenItem( state = state.state, isBalanceHidden = isBalanceHidden, modifier = modifier, ) } - is TokensListItemState.SearchBar -> { + is TokensListItemUM.SearchBar -> { SearchBar(state = state.searchBarUM, modifier = modifier.padding(all = 12.dp)) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkTitleItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt similarity index 89% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkTitleItem.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt index 5bd9e5e129..4f6bf25949 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkTitleItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.common.component +package com.tangem.core.ui.components.tokenlist.internal import android.content.res.Configuration import androidx.compose.foundation.background @@ -17,21 +17,34 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.R import com.tangem.core.ui.components.rows.NetworkTitle import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.impl.R import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.detectReorder import org.burnoutcrew.reorderable.rememberReorderableLazyListState +/** + * Network title item + * + * @param networkName network name + * @param modifier modifier + */ @Composable internal fun NetworkTitleItem(networkName: String, modifier: Modifier = Modifier) { BaseNetworkTitleItem(networkName = networkName, modifier = modifier) } +/** + * Draggable network title item + * + * @param networkName network name + * @param reorderableTokenListState reorderable token list state + * @param modifier modifier + */ @Composable -internal fun DraggableNetworkTitleItem( +fun DraggableNetworkTitleItem( networkName: String, reorderableTokenListState: ReorderableLazyListState, modifier: Modifier = Modifier, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt new file mode 100644 index 0000000000..8a26f00508 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt @@ -0,0 +1,42 @@ +package com.tangem.core.ui.components.tokenlist.state + +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 + +/** Tokens list item state */ +@Immutable +sealed interface TokensListItemUM { + + /** Unique ID */ + val id: Any + + /** + * Search bar item + * + * @property id id + * @property searchBarUM search bar UI model + */ + data class SearchBar( + override val id: Any = "search_bar", + val searchBarUM: SearchBarUM, + ) : TokensListItemUM + + /** + * Network group title + * + * @property id id + * @property name network group name + */ + data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemUM + + /** + * Token item + * + * @property state token state + */ + data class Token(val state: TokenItemState) : TokensListItemUM { + override val id: String = state.id + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 3dbc17ce2f..a871134063 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -3,15 +3,16 @@ package com.tangem.feature.wallet.presentation.common.preview import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf internal object WalletScreenPreviewData { @@ -32,16 +33,16 @@ internal object WalletScreenPreviewData { private val textContentTokensState = WalletTokensListState.ContentState.Content( items = persistentListOf( - WalletTokensListState.TokensListItemState.NetworkGroupTitle( + TokensListItemUM.NetworkGroupTitle( id = 1, name = stringReference("Bitcoin"), ), - WalletTokensListState.TokensListItemState.Token(state = tokenItemState), - WalletTokensListState.TokensListItemState.NetworkGroupTitle( + TokensListItemUM.Token(state = tokenItemState), + TokensListItemUM.NetworkGroupTitle( id = 2, name = stringReference("Ethereum"), ), - WalletTokensListState.TokensListItemState.Token( + TokensListItemUM.Token( state = tokenItemState.copy( id = "2", titleState = TokenItemState.TitleState.Content(text = "Ethereum"), @@ -54,7 +55,7 @@ internal object WalletScreenPreviewData { ), ), ), - WalletTokensListState.TokensListItemState.Token( + TokensListItemUM.Token( state = TokenItemState.Unreachable( id = "3", iconState = CurrencyIconState.Locked, @@ -63,7 +64,7 @@ internal object WalletScreenPreviewData { onItemLongClick = {}, ), ), - WalletTokensListState.TokensListItemState.Token( + TokensListItemUM.Token( state = tokenItemState.copy( id = "4", titleState = TokenItemState.TitleState.Content(text = "Shiba Inu"), @@ -106,7 +107,7 @@ internal object WalletScreenPreviewData { ), imageResId = R.drawable.ill_wallet2_cards3_120_106, cardCount = 3, - balance = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + balance = DASH_SIGN, onRenameClick = { _ -> }, onDeleteClick = {}, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 17dca9d4fe..15a4f2250e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -32,6 +32,7 @@ import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.tokenlist.internal.DraggableNetworkTitleItem import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.reordarable.ReorderableItem @@ -40,7 +41,6 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkTitleItem import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState 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 index 83d5fc1640..c86dc9a2f9 100644 --- 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 @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.tokenlist.entity -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -13,17 +13,17 @@ import kotlinx.collections.immutable.persistentListOf [REDACTED_AUTHOR] */ internal data class TokenListUM( - val items: ImmutableList, + val items: ImmutableList, val isBalanceHidden: Boolean, ) { /** Get search bar if it exists */ - fun getSearchBar(): TokensListItemState.SearchBar? { - return items.firstOrNull() as? TokensListItemState.SearchBar + fun getSearchBar(): TokensListItemUM.SearchBar? { + return items.firstOrNull() as? TokensListItemUM.SearchBar } /** Get tokens */ - fun getTokens(): ImmutableList { + fun getTokens(): ImmutableList { if (getSearchBar() == null) return items return if (items.size > 1) { 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 index 04592b5ed0..43c5bb75b8 100644 --- 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 @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.tokenlist.entity -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -35,7 +35,7 @@ internal class TokenListUMController @Inject constructor() { } /** Get search bar if it exists */ - fun getSearchBar(): TokensListItemState.SearchBar? { + fun getSearchBar(): TokensListItemUM.SearchBar? { return _state.value.getSearchBar() } } \ 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 index ed4dcf5c9e..0fe02a9f42 100644 --- 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 @@ -2,12 +2,12 @@ package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.impl.R 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( @@ -20,7 +20,7 @@ internal class UpdateTokenItemsTransformer( ) : TokenListUMTransformer { override fun transform(prevState: TokenListUM): TokenListUM { - val items = tokenItemStateConverter.convertList(input = statuses).map(TokensListItemState::Token) + val items = tokenItemStateConverter.convertList(input = statuses).map(TokensListItemUM::Token) val searchBarItem = if (hasSearchBar) { prevState.getSearchBar() ?: createSearchBarItem() @@ -34,8 +34,8 @@ internal class UpdateTokenItemsTransformer( ) } - private fun createSearchBarItem(): TokensListItemState.SearchBar { - return TokensListItemState.SearchBar( + private fun createSearchBarItem(): TokensListItemUM.SearchBar { + return TokensListItemUM.SearchBar( searchBarUM = SearchBarUM( placeholderText = resourceReference(id = R.string.common_search), query = "", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt index 68a102e845..b8fcb55c6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt @@ -10,12 +10,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM import com.tangem.feature.wallet.presentation.tokenlist.ui.preview.PreviewTokenListUMProvider -import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.MultiCurrencyContentItem /** * Token list @@ -33,7 +33,7 @@ internal fun TokenList(state: TokenListUM, contentPadding: PaddingValues, modifi key = { _, item -> item.id }, contentType = { _, item -> item::class.java }, itemContent = { index, item -> - MultiCurrencyContentItem( + TokenListItem( state = item, isBalanceHidden = state.isBalanceHidden, modifier = Modifier diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt index 4a0b5ad769..1b735ea3c8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt @@ -5,10 +5,10 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState import kotlinx.collections.immutable.toImmutableList internal class PreviewTokenListUMProvider : PreviewParameterProvider { @@ -18,11 +18,11 @@ internal class PreviewTokenListUMProvider : PreviewParameterProvider + abstract val items: ImmutableList abstract val organizeTokensButtonConfig: OrganizeTokensButtonConfig? data object Loading : ContentState() { - override val items = persistentListOf() + override val items = persistentListOf() override val organizeTokensButtonConfig = null } data class Content( - override val items: ImmutableList, + override val items: ImmutableList, override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?, ) : ContentState() data object Locked : ContentState() { override val items = persistentListOf( - TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), - TokensListItemState.Token(state = TokenItemState.Locked(id = "Locked#1")), + TokensListItemUM.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), + TokensListItemUM.Token(state = TokenItemState.Locked(id = "Locked#1")), ) override val organizeTokensButtonConfig = null } } data class OrganizeTokensButtonConfig(val isEnabled: Boolean, val onClick: () -> Unit) - - @Immutable - sealed class TokensListItemState { - - 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() { - override val id: String = state.id - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 4ae09b322f..668118d2fb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.cardTypesResolver @@ -10,7 +11,6 @@ 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.state.model.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList @@ -47,20 +47,20 @@ internal class TokenListStateConverter( } } - private fun TokenList.GroupedByNetwork.toGroupedItems(): PersistentList { + private fun TokenList.GroupedByNetwork.toGroupedItems(): PersistentList { return groups.fold(initial = persistentListOf()) { acc, group -> acc.mutate { it.addGroup(group) } } } - private fun TokenList.Ungrouped.toUngroupedItems(): PersistentList { + private fun TokenList.Ungrouped.toUngroupedItems(): PersistentList { return currencies.fold(initial = persistentListOf()) { acc, token -> acc.mutate { it.addToken(token) } } } - private fun MutableList.addGroup(group: NetworkGroup): List { - val groupTitle = TokensListItemState.NetworkGroupTitle( + private fun MutableList.addGroup(group: NetworkGroup): List { + val groupTitle = TokensListItemUM.NetworkGroupTitle( id = group.network.hashCode(), name = stringReference(group.network.name), ) @@ -71,9 +71,9 @@ internal class TokenListStateConverter( return this } - private fun MutableList.addToken(token: CryptoCurrencyStatus): List { + private fun MutableList.addToken(token: CryptoCurrencyStatus): List { val tokenItemState = tokenStatusConverter.convert(token) - add(TokensListItemState.Token(tokenItemState)) + add(TokensListItemUM.Token(tokenItemState)) return this } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 14c3dc4b41..49c11b5799 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -14,11 +13,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.tokenlist.TokenListItem +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState import kotlinx.collections.immutable.ImmutableList private const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" @@ -49,7 +49,7 @@ internal fun LazyListScope.tokensListItems( } private fun LazyListScope.contentItems( - items: ImmutableList, + items: ImmutableList, modifier: Modifier = Modifier, isBalanceHidden: Boolean, ) { @@ -58,7 +58,7 @@ private fun LazyListScope.contentItems( key = { _, item -> item.id }, contentType = { _, item -> item::class.java }, itemContent = { index, item -> - MultiCurrencyContentItem( + TokenListItem( state = item, isBalanceHidden = isBalanceHidden, modifier = modifier.roundedShapeItemDecoration( @@ -71,7 +71,6 @@ private fun LazyListScope.contentItems( ) } -@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { item( key = NON_CONTENT_TOKENS_LIST_KEY, @@ -79,7 +78,7 @@ private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { ) { Column( modifier = modifier - .animateItemPlacement() + .animateItem() .padding(top = TangemTheme.dimens.spacing96), verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), horizontalAlignment = Alignment.CenterHorizontally, From 6ba6f5b34830585bf47f03301955e579edb55fdb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 16:52:58 +0400 Subject: [PATCH 32/41] Updated on 2026-08-14 --- .../kotlin/com/tangem/domain/core/lce/LceFlow.kt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) 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 037775753e..65e29bca2b 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 @@ -6,7 +6,6 @@ import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow @@ -38,13 +37,9 @@ class LceFlowScope @PublishedApi internal constructor( 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. + * Sends an error of type [E] within the [ProducerScope] without closing. * - * This method blocks the coroutine until a error is handled by the receiver. - * - * If the [ProducerScope] is already closed for send (e.g. after rising another error), it just raises [r] - * without closing. + * This method blocks the coroutine until an error is handled by the receiver. * * @param r Error to raise. */ @@ -52,7 +47,6 @@ class LceFlowScope @PublishedApi internal constructor( isLoading.set(false) producerScope.trySendBlocking(r.lceError()) - producerScope.close() raise.raise(r.lceError()) } @@ -88,14 +82,9 @@ class LceFlowScope @PublishedApi internal constructor( * * This method suspends until the [Lce] instance is handled by the receiver. * - * If the [ProducerScope] is closed for send (e.g. after rising a error), it does nothing. - * * @param value The [Lce] instance to send. */ - @OptIn(DelicateCoroutinesApi::class) suspend fun send(value: Lce) { - if (producerScope.isClosedForSend) return - isLoading.set(value.isLoading()) producerScope.send(value) From 5e8273c248a2d53a94db73cbe705b2ec2e85ba3c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 16:54:23 +0400 Subject: [PATCH 33/41] Updated on 2026-08-14 --- .../main/kotlin/com/tangem/domain/tokens/model/TokenList.kt | 2 +- .../tokens/operations/CurrenciesStatusesLceOperations.kt | 3 +-- 2 files changed, 2 insertions(+), 3 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 f5eaf0b0e3..2bd9431825 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 @@ -42,7 +42,7 @@ sealed class TokenList { ) : TokenList() /** Represents a state where the token list is empty. */ - object Empty : TokenList() { + data object Empty : TokenList() { override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded( amount = BigDecimal.ZERO, 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 cc96309f1f..f56873489c 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 @@ -75,10 +75,9 @@ internal class CurrenciesStatusesLceOperations( ::createCurrenciesStatuses, ) .distinctUntilChanged() - .mapLatest { maybeCurrenciesStatuses -> + .collectLatest { maybeCurrenciesStatuses -> send(maybeCurrenciesStatuses) } - .launchIn(scope = this) } } From f5eca5ab2a82c309638fd6362f2dfeee840e2c62 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 16:52:05 +0400 Subject: [PATCH 34/41] Updated on 2026-08-14 --- features/onramp/impl/build.gradle.kts | 15 ++++++ .../DefaultOnrampSelectTokenComponent.kt | 46 +++++++++++++++++++ .../OnrampSelectTokenComponent.kt} | 6 +-- .../di/OnrampSelectTokenComponentModule.kt | 20 ++++++++ .../selecttoken/ui/OnrampSelectToken.kt} | 12 ++--- .../DefaultOnrampTokenListComponent.kt} | 21 +++++---- .../tokenlist/OnrampTokenListComponent.kt} | 6 +-- .../di/OnrampTokenListComponentModule.kt | 20 ++++++++ .../di/OnrampTokenListModelModule.kt} | 10 ++-- .../onramp}/tokenlist/entity/TokenListUM.kt | 2 +- .../tokenlist/entity/TokenListUMController.kt | 2 +- .../entity/TokenListUMTransformer.kt | 2 +- .../transformer/SearchBarUMTransformer.kt | 6 +-- .../UpdateSearchBarActiveStateTransformer.kt | 4 +- .../UpdateSearchQueryTransformer.kt | 2 +- .../UpdateTokenItemsTransformer.kt | 8 ++-- .../tokenlist/model/OnrampTokenListModel.kt} | 20 ++++---- .../onramp/tokenlist/ui/OnrampTokenList.kt} | 6 +-- .../ui/preview/PreviewTokenListUMProvider.kt | 6 +-- .../tokenlist/utils/SearchTokensManager.kt | 2 +- .../DefaultSelectTokenComponent.kt | 46 ------------------- .../di/SelectTokenComponentModule.kt | 18 -------- .../tokenlist/di/TokenListComponentModule.kt | 18 -------- 23 files changed, 160 insertions(+), 138 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampSelectTokenComponent.kt rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/SelectTokenComponent.kt => onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampSelectTokenComponent.kt} (80%) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/di/OnrampSelectTokenComponentModule.kt rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/ui/SelectToken.kt => onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt} (81%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt => onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt} (55%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt => onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt} (85%) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListComponentModule.kt rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListModelModule.kt => onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListModelModule.kt} (53%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation => onramp/impl/src/main/kotlin/com/tangem/features/onramp}/tokenlist/entity/TokenListUM.kt (93%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation => onramp/impl/src/main/kotlin/com/tangem/features/onramp}/tokenlist/entity/TokenListUMController.kt (94%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation => onramp/impl/src/main/kotlin/com/tangem/features/onramp}/tokenlist/entity/TokenListUMTransformer.kt (73%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation => onramp/impl/src/main/kotlin/com/tangem/features/onramp}/tokenlist/entity/transformer/SearchBarUMTransformer.kt (78%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation => onramp/impl/src/main/kotlin/com/tangem/features/onramp}/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt (84%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation => onramp/impl/src/main/kotlin/com/tangem/features/onramp}/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt (79%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation => onramp/impl/src/main/kotlin/com/tangem/features/onramp}/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt (85%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/model/TokenListModel.kt => onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt} (82%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt => onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt} (90%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation => onramp/impl/src/main/kotlin/com/tangem/features/onramp}/tokenlist/ui/preview/PreviewTokenListUMProvider.kt (93%) rename features/{wallet/impl/src/main/java/com/tangem/feature/wallet/presentation => onramp/impl/src/main/kotlin/com/tangem/features/onramp}/tokenlist/utils/SearchTokensManager.kt (92%) delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/DefaultSelectTokenComponent.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/di/SelectTokenComponentModule.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListComponentModule.kt diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 94b9c2785a..8de571b7a7 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -20,8 +20,21 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.featuretoggles) + /** Project - Common */ + implementation(projects.common.ui) + + /** Project - Domain */ + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + /** DI */ implementation(deps.hilt.android) + implementation(project(":common:ui")) kapt(deps.hilt.kapt) /** AndroidX */ @@ -40,4 +53,6 @@ dependencies { /** Other */ implementation(deps.decompose.ext.compose) + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampSelectTokenComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampSelectTokenComponent.kt new file mode 100644 index 0000000000..50ec027209 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampSelectTokenComponent.kt @@ -0,0 +1,46 @@ +package com.tangem.features.onramp.selecttoken + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.features.onramp.selecttoken.ui.OnrampSelectToken +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnrampSelectTokenComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + onrampTokenListComponentFactory: OnrampTokenListComponent.Factory, + @Assisted private val params: OnrampSelectTokenComponent.Params, +) : AppComponentContext by appComponentContext, OnrampSelectTokenComponent { + + private val onrampTokenListComponent: OnrampTokenListComponent = onrampTokenListComponentFactory.create( + context = child(key = "token_list"), + params = OnrampTokenListComponent.Params( + hasSearchBar = params.hasSearchBar, + userWalletId = params.userWalletId, + onTokenClick = params.onTokenClick, + ), + ) + + @Composable + override fun Content(modifier: Modifier) { + OnrampSelectToken( + titleResId = params.titleResId, + onBackClick = router::pop, + onrampTokenListComponent = onrampTokenListComponent, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : OnrampSelectTokenComponent.Factory { + + override fun create( + context: AppComponentContext, + params: OnrampSelectTokenComponent.Params, + ): DefaultOnrampSelectTokenComponent + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/SelectTokenComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampSelectTokenComponent.kt similarity index 80% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/SelectTokenComponent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampSelectTokenComponent.kt index ff36917781..69aa520462 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/SelectTokenComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampSelectTokenComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.selecttoken +package com.tangem.features.onramp.selecttoken import androidx.annotation.StringRes import com.tangem.core.decompose.factory.ComponentFactory @@ -11,9 +11,9 @@ import com.tangem.domain.wallets.models.UserWalletId * [REDACTED_AUTHOR] */ -interface SelectTokenComponent : ComposableContentComponent { +interface OnrampSelectTokenComponent : ComposableContentComponent { - interface Factory : ComponentFactory + interface Factory : ComponentFactory /** * Params diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/di/OnrampSelectTokenComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/di/OnrampSelectTokenComponentModule.kt new file mode 100644 index 0000000000..01bece010d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/di/OnrampSelectTokenComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.selecttoken.di + +import com.tangem.features.onramp.selecttoken.DefaultOnrampSelectTokenComponent +import com.tangem.features.onramp.selecttoken.OnrampSelectTokenComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampSelectTokenComponentModule { + + @Binds + @Singleton + fun bindOnrampSelectTokenComponentFactory( + factory: DefaultOnrampSelectTokenComponent.Factory, + ): OnrampSelectTokenComponent.Factory +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/ui/SelectToken.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt similarity index 81% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/ui/SelectToken.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt index 56c6872198..653883fb90 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/ui/SelectToken.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.selecttoken.ui +package com.tangem.features.onramp.selecttoken.ui import androidx.activity.compose.BackHandler import androidx.annotation.StringRes @@ -13,14 +13,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.tokenlist.TokenListComponent +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent @Composable -internal fun SelectToken( +internal fun OnrampSelectToken( @StringRes titleResId: Int, onBackClick: () -> Unit, - tokenListComponent: TokenListComponent, + onrampTokenListComponent: OnrampTokenListComponent, modifier: Modifier = Modifier, ) { BackHandler(onBack = onBackClick) @@ -37,7 +37,7 @@ internal fun SelectToken( iconRes = R.drawable.ic_close_24, ) - tokenListComponent.Content( + onrampTokenListComponent.Content( contentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp), modifier = Modifier, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt similarity index 55% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt index 86dfa0a67f..67ada9b94b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/DefaultTokenListComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.tokenlist +package com.tangem.features.onramp.tokenlist import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable @@ -8,19 +8,19 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.feature.wallet.presentation.tokenlist.model.TokenListModel -import com.tangem.feature.wallet.presentation.tokenlist.ui.TokenList +import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel +import com.tangem.features.onramp.tokenlist.ui.TokenList import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @Stable -internal class DefaultTokenListComponent @AssistedInject constructor( +internal class DefaultOnrampTokenListComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted params: TokenListComponent.Params, -) : TokenListComponent, AppComponentContext by context { + @Assisted params: OnrampTokenListComponent.Params, +) : OnrampTokenListComponent, AppComponentContext by context { - private val model: TokenListModel = getOrCreateModel(params) + private val model: OnrampTokenListModel = getOrCreateModel(params) @Composable override fun Content(contentPadding: PaddingValues, modifier: Modifier) { @@ -30,7 +30,10 @@ internal class DefaultTokenListComponent @AssistedInject constructor( } @AssistedFactory - interface Factory : TokenListComponent.Factory { - override fun create(context: AppComponentContext, params: TokenListComponent.Params): DefaultTokenListComponent + interface Factory : OnrampTokenListComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnrampTokenListComponent.Params, + ): DefaultOnrampTokenListComponent } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt similarity index 85% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt index 4bb85444e3..8b924c893e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/TokenListComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.tokenlist +package com.tangem.features.onramp.tokenlist import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable @@ -10,13 +10,13 @@ import com.tangem.domain.wallets.models.UserWalletId /** Token list component that present list of token for multi-currency wallet */ @Stable -internal interface TokenListComponent { +internal interface OnrampTokenListComponent { @Composable fun Content(contentPadding: PaddingValues, modifier: Modifier) /** Component factory */ - interface Factory : ComponentFactory + interface Factory : ComponentFactory /** * Params diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListComponentModule.kt new file mode 100644 index 0000000000..913817a3e6 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.tokenlist.di + +import com.tangem.features.onramp.tokenlist.DefaultOnrampTokenListComponent +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampTokenListComponentModule { + + @Binds + @Singleton + fun bindOnrampTokenListComponentFactory( + factory: DefaultOnrampTokenListComponent.Factory, + ): OnrampTokenListComponent.Factory +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListModelModule.kt similarity index 53% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListModelModule.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListModelModule.kt index 732340f818..7977384f55 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListModelModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListModelModule.kt @@ -1,8 +1,8 @@ -package com.tangem.feature.wallet.presentation.tokenlist.di +package com.tangem.features.onramp.tokenlist.di import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.decompose.model.Model -import com.tangem.feature.wallet.presentation.tokenlist.model.TokenListModel +import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -11,10 +11,10 @@ import dagger.multibindings.IntoMap @Module @InstallIn(DecomposeComponent::class) -internal interface TokenListModelModule { +internal interface OnrampTokenListModelModule { @Binds @IntoMap - @ClassKey(TokenListModel::class) - fun provideTokenListModel(model: TokenListModel): Model + @ClassKey(OnrampTokenListModel::class) + fun bindOnrampTokenListModel(model: OnrampTokenListModel): Model } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt similarity index 93% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUM.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt index c86dc9a2f9..bb9e8f3562 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.tokenlist.entity +package com.tangem.features.onramp.tokenlist.entity import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import kotlinx.collections.immutable.ImmutableList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt similarity index 94% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMController.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt index 43c5bb75b8..bb662a5b7a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMController.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.tokenlist.entity +package com.tangem.features.onramp.tokenlist.entity import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import kotlinx.collections.immutable.persistentListOf diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMTransformer.kt similarity index 73% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMTransformer.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMTransformer.kt index 49678c022f..4ffbf26c31 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/TokenListUMTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMTransformer.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.tokenlist.entity +package com.tangem.features.onramp.tokenlist.entity import com.tangem.utils.transformer.Transformer diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/SearchBarUMTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SearchBarUMTransformer.kt similarity index 78% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/SearchBarUMTransformer.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SearchBarUMTransformer.kt index a177307b2c..db17cb19ea 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/SearchBarUMTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SearchBarUMTransformer.kt @@ -1,8 +1,8 @@ -package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer +package com.tangem.features.onramp.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 com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import kotlinx.collections.immutable.persistentListOf /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt similarity index 84% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt index f33d3ea4ee..cf69073114 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt @@ -1,9 +1,9 @@ -package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer +package com.tangem.features.onramp.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 +import com.tangem.features.onramp.impl.R internal class UpdateSearchBarActiveStateTransformer(private val isActive: Boolean) : SearchBarUMTransformer() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt similarity index 79% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt index 93a6a20675..3e463d159a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer +package com.tangem.features.onramp.tokenlist.entity.transformer import com.tangem.core.ui.components.fields.entity.SearchBarUM diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt similarity index 85% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt index 0fe02a9f42..b6065912be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt @@ -1,13 +1,13 @@ -package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer +package com.tangem.features.onramp.tokenlist.entity.transformer import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM -import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUMTransformer +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import kotlinx.collections.immutable.toImmutableList internal class UpdateTokenItemsTransformer( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/model/TokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt similarity index 82% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/model/TokenListModel.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index fb13ef8d9d..efbb04ac25 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/model/TokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.tokenlist.model +package com.tangem.features.onramp.tokenlist.model import arrow.core.getOrElse import com.tangem.common.ui.tokens.TokenItemStateConverter @@ -11,20 +11,20 @@ import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList -import com.tangem.feature.wallet.presentation.tokenlist.TokenListComponent -import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM -import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUMController -import com.tangem.feature.wallet.presentation.tokenlist.entity.transformer.UpdateSearchBarActiveStateTransformer -import com.tangem.feature.wallet.presentation.tokenlist.entity.transformer.UpdateSearchQueryTransformer -import com.tangem.feature.wallet.presentation.tokenlist.entity.transformer.UpdateTokenItemsTransformer -import com.tangem.feature.wallet.presentation.tokenlist.utils.SearchTokensManager +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMController +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateSearchBarActiveStateTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateSearchQueryTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer +import com.tangem.features.onramp.tokenlist.utils.SearchTokensManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @Suppress("LongParameterList") -internal class TokenListModel @Inject constructor( +internal class OnrampTokenListModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val tokenListUMController: TokenListUMController, @@ -36,7 +36,7 @@ internal class TokenListModel @Inject constructor( val state: StateFlow = tokenListUMController.state - private val params: TokenListComponent.Params = paramsContainer.require() + private val params: OnrampTokenListComponent.Params = paramsContainer.require() init { subscribeOnUpdateState() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt similarity index 90% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index b8fcb55c6d..c75b080c3b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/TokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.tokenlist.ui +package com.tangem.features.onramp.tokenlist.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.PaddingValues @@ -14,8 +14,8 @@ import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM -import com.tangem.feature.wallet.presentation.tokenlist.ui.preview.PreviewTokenListUMProvider +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider /** * Token list diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt similarity index 93% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt index 1b735ea3c8..ce2f3e4f88 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/ui/preview/PreviewTokenListUMProvider.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.tokenlist.ui.preview +package com.tangem.features.onramp.tokenlist.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.currency.icon.CurrencyIconState @@ -7,8 +7,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.tokenlist.entity.TokenListUM import kotlinx.collections.immutable.toImmutableList internal class PreviewTokenListUMProvider : PreviewParameterProvider { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/utils/SearchTokensManager.kt similarity index 92% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/utils/SearchTokensManager.kt index ff57e2818c..dde1a1ab4f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/utils/SearchTokensManager.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/utils/SearchTokensManager.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.tokenlist.utils +package com.tangem.features.onramp.tokenlist.utils import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.withDebounce diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/DefaultSelectTokenComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/DefaultSelectTokenComponent.kt deleted file mode 100644 index 680be64c0b..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/DefaultSelectTokenComponent.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.feature.wallet.presentation.selecttoken - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.child -import com.tangem.feature.wallet.presentation.selecttoken.ui.SelectToken -import com.tangem.feature.wallet.presentation.tokenlist.TokenListComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class DefaultSelectTokenComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - tokenListComponentFactory: TokenListComponent.Factory, - @Assisted private val params: SelectTokenComponent.Params, -) : AppComponentContext by appComponentContext, SelectTokenComponent { - - private val tokenListComponent: TokenListComponent = tokenListComponentFactory.create( - context = child(key = "token_list"), - params = TokenListComponent.Params( - hasSearchBar = params.hasSearchBar, - userWalletId = params.userWalletId, - onTokenClick = params.onTokenClick, - ), - ) - - @Composable - override fun Content(modifier: Modifier) { - SelectToken( - titleResId = params.titleResId, - onBackClick = router::pop, - tokenListComponent = tokenListComponent, - modifier = modifier, - ) - } - - @AssistedFactory - interface Factory : SelectTokenComponent.Factory { - - override fun create( - context: AppComponentContext, - params: SelectTokenComponent.Params, - ): DefaultSelectTokenComponent - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/di/SelectTokenComponentModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/di/SelectTokenComponentModule.kt deleted file mode 100644 index 620bf54e1b..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/selecttoken/di/SelectTokenComponentModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.feature.wallet.presentation.selecttoken.di - -import com.tangem.feature.wallet.presentation.selecttoken.DefaultSelectTokenComponent -import com.tangem.feature.wallet.presentation.selecttoken.SelectTokenComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface SelectTokenComponentModule { - - @Binds - @Singleton - fun bindSelectTokenComponentFactory(factory: DefaultSelectTokenComponent.Factory): SelectTokenComponent.Factory -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListComponentModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListComponentModule.kt deleted file mode 100644 index 1e2672c2af..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/tokenlist/di/TokenListComponentModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.feature.wallet.presentation.tokenlist.di - -import com.tangem.feature.wallet.presentation.tokenlist.DefaultTokenListComponent -import com.tangem.feature.wallet.presentation.tokenlist.TokenListComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface TokenListComponentModule { - - @Binds - @Singleton - fun bindTokenListComponentFactory(factory: DefaultTokenListComponent.Factory): TokenListComponent.Factory -} \ No newline at end of file From 679a54bc0d5e94d39ca7b51ecc6af0f2db413879 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 17:46:06 +0300 Subject: [PATCH 35/41] Updated on 2026-08-14 --- app/build.gradle.kts | 2 ++ features/details/api/build.gradle.kts | 3 ++ features/disclaimer/api/build.gradle.kts | 3 ++ .../onboarding/legacy/di/FeatureModule.kt | 18 ------------ .../OnboardingLegacyFeatureToggles.kt | 5 ---- .../api}/.gitignore | 0 features/onboarding-v2/api/build.gradle.kts | 22 +++++++++++++++ .../v2/OnboardingV2FeatureToggles.kt | 5 ++++ .../v2/entry/OnboardingEntryComponent.kt | 13 +++++++++ features/onboarding-v2/impl/.gitignore | 1 + .../impl}/build.gradle.kts | 28 ++++++------------- .../v2/DefaultOnboardingV2FeatureToggles.kt} | 6 ++-- .../onboarding/v2/di/FeatureModule.kt | 18 ++++++++++++ features/onramp/api/build.gradle.kts | 3 ++ features/wallet-settings/api/build.gradle.kts | 3 ++ settings.gradle.kts | 3 +- 16 files changed, 87 insertions(+), 46 deletions(-) delete mode 100644 features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/di/FeatureModule.kt delete mode 100644 features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/OnboardingLegacyFeatureToggles.kt rename features/{onboarding-legacy => onboarding-v2/api}/.gitignore (100%) create mode 100644 features/onboarding-v2/api/build.gradle.kts create mode 100644 features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt create mode 100644 features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt create mode 100644 features/onboarding-v2/impl/.gitignore rename features/{onboarding-legacy => onboarding-v2/impl}/build.gradle.kts (60%) rename features/{onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/DefaultOnboardingLegacyFeatureToggles.kt => onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt} (64%) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/di/FeatureModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b42a58fe8e..5d371ecb8e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -148,6 +148,8 @@ dependencies { implementation(projects.features.markets.impl) implementation(projects.features.onramp.api) implementation(projects.features.onramp.impl) + implementation(projects.features.onboardingV2.api) + implementation(projects.features.onboardingV2.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/features/details/api/build.gradle.kts b/features/details/api/build.gradle.kts index ce267fe5d1..72035f72bb 100644 --- a/features/details/api/build.gradle.kts +++ b/features/details/api/build.gradle.kts @@ -16,4 +16,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/disclaimer/api/build.gradle.kts b/features/disclaimer/api/build.gradle.kts index 7b97fa25c1..d6fd71c5e8 100644 --- a/features/disclaimer/api/build.gradle.kts +++ b/features/disclaimer/api/build.gradle.kts @@ -13,4 +13,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/di/FeatureModule.kt b/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/di/FeatureModule.kt deleted file mode 100644 index 58798b2583..0000000000 --- a/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/di/FeatureModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.feature.onboarding.legacy.di - -import com.tangem.feature.onboarding.legacy.featuretoggle.DefaultOnboardingLegacyFeatureToggles -import com.tangem.feature.onboarding.legacy.featuretoggle.OnboardingLegacyFeatureToggles -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface FeatureModule { - - @Singleton - @Binds - fun provideFeatureToggles(featureToggles: DefaultOnboardingLegacyFeatureToggles): OnboardingLegacyFeatureToggles -} \ No newline at end of file diff --git a/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/OnboardingLegacyFeatureToggles.kt b/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/OnboardingLegacyFeatureToggles.kt deleted file mode 100644 index aa692ec61e..0000000000 --- a/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/OnboardingLegacyFeatureToggles.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.feature.onboarding.legacy.featuretoggle - -interface OnboardingLegacyFeatureToggles { - val isRewrittenOnboardingCodeInUse: Boolean -} \ No newline at end of file diff --git a/features/onboarding-legacy/.gitignore b/features/onboarding-v2/api/.gitignore similarity index 100% rename from features/onboarding-legacy/.gitignore rename to features/onboarding-v2/api/.gitignore diff --git a/features/onboarding-v2/api/build.gradle.kts b/features/onboarding-v2/api/build.gradle.kts new file mode 100644 index 0000000000..d6e9adffe8 --- /dev/null +++ b/features/onboarding-v2/api/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.onboarding.v2.api" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.wallets.models) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt new file mode 100644 index 0000000000..af4b3152e0 --- /dev/null +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.onboarding.v2 + +interface OnboardingV2FeatureToggles { + val isRewrittenOnboardingCodeInUse: Boolean +} \ No newline at end of file diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt new file mode 100644 index 0000000000..93b12a1d21 --- /dev/null +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt @@ -0,0 +1,13 @@ +package com.tangem.features.onboarding.v2.entry + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface OnboardingEntryComponent : ComposableContentComponent { + + data class Params( + val todo: String, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/.gitignore b/features/onboarding-v2/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/onboarding-v2/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/onboarding-legacy/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts similarity index 60% rename from features/onboarding-legacy/build.gradle.kts rename to features/onboarding-v2/impl/build.gradle.kts index d82dd86a7d..e0028b37e7 100644 --- a/features/onboarding-legacy/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -8,21 +8,20 @@ plugins { } android { - namespace = "com.tangem.feature.onboarding.legacy" + namespace = "com.tangem.features.onboarding.v2.impl" } dependencies { + /** Api */ + implementation(projects.features.onboardingV2.api) + /** Core modules */ - implementation(project(":common")) - implementation(project(":domain:models")) - implementation(project(":core:featuretoggles")) - implementation(project(":core:datasource")) - implementation(project(":core:analytics")) + implementation(projects.core.featuretoggles) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(project(":core:utils")) - implementation(project(":core:ui")) - implementation(project(":core:res")) - implementation(project(":data:common")) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.res) /** Tangem libraries */ implementation(deps.tangem.card.core) @@ -33,27 +32,18 @@ dependencies { /** AndroidX libraries */ implementation(deps.androidx.core.ktx) implementation(deps.androidx.appCompat) - implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.constraintLayout) - implementation(deps.androidx.activity.compose) implementation(deps.lifecycle.runtime.ktx) - implementation(deps.lifecycle.common.java8) - implementation(deps.lifecycle.viewModel.ktx) /** Compose libraries */ - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.coil) - implementation(deps.compose.shimmer) - - implementation(deps.tangem.card.core) /** Other libraries */ - implementation(deps.compose.shimmer) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) implementation(deps.timber) diff --git a/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/DefaultOnboardingLegacyFeatureToggles.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt similarity index 64% rename from features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/DefaultOnboardingLegacyFeatureToggles.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt index d46fdf1c92..4245576b3e 100644 --- a/features/onboarding-legacy/src/main/kotlin/com/tangem/feature/onboarding/legacy/featuretoggle/DefaultOnboardingLegacyFeatureToggles.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt @@ -1,11 +1,11 @@ -package com.tangem.feature.onboarding.legacy.featuretoggle +package com.tangem.features.onboarding.v2 import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import javax.inject.Inject -internal class DefaultOnboardingLegacyFeatureToggles @Inject constructor( +internal class DefaultOnboardingV2FeatureToggles @Inject constructor( private val featureTogglesManager: FeatureTogglesManager, -) : OnboardingLegacyFeatureToggles { +) : OnboardingV2FeatureToggles { override val isRewrittenOnboardingCodeInUse: Boolean get() = featureTogglesManager.isFeatureEnabled("ONBOARDING_CODE_REFACTORING_ENABLED") } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/di/FeatureModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/di/FeatureModule.kt new file mode 100644 index 0000000000..58f1c6cd79 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/di/FeatureModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onboarding.v2.di + +import com.tangem.features.onboarding.v2.DefaultOnboardingV2FeatureToggles +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface FeatureModule { + + @Singleton + @Binds + fun provideFeatureToggles(featureToggles: DefaultOnboardingV2FeatureToggles): OnboardingV2FeatureToggles +} \ No newline at end of file diff --git a/features/onramp/api/build.gradle.kts b/features/onramp/api/build.gradle.kts index dfd87a2e88..a241d32468 100644 --- a/features/onramp/api/build.gradle.kts +++ b/features/onramp/api/build.gradle.kts @@ -13,4 +13,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/wallet-settings/api/build.gradle.kts b/features/wallet-settings/api/build.gradle.kts index 7c48ae5a59..aee60dc9be 100644 --- a/features/wallet-settings/api/build.gradle.kts +++ b/features/wallet-settings/api/build.gradle.kts @@ -16,4 +16,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2876b342f6..251a4e76a0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -151,7 +151,8 @@ include(":libs:visa") // region Feature modules include(":features:onboarding") -include(":features:onboarding-legacy") +include(":features:onboarding-v2:api") +include(":features:onboarding-v2:impl") include(":features:referral:data") include(":features:referral:domain") From f71aa5ea1b196888ca5f4af9a2d3d9d9def9c4a9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 19:36:21 +0500 Subject: [PATCH 36/41] Updated on 2026-08-14 --- .../java/com/tangem/data/staking/DefaultStakingRepository.kt | 2 +- gradle/dependencies.toml | 2 +- 2 files changed, 2 insertions(+), 2 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 e57c15dd67..1a40afb7ac 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 @@ -645,7 +645,7 @@ internal class DefaultStakingRepository( Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCointId() to ETHEREUM_POLYGON_INTEGRATION_ID, // Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, - // Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, + Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, // Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID, // Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID, // Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 9bf5436270..d010c3fb81 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-835" +tangemBlockchainSdk = "develop-836" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-391" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 4aaaefbd71dde36e84b1af85d84b9bcb294ceea1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 16:31:30 +0300 Subject: [PATCH 37/41] Updated on 2026-08-14 --- .../wallet/domain/Wallet2CobrandImage.kt | 18 ++++++++++++++++++ .../ill_crypto_casey_card2_120_106.webp | Bin 0 -> 25890 bytes .../ill_crypto_casey_card3_120_106.webp | Bin 0 -> 43166 bytes .../ill_crypto_org_card2_120_106.webp | Bin 0 -> 26526 bytes .../ill_crypto_org_card3_120_106.webp | Bin 0 -> 32680 bytes .../drawable/ill_stealth_cards2_120_106.webp | Bin 0 -> 10750 bytes .../drawable/ill_stealth_cards3_120_106.webp | Bin 0 -> 15840 bytes 7 files changed, 18 insertions(+) create mode 100644 features/wallet/impl/src/main/res/drawable/ill_crypto_casey_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_crypto_casey_card3_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_crypto_org_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_crypto_org_card3_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_stealth_cards2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_stealth_cards3_120_106.webp diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index 1f191d3585..21890462f2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -61,6 +61,18 @@ internal enum class Wallet2CobrandImage( batchIds = setOf("AF28"), ), + CryptoCasey( + cards2ResId = R.drawable.ill_crypto_casey_card2_120_106, + cards3ResId = R.drawable.ill_crypto_casey_card3_120_106, + batchIds = setOf("AF21", "AF22", "AF23"), + ), + + CryptoOrg( + cards2ResId = R.drawable.ill_crypto_org_card2_120_106, + cards3ResId = R.drawable.ill_crypto_org_card3_120_106, + batchIds = setOf("AF57"), + ), + CryptoSeth( cards2ResId = R.drawable.ill_crypto_seth_card2_120_106, cards3ResId = R.drawable.ill_crypto_seth_card3_120_106, @@ -134,6 +146,12 @@ internal enum class Wallet2CobrandImage( batchIds = setOf("AF19"), ), + StealthCard( + cards2ResId = R.drawable.ill_stealth_cards2_120_106, + cards3ResId = R.drawable.ill_stealth_cards3_120_106, + batchIds = setOf("AF60", "AF74", "AF88"), + ), + Trillant( cards2ResId = R.drawable.ill_trillant_card2_120_106, cards3ResId = R.drawable.ill_trillant_card3_120_106, diff --git a/features/wallet/impl/src/main/res/drawable/ill_crypto_casey_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_crypto_casey_card2_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..5aaa6f9477cf3ed7f3fe56e60469b95f39e76a84 GIT binary patch literal 25890 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 z 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 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_crypto_org_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_crypto_org_card2_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..3c042caa95954d5f7d5b3e20796b2bd75663fc8d GIT binary patch literal 26526 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 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_crypto_org_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_crypto_org_card3_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..ec14a4dcb55d6f59780920d3eb528d91b7c53d87 GIT binary patch literal 32680 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 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_stealth_cards2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_stealth_cards2_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..5a39ebf1ab8819953142fd99fb64bb5b8b63befa GIT binary patch literal 10750 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 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_stealth_cards3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_stealth_cards3_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..0e6b50a2fede1d2c70f8d3e2e6bc672cd4cde60c GIT binary patch literal 15840 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 Date: Tue, 29 Oct 2024 09:18:22 +0200 Subject: [PATCH 38/41] Updated on 2026-08-14 --- .../state/helpers/StakingBalanceUpdater.kt | 14 ++++++++++++-- .../state/helpers/StakingTransactionSender.kt | 2 +- .../presentation/viewmodel/StakingViewModel.kt | 9 +++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index aa8342846e..e10d32b8a1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -29,7 +29,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor( @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val yield: Yield, ) { - fun scheduleUpdates() { + fun fullUpdate() { coroutineScope.launch { listOf( // we should update network to find pending tx after 1 sec @@ -56,7 +56,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor( } } - suspend fun instantUpdate() { + suspend fun partialUpdate() { coroutineScope { listOf( async { @@ -72,6 +72,16 @@ internal class StakingBalanceUpdater @AssistedInject constructor( } } + suspend fun initialUpdate() { + coroutineScope { + listOf( + async { + updateStakeBalance() + }, + ).awaitAll() + } + } + private suspend fun updateNetworkStatuses(delay: Long = BALANCE_UPDATE_DELAY) { updateDelayedNetworkStatusUseCase( userWalletId = userWallet.walletId, 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 5e5538c4e1..e61018217b 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 @@ -227,7 +227,7 @@ internal class StakingTransactionSender @AssistedInject constructor( networkId = cryptoCurrencyStatus.currency.network.id, ).getOrElse { "" } - balanceUpdater.scheduleUpdates() + balanceUpdater.fullUpdate() onSendSuccess(txUrl) }, ) 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 6850137ee4..68fef18387 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 @@ -195,6 +195,7 @@ internal class StakingViewModel @Inject constructor( private var approvalJobHolder: JobHolder = JobHolder() private var feeJobHolder: JobHolder = JobHolder() private var sendTransactionJobHolder = JobHolder() + private var stepChangesJobHolder = JobHolder() init { subscribeOnSelectedAppCurrency() @@ -207,6 +208,7 @@ internal class StakingViewModel @Inject constructor( approvalJobHolder.cancel() feeJobHolder.cancel() sendTransactionJobHolder.cancel() + stepChangesJobHolder.cancel() } override fun onBackClick() { @@ -330,7 +332,7 @@ internal class StakingViewModel @Inject constructor( override fun onRefreshSwipe(isRefreshing: Boolean) { stateController.update(SetInitialLoadingStateTransformer(isRefreshing)) coroutineScope.launch { - balanceUpdater.instantUpdate() + balanceUpdater.partialUpdate() }.invokeOnCompletion { stateController.update(SetInitialLoadingStateTransformer(false)) } @@ -762,6 +764,8 @@ internal class StakingViewModel @Inject constructor( .onEach { maybeStatus -> maybeStatus.fold( ifRight = { status -> + if (status.value !is CryptoCurrencyStatus.Loaded) return@fold + if (!isInitialInfoAnalyticSent) { isInitialInfoAnalyticSent = true val balances = status.value.yieldBalance as? YieldBalance.Data @@ -822,7 +826,7 @@ internal class StakingViewModel @Inject constructor( when { isInitState() -> { updateInitialData() - onRefreshSwipe(isRefreshing = false) + balanceUpdater.initialUpdate() } isAssentState() -> { getFee() @@ -839,6 +843,7 @@ internal class StakingViewModel @Inject constructor( } .flowOn(dispatchers.main) .launchIn(viewModelScope) + .saveIn(stepChangesJobHolder) } private fun subscribeOnActionsUpdates() { From 47f5ac82df354a3bbebf4b876bd378af4f66b6e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Oct 2024 17:27:49 +0400 Subject: [PATCH 39/41] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 11 --- .../repository/DefaultCurrenciesRepository.kt | 34 +++++--- .../domain/tokens/GetNodlTokenListUseCase.kt | 79 ------------------- .../CurrenciesStatusesOperations.kt | 50 ------------ .../tokens/operations/TokenListOperations.kt | 26 ------ .../SingleWalletWithTokenContentLoader.kt | 6 +- ...ngleWalletWithTokenContentLoaderFactory.kt | 6 +- .../SingleWalletWithTokenListSubscriber.kt | 16 ++-- 8 files changed, 34 insertions(+), 194 deletions(-) delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index c28ded59d7..c72518ff14 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -67,17 +67,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideGetCardTokensListUseCase( - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, - ): GetNodlTokenListUseCase { - return GetNodlTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository) - } - @Provides @Singleton fun provideRemoveCurrencyUseCase( 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 e5b5f561bd..a739407505 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 @@ -38,7 +38,6 @@ import com.tangem.domain.wallets.models.UserWalletId 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 @@ -207,8 +206,9 @@ internal class DefaultCurrenciesRepository( if (userWallet.isMultiCurrency) { getMultiCurrencyWalletCurrenciesUpdates(userWalletId).collect(::send) } else { - val currency = getSingleCurrencyWalletPrimaryCurrency(userWalletId) - send(listOf(currency)) + val currencies = getSingleCurrencyWalletWithCardCurrencies(userWalletId) + + send(currencies) } } } @@ -349,26 +349,34 @@ internal class DefaultCurrenciesRepository( override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return channelFlow { - ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) + val userWallet = getUserWallet(userWalletId) - launch(dispatchers.io) { + if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWalletId) - .map { it.group == UserTokensResponse.GroupType.NETWORK } - .collect(::send) + .map { response -> response.group == UserTokensResponse.GroupType.NETWORK } + .distinctUntilChanged() + .onEach { isGrouped -> send(isGrouped) } + .launchIn(scope = this + dispatchers.io) + } else { + send(element = false) } - }.cancellable() + } } override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { return channelFlow { - ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) + val userWallet = getUserWallet(userWalletId) - launch(dispatchers.io) { + if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWalletId) - .map { it.sort == UserTokensResponse.SortType.BALANCE } - .collect(::send) + .map { response -> response.sort == UserTokensResponse.SortType.BALANCE } + .distinctUntilChanged() + .onEach { isSorted -> send(isSorted) } + .launchIn(scope = this + dispatchers.io) + } else { + send(element = false) } - }.cancellable() + } } override fun isSendBlockedByPendingTransactions( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt deleted file mode 100644 index abbe7fa327..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.left -import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.operations.TokenListOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.transformLatest - -/** - * Use case for getting a list of tokens for NODL card - * - * @property currenciesRepository currencies repository - * @property quotesRepository quotes repository - * @property networksRepository networks repository - * @property stakingRepository staking repository - */ -class GetNodlTokenListUseCase( - private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, -) { - - @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId): EitherFlow { - return getTokensStatuses(userWalletId).transformLatest { maybeTokens -> - maybeTokens.fold( - ifLeft = { error -> - emit(error.left()) - }, - ifRight = { tokens -> - emitAll(createTokenList(userWalletId, tokens)) - }, - ) - } - } - - private fun getTokensStatuses(userWalletId: UserWalletId): EitherFlow> { - val operations = CurrenciesStatusesOperations( - userWalletId = userWalletId, - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - - return operations.getCardCurrenciesStatusesFlow() - .map { maybeCurrenciesStatuses -> - maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) - } - } - - private fun createTokenList( - userWalletId: UserWalletId, - tokens: List, - ): EitherFlow { - val operations = TokenListOperations( - userWalletId = userWalletId, - tokens = tokens, - currenciesRepository = currenciesRepository, - ) - - return operations.getTokenListForSingleCurrencyFlow().map { maybeTokenList -> - maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 34ac4153c2..639ccd6991 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -120,50 +120,6 @@ internal class CurrenciesStatusesOperations( return createCurrencyStatus(currency, quotes, networkStatus, yieldBalances) } - fun getCardCurrenciesStatusesFlow(): Flow>> { - return flow { - val nonEmptyCurrencies = recover( - block = { getCurrenciesFromCard(userWalletId) }, - recover = { - emit(it.left()) - return@flow - }, - ).toNonEmptyListOrNull() - - if (nonEmptyCurrencies == null) { - val emptyCurrenciesStatuses = emptyList() - - emit(emptyCurrenciesStatuses.right()) - return@flow - } - - val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeNetworkStatuses = null, - maybeQuotes = null, - maybeYieldBalances = null, - ) - - emit(maybeLoadingCurrenciesStatuses) - - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - - val currenciesFlow = combine( - getQuotes(currenciesIds), - getNetworksStatuses(networks), - ) { maybeQuotes, maybeNetworksStatuses -> - createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeQuotes = maybeQuotes, - maybeNetworkStatuses = maybeNetworksStatuses, - maybeYieldBalances = null, - ) - } - - emitAll(currenciesFlow) - } - } - suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow> { val currency = recover( block = { getMultiCurrencyWalletCurrency(currencyId) }, @@ -377,12 +333,6 @@ internal class CurrenciesStatusesOperations( ) } - private suspend fun Raise.getCurrenciesFromCard(userWalletId: UserWalletId): List { - return catch({ currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId) }) { - raise(Error.DataError(it)) - } - } - private fun getQuotes(tokensIds: NonEmptySet): Flow>> { return quotesRepository.getQuotesUpdates(tokensIds) .map, Either>> { quotes -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 0af48b7898..8f2404de68 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -29,16 +29,6 @@ internal class TokenListOperations( } } - fun getTokenListForSingleCurrencyFlow(): Flow> { - return flow { - emit( - either { - createTokenList() - }, - ) - } - } - private fun Raise.createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList { val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() ?: return TokenList.Empty @@ -54,22 +44,6 @@ internal class TokenListOperations( ) } - private fun Raise.createTokenList(): TokenList { - val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() - ?: return TokenList.Empty - - val isAnyTokenLoading = nonEmptyCurrencies.any { it.value is CryptoCurrencyStatus.Loading } - val fiatBalanceOperations = TokenListFiatBalanceOperations(nonEmptyCurrencies, isAnyTokenLoading) - - return createTokenList( - currencies = nonEmptyCurrencies, - fiatBalance = fiatBalanceOperations.calculateFiatBalance(), - isAnyTokenLoading = isAnyTokenLoading, - isGrouped = false, - isSortedByBalance = false, - ) - } - private fun Raise.createTokenList( currencies: NonEmptyList, fiatBalance: TotalFiatBalance, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index f3829783da..897b44ae92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -1,12 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.tokens.GetNodlTokenListUseCase 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.MultiWalletWarningsSubscriber @@ -23,7 +23,7 @@ internal class SingleWalletWithTokenContentLoader( private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getNodlTokenListUseCase: GetNodlTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : WalletContentLoader(id = userWallet.walletId) { @@ -36,7 +36,7 @@ internal class SingleWalletWithTokenContentLoader( clickIntents = clickIntents, tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, - getNodlTokenListUseCase = getNodlTokenListUseCase, + tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 5d820846c7..1c433dc756 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -1,12 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.tokens.GetNodlTokenListUseCase 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 @@ -19,7 +19,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val tokenListAnalyticsSender: TokenListAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getNodlTokenListUseCase: GetNodlTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, @@ -33,7 +33,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - getNodlTokenListUseCase = getNodlTokenListUseCase, + tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, 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 c8848d06e9..2d37fea506 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 @@ -2,23 +2,21 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.utils.toLce -import com.tangem.domain.tokens.GetNodlTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList 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 -import kotlinx.coroutines.flow.map @Suppress("LongParameterList") internal class SingleWalletWithTokenListSubscriber( private val userWallet: UserWallet, - private val getNodlTokenListUseCase: GetNodlTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, stateHolder: WalletStateController, clickIntents: WalletClickIntents, tokenListAnalyticsSender: TokenListAnalyticsSender, @@ -35,9 +33,9 @@ internal class SingleWalletWithTokenListSubscriber( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) { - override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow = - getNodlTokenListUseCase( - userWallet.walletId, - ) - .map { it.toLce() } + override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { + tokenListStore.addIfNot(userWallet.walletId, coroutineScope) + + return tokenListStore.getOrThrow(userWallet.walletId) + } } \ No newline at end of file From 42b8de79092a1d90758f6d1856db3d193af1455a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Oct 2024 13:12:57 +0300 Subject: [PATCH 40/41] Updated on 2026-08-14 --- features/onboarding-v2/impl/build.gradle.kts | 8 +- .../stepper/api/OnboardingStepperComponent.kt | 31 +++++++ .../impl/DefaultOnboardingStepperComponent.kt | 76 +++++++++++++++++ .../v2/stepper/impl/di/ComponentModule.kt | 20 +++++ .../v2/stepper/impl/ui/OnboardingStepper.kt | 83 +++++++++++++++++++ 5 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/api/OnboardingStepperComponent.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/di/ComponentModule.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index e0028b37e7..8cc0843478 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -22,6 +22,12 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.ui) implementation(projects.core.res) + implementation(projects.core.decompose) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.feedback) + implementation(projects.domain.core) /** Tangem libraries */ implementation(deps.tangem.card.core) @@ -31,8 +37,6 @@ dependencies { /** AndroidX libraries */ implementation(deps.androidx.core.ktx) - implementation(deps.androidx.appCompat) - implementation(deps.androidx.constraintLayout) implementation(deps.lifecycle.runtime.ktx) /** Compose libraries */ diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/api/OnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/api/OnboardingStepperComponent.kt new file mode 100644 index 0000000000..3a6cd92582 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/api/OnboardingStepperComponent.kt @@ -0,0 +1,31 @@ +package com.tangem.features.onboarding.v2.stepper.api + +import androidx.annotation.IntRange +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.scan.ScanResponse +import kotlinx.coroutines.flow.StateFlow + +interface OnboardingStepperComponent : ComposableContentComponent { + + data class StepperState( + @IntRange(from = 0) val currentStep: Int, + @IntRange(from = 0) val steps: Int, + val title: TextReference, + ) + + class Params( + val initState: StepperState, + val popBack: () -> Unit, + val scanResponse: ScanResponse, + ) + + val state: StateFlow + + fun changeStep(step: Int) + fun nextStep() + fun changeTitle(title: TextReference) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt new file mode 100644 index 0000000000..8e0bfb77ac --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt @@ -0,0 +1,76 @@ +package com.tangem.features.onboarding.v2.stepper.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.essenty.instancekeeper.getOrCreateSimple +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent +import com.tangem.features.onboarding.v2.stepper.impl.ui.OnboardingStepper +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class DefaultOnboardingStepperComponent @AssistedInject constructor( + @Assisted val context: AppComponentContext, + @Assisted val params: OnboardingStepperComponent.Params, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, +) : OnboardingStepperComponent, AppComponentContext by context { + + override val state = instanceKeeper.getOrCreateSimple { MutableStateFlow(params.initState) } + + override fun changeStep(step: Int) { + state.update { it.copy(currentStep = step.coerceAtLeast(minimumValue = 0)) } + } + + override fun nextStep() { + state.update { state -> + if (state.currentStep < state.steps - 1) { + state.copy(currentStep = state.currentStep + 1) + } else { + state + } + } + } + + override fun changeTitle(title: TextReference) { + state.update { it.copy(title = title) } + } + + private fun openSupport() { + componentScope.launch { + val cardInfo = getCardInfoUseCase(params.scanResponse).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val state by state.collectAsStateWithLifecycle() + + OnboardingStepper( + state = state, + onBackClick = remember(this) { params.popBack }, + onSupportButtonClick = remember(this) { ::openSupport }, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : OnboardingStepperComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnboardingStepperComponent.Params, + ): DefaultOnboardingStepperComponent + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..e0401a486d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/di/ComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onboarding.v2.stepper.impl.di + +import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent +import com.tangem.features.onboarding.v2.stepper.impl.DefaultOnboardingStepperComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindOnboardingStepperComponent( + factory: DefaultOnboardingStepperComponent.Factory, + ): OnboardingStepperComponent.Factory +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt new file mode 100644 index 0000000000..a4d735e82b --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt @@ -0,0 +1,83 @@ +package com.tangem.features.onboarding.v2.stepper.impl.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.progressbar.LinearProgressIndicator +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent + +@Composable +internal fun OnboardingStepper( + state: OnboardingStepperComponent.StepperState, + onBackClick: () -> Unit, + onSupportButtonClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val fraction = state.currentStep.toFloat() / state.steps.coerceAtLeast(1) + val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState(targetFraction = fraction) + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + TangemTopAppBar( + startButton = TopAppBarButtonUM.Back(onBackClick), + endButton = TopAppBarButtonUM(iconRes = R.drawable.ic_chat_24, onIconClicked = onSupportButtonClick) + .takeIf { state.steps != state.currentStep }, + title = if (state.steps == state.currentStep) { + resourceReference(R.string.common_done) + } else { + state.title + }, + containerColor = TangemTheme.colors.background.primary, + modifier = modifier, + ) + + LinearProgressIndicator( + modifier = Modifier + .padding(horizontal = 16.dp) + .height(4.dp) + .fillMaxWidth(), + progress = { animatedIndicatorFraction }, + color = TangemTheme.colors.icon.primary1, + backgroundColor = TangemTheme.colors.background.tertiary, + strokeCap = StrokeCap.Round, + ) + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + Box( + Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + OnboardingStepper( + modifier = Modifier.align(Alignment.TopCenter), + state = OnboardingStepperComponent.StepperState( + currentStep = 2, + steps = 3, + title = resourceReference(R.string.common_done), + ), + onBackClick = {}, + onSupportButtonClick = {}, + ) + } + } +} \ No newline at end of file From 06f2d51e87eaf7118caaf29748e8dc2ce0e253f3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Oct 2024 12:14:56 +0400 Subject: [PATCH 41/41] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 9 +++ .../com/tangem/common/routing/AppRoute.kt | 9 +-- .../GetSelectedAppCurrencyUseCase.kt | 9 +-- features/onramp/api/build.gradle.kts | 3 + .../onramp/component/BuyCryptoComponent.kt | 22 +++++++ features/onramp/impl/build.gradle.kts | 3 + .../onramp/buy/DefaultBuyCryptoComponent.kt | 65 +++++++++++++++++++ .../onramp/buy/di/BuyCryptoComponentModule.kt | 18 +++++ 8 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/BuyCryptoComponent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/di/BuyCryptoComponentModule.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index cca2474fa1..06b53a8fd3 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -11,6 +11,7 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.features.onramp.component.BuyCryptoComponent import com.tangem.features.onramp.component.OnrampComponent import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter @@ -48,6 +49,7 @@ internal class ChildFactory @Inject constructor( private val manageTokensComponentFactory: ManageTokensComponent.Factory, private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, private val onrampComponentFactory: OnrampComponent.Factory, + private val buyCryptoComponentFactory: BuyCryptoComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, @@ -195,6 +197,13 @@ internal class ChildFactory @Inject constructor( componentFactory = onrampComponentFactory, ) } + is AppRoute.BuyCrypto -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = BuyCryptoComponent.Params(userWalletId = route.userWalletId), + componentFactory = buyCryptoComponentFactory, + ) + } } } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 94e8f8487c..c10a008033 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -117,10 +117,6 @@ sealed class AppRoute(val path: String) : Route { ) : AppRoute(path = "/details/security"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - const val USER_WALLET_ID_KEY = "userWalletId" - } } @Serializable @@ -291,4 +287,9 @@ sealed class AppRoute(val path: String) : Route { @Serializable data object Onramp : AppRoute(path = "/onramp") + + @Serializable + data class BuyCrypto( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/buy_crypto/${userWalletId.stringValue}") } \ No newline at end of file diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt index 4bd3a52e99..179973c048 100644 --- a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt @@ -6,10 +6,7 @@ import arrow.core.right import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.repository.AppCurrencyRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.flow.* class GetSelectedAppCurrencyUseCase( private val appCurrencyRepository: AppCurrencyRepository, @@ -21,4 +18,8 @@ class GetSelectedAppCurrencyUseCase( .catch { emit(SelectedAppCurrencyError.DataError(it).left()) } .onEmpty { emit(SelectedAppCurrencyError.NoAppCurrencySelected.left()) } } + + suspend fun invokeSync(): Either { + return invoke().firstOrNull() ?: SelectedAppCurrencyError.NoAppCurrencySelected.left() + } } \ No newline at end of file diff --git a/features/onramp/api/build.gradle.kts b/features/onramp/api/build.gradle.kts index a241d32468..cde16c9b92 100644 --- a/features/onramp/api/build.gradle.kts +++ b/features/onramp/api/build.gradle.kts @@ -14,6 +14,9 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) + /* Project - Domain */ + implementation(projects.domain.wallets.models) + /* Compose */ implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/BuyCryptoComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/BuyCryptoComponent.kt new file mode 100644 index 0000000000..c627160e26 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/BuyCryptoComponent.kt @@ -0,0 +1,22 @@ +package com.tangem.features.onramp.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Buy crypto component + * +[REDACTED_AUTHOR] + */ +interface BuyCryptoComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + /** + * Params + * + * @property userWalletId user wallet id + */ + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 8de571b7a7..042df5ae39 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -28,8 +28,10 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.legacy) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) /** DI */ @@ -54,5 +56,6 @@ dependencies { /** Other */ implementation(deps.decompose.ext.compose) implementation(deps.kotlin.immutable.collections) + implementation(deps.reKotlin) implementation(deps.timber) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt new file mode 100644 index 0000000000..1c463d0cdf --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt @@ -0,0 +1,65 @@ +package com.tangem.features.onramp.buy + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import arrow.core.getOrElse +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.onramp.component.BuyCryptoComponent +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.selecttoken.OnrampSelectTokenComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch + +@Stable +internal class DefaultBuyCryptoComponent @AssistedInject constructor( + onrampSelectTokenComponentFactory: OnrampSelectTokenComponent.Factory, + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: BuyCryptoComponent.Params, + private val reduxStateHolder: ReduxStateHolder, + private val getWalletsUseCase: GetWalletsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, +) : BuyCryptoComponent { + + private val selectTokenComponent: OnrampSelectTokenComponent = onrampSelectTokenComponentFactory.create( + context = appComponentContext, + params = OnrampSelectTokenComponent.Params( + hasSearchBar = true, + userWalletId = params.userWalletId, + titleResId = R.string.common_buy, + onTokenClick = ::onTokenClick, + ), + ) + + @Composable + override fun Content(modifier: Modifier) { + selectTokenComponent.Content(modifier = modifier) + } + + private fun onTokenClick(status: CryptoCurrencyStatus) { + appComponentContext.componentScope.launch { + reduxStateHolder.dispatch( + TradeCryptoAction.Buy( + userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }, + cryptoCurrencyStatus = status, + appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync() + .getOrElse { AppCurrency.Default }.code, + ), + ) + } + } + + @AssistedFactory + interface Factory : BuyCryptoComponent.Factory { + + override fun create(context: AppComponentContext, params: BuyCryptoComponent.Params): DefaultBuyCryptoComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/di/BuyCryptoComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/di/BuyCryptoComponentModule.kt new file mode 100644 index 0000000000..d45a326a89 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/di/BuyCryptoComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onramp.buy.di + +import com.tangem.features.onramp.buy.DefaultBuyCryptoComponent +import com.tangem.features.onramp.component.BuyCryptoComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface BuyCryptoComponentModule { + + @Binds + @Singleton + fun bindBuyCryptoComponentFactory(factory: DefaultBuyCryptoComponent.Factory): BuyCryptoComponent.Factory +} \ No newline at end of file