Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-29 14:27:56 +00:00
commit 1ffa785bc0
26 changed files with 288 additions and 144 deletions

View file

@ -1,6 +1,5 @@
package com.tangem.tap.common.feedback
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.extensions.breakLine
class FeedbackDataBuilder(
@ -33,9 +32,10 @@ class FeedbackDataBuilder(
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
if (walletInfo.blockchain == Blockchain.Bitcoin) {
builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey)
}
// enable later
// if (walletInfo.blockchain == Blockchain.Bitcoin) {
// builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey)
// }
builder.appendKeyValue("Outputs count", walletInfo.outputsCount)

View file

@ -23,6 +23,7 @@ import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.ScanTask
@ -247,7 +248,7 @@ private class ScanWalletProcessor(
}
private fun getWalletProductType(card: CardDTO): ProductType {
if (card.batchId == CardDTO.RING_BATCH_ID) {
if (RING_BATCH_IDS.contains(card.batchId)) {
return ProductType.Ring
}
return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&

View file

@ -41,7 +41,7 @@ internal object CoinsResponseConverter : Converter<CoinsResponse, List<Token>> {
)
},
)
}
}.filter { it.networks.isNotEmpty() }
}
fun getIconUrl(id: String, imageHost: String? = null): String {

View file

@ -74,7 +74,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
.appendQueryParameter("address", walletAddress)
.appendQueryParameter("signature", signature(walletAddress))
.appendQueryParameter("fix_currency", "true")
.appendQueryParameter("return_url", ExchangeUrlBuilder.SUCCESS_URL)
.appendQueryParameter("redirect_url", ExchangeUrlBuilder.SUCCESS_URL)
if (isDarkTheme) builder.appendQueryParameter("theme", "1inch")
blockchain.mercuryoNetwork()?.let {

View file

@ -81,7 +81,7 @@ class MoonPayService(
override fun availableForSell(currency: Currency): Boolean {
val availableForSell = status?.availableForSell ?: return false
val metadata = status?.responseCurrencies?.map { it.metadata }
val metadata = status?.responseCurrencies?.filter { it.isSellSupported }?.map { it.metadata }
if (!isSellAllowed()) return false
return when (currency) {

View file

@ -6,5 +6,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CreateUserNetworkAccountBody(
@Json(name = "networkId") val networkId: String,
@Json(name = "walletPublicKey") val publicWalletKey: String,
@Json(name = "walletPublicKey") val walletPublicKey: String,
)

View file

@ -12,6 +12,6 @@ data class CreateUserNetworkAccountResponse(
@JsonClass(generateAdapter = true)
data class AccountCreated(
@Json(name = "accountId") val accountId: String,
@Json(name = "publicWalletKey") val publicWalletKey: String,
@Json(name = "walletPublicKey") val walletPublicKey: String,
)
}

View file

@ -53,6 +53,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"algorand", "algorand/test" -> R.drawable.img_algorand_22
"hedera", "hedera/test" -> R.drawable.img_hedera_22
"playa3ull" -> R.drawable.img_playa3ull_22
"DUC" -> R.drawable.img_ducatus_22
else -> R.drawable.ic_alert_24
}
}
@ -107,6 +108,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"algorand", "algorand/test" -> R.drawable.img_algorand_22
"hedera-hashgraph", "hedera/test" -> R.drawable.img_hedera_22
"playa3ull-games" -> R.drawable.img_playa3ull_22
"ducatus" -> R.drawable.img_ducatus_22
else -> R.drawable.ic_alert_24
}
}
@ -158,6 +160,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"algorand" -> R.drawable.img_algorand_22
"hedera-hashgraph" -> R.drawable.img_hedera_22
"playa3ull-games-2" -> R.drawable.img_playa3ull_22
"ducatus" -> R.drawable.img_ducatus_22
else -> R.drawable.ic_alert_24
}
}
@ -212,6 +215,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"algorand", "algorand/test" -> R.drawable.ic_algorand_22
"hedera", "hedera/test" -> R.drawable.ic_hedera_22
"playa3ull" -> R.drawable.ic_playa3ull_22
"DUC" -> R.drawable.ic_ducatus_22
else -> R.drawable.ic_alert_24
}
}
@ -266,6 +270,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"algorand", "algorand/test" -> R.drawable.ic_algorand_22
"hedera-hashgraph", "hedera/test" -> R.drawable.ic_hedera_22
"playa3ull-games" -> R.drawable.ic_playa3ull_22
"ducatus" -> R.drawable.ic_ducatus_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -14,12 +14,18 @@ object BigDecimalFormatter {
private const val TEMP_CURRENCY_CODE = "USD"
fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: String, decimals: Int): String {
fun formatCryptoAmount(
cryptoAmount: BigDecimal?,
cryptoCurrency: String,
decimals: Int,
locale: Locale = Locale.getDefault(),
): String {
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
val formatter = NumberFormat.getNumberInstance().apply {
val formatter = NumberFormat.getNumberInstance(locale).apply {
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
minimumFractionDigits = 2
isGroupingUsed = true
roundingMode = RoundingMode.DOWN
}
@ -32,8 +38,12 @@ object BigDecimalFormatter {
}
}
fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String {
return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals)
fun formatCryptoAmount(
cryptoAmount: BigDecimal?,
cryptoCurrency: CryptoCurrency,
locale: Locale = Locale.getDefault(),
): String {
return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals, locale)
}
fun formatFiatAmount(

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" />
<path
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M11.026,3.5L11.428,3.768L12.235,3.792L12.65,4.376L15.042,4.588L15.737,6.742L17.5,7.709L18.236,11.706L17.647,12.295L17.785,13.814L16.566,14.894L16.183,15.938L13.214,17.354L13.032,17.68H10.335L7.129,17.474L7.128,17.473L4.268,17.336L4.246,17.203L5.823,16.566V13.338L5.823,13.337L5.857,10.684V6.039L4,5.128L4.03,4.997H5.677L10.516,3.864L11.026,3.5ZM10.28,4.061L5.974,5.069L8.356,5.68L10.28,4.061ZM8.217,5.786L7.488,6.332L6.131,9.944L6.946,5.46L8.217,5.786ZM6.812,5.426L5.781,5.162L5.995,5.988V9.919L6.812,5.426ZM5.993,10.811L5.963,13.146L7.237,11.629L5.993,10.811ZM7.224,11.859L6.154,13.132L7.071,12.683L7.224,11.859ZM7.225,12.608L7.499,12.474L7.352,11.926L7.225,12.608ZM7.343,11.535L8.187,11.099L6.227,10.8L7.343,11.535ZM6.415,10.689L7.729,10.515L8.2,10.962L6.415,10.689ZM8.232,11.231L7.42,11.65L7.603,12.331L8.232,11.231ZM7.894,12.1L7.63,12.563L6.01,13.356L6.97,14.287L7.894,12.1ZM6.935,14.445L6.035,13.573L7.172,17.3L7.841,16.893L6.935,14.445ZM7.99,16.869L10.019,17.449L8.366,16.15L8.365,16.149L7.903,15.796L7.99,16.869ZM7.893,15.615L8.059,13.594L8.486,13.434L8.614,14.679L8.361,15.973L7.893,15.615ZM8.507,16.085L10.162,17.386L9.216,15.258L8.507,16.085ZM9.502,15.56L10.382,17.543H11.06L10.008,16.172L9.502,15.56ZM10.17,16.157L11.127,17.404L11.098,15.752L10.17,16.157ZM11.238,15.86L11.267,17.531L12.351,17.36L11.238,15.86ZM12.505,17.336L13,17.257L12.458,16.818L12.177,16.894L12.505,17.336ZM12.108,16.769L12.326,16.711L12.017,16.46L12.108,16.769ZM12.064,16.321L12.492,16.668L15.158,16.044L12.064,16.321ZM15.522,16.101L12.627,16.778L13.174,17.22L15.522,16.101ZM13.035,17.392L12.078,17.543H12.951L13.035,17.392ZM16.101,15.761L16.448,14.814L17.226,14.125L15.448,15.079L16.101,15.761ZM15.601,14.84L17.641,13.747L17.522,12.446L15.601,14.84ZM15.31,15.135L15.979,15.832L12.097,16.18L12.884,15.505L15.31,15.135ZM13.001,15.348L15.146,15.02L13.24,13.78L13.303,14.908L13.001,15.348ZM13.269,13.634L15.127,14.843L14.596,13.941L13.523,13.355L13.269,13.634ZM14.706,13.856L15.241,14.764L14.976,11.689L14.253,12.994L14.706,13.856ZM14.177,12.848L14.885,11.568L13.678,11.899L14.177,12.848ZM13.693,11.752L13.97,11.059L17.229,10.782L13.693,11.752ZM17.824,10.761L15.099,11.509L15.386,14.842L17.824,10.761ZM15.907,14.238L17.524,12.223L18.087,11.66L17.936,10.842L15.907,14.238ZM17.867,10.465L17.61,9.071L15.769,7.85L15.783,8.619L17.867,10.465ZM15.643,8.517L15.613,6.921L14.49,7.717L15.643,8.517ZM14.37,7.634L15.474,6.851L14.692,6.824L13.725,7.187L14.37,7.634ZM13.477,7.145L11.108,3.816L11.332,8.33L13.477,7.145ZM11.193,8.322L10.972,3.869L9.393,7.312L11.193,8.322ZM9.296,7.192L10.856,3.79L10.587,3.983L8.462,5.77L9.296,7.192ZM8.304,5.893L7.627,6.401L8.304,8.519V5.893ZM8.279,8.892L7.541,6.585L6.674,8.892H8.279ZM6.596,9.099L6.031,10.6L7.623,10.39L6.596,9.099ZM7.817,10.409L8.476,11.033L8.511,12.875L8.166,13.406L8.605,13.242L8.749,14.646L9.284,15.081L10.082,16.045L11.177,15.566L11.889,16.177L12.802,15.394L13.163,14.869L13.094,13.623L13.494,13.182L14.479,13.72L13.502,11.86L13.865,10.953L14.578,10.341L14.613,9.443L15.341,8.861L14.65,9.114L13.552,7.261L11.268,8.522L9.254,7.392L8.442,6.009V8.994L7.689,9.576L7.817,10.409ZM7.609,9.464L8.171,9.029H6.837L7.609,9.464ZM6.847,9.193L7.645,10.196L7.552,9.59L6.847,9.193ZM13.603,7.085L11.3,3.849L13.461,5.289L14.569,6.723L13.603,7.085ZM14.715,6.687L15.584,6.717L14.948,4.743L13.619,5.269L14.715,6.687ZM13.517,5.161L14.694,4.695L12.531,4.503L13.517,5.161ZM12.291,4.344L12.47,4.36L12.163,3.927L11.644,3.912L12.291,4.344ZM15.751,6.907L15.765,7.652L17.159,7.68L15.751,6.907ZM17.38,7.822L15.933,7.793L17.576,8.882L17.38,7.822ZM13.798,7.405L14.71,8.945L15.566,8.631L13.798,7.405ZM15.445,8.954L14.725,10.116L14.748,9.512L15.445,8.954ZM14.764,10.314L15.73,8.756L17.77,10.564L14.764,10.314ZM17.148,10.65L14.669,10.445L14.129,10.907L17.148,10.65ZM11.828,16.306L11.587,16.099L11.886,16.502L11.828,16.306ZM8.729,14.808L8.525,15.852L9.136,15.139L8.729,14.808ZM8.116,13.23L8.373,12.835L8.353,11.81L8.116,13.23ZM8.243,11.628L7.926,13.536L7.769,15.444L7.072,14.399L8.243,11.628ZM7.263,14.933L7.816,16.428L7.754,15.67L7.263,14.933ZM9.701,17.501L7.355,17.35L7.935,16.997L9.701,17.501ZM7.012,17.246L5.96,13.8V16.574L7.012,17.246ZM5.884,16.689L4.589,17.213L6.877,17.323L5.884,16.689ZM5.631,5.135L5.822,5.868L4.327,5.135H5.631ZM9.442,10.098L8.442,9.34H9.752L10.165,9.822V10.374L9.855,9.994H9.717L9.442,10.098ZM12.095,11.201V10.098L12.647,9.34H13.957L13.061,10.029H12.509L12.302,10.305L12.095,11.201ZM10.234,11.476L10.096,11.683L11.13,12.166V12.717L10.131,13.338H9.821V13.786L10.751,13.131H11.647L12.578,13.82V13.338H12.233L11.268,12.717V12.166L12.302,11.683L12.164,11.476H10.234ZM11.13,14.441L10.717,14.854H11.647L11.234,14.441H11.13Z" />
</group>
</vector>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#E8E8E8"/>
<path
android:pathData="M5.754,5.072H4.134L5.892,6.007V10.476L6.615,8.882H8.201L8.27,6.769L9.235,7.323L11.234,8.432L13.095,7.462L13.922,7.878L14.715,9.09H15.301L14.198,9.644V10.372L13.612,10.995L13.095,11.757L13.715,12.589L13.509,12.935L14.094,13.489L13.509,13.212L13.095,13.731V14.805L11.992,16.399L11.061,15.741L10.165,16.052L9.166,15.221L8.27,15.394L8.408,13.489H8.028L8.477,12.866L8.408,11.099L7.615,12.519L5.892,13.316V16.641L4.41,17.403L7.408,17.542L11.234,17.715L12.44,17.403L12.199,16.884L16.025,15.914L16.404,14.979L17.61,13.731L17.231,12.104L17.92,10.995L17.231,9.471L17.403,8.778L17.231,7.982L16.68,7.704L16.438,6.873L15.611,6.769L15.542,6.596L14.991,5.799L14.715,5.072L12.819,4.76L12.095,4.31L10.889,3.825L10.545,4.414L5.754,5.072Z"
android:fillColor="#911413"/>
<path
android:pathData="M11.026,3.5L11.428,3.768L12.235,3.792L12.65,4.376L15.042,4.588L15.737,6.742L17.5,7.709L18.236,11.706L17.647,12.295L17.785,13.814L16.566,14.894L16.183,15.938L13.214,17.354L13.032,17.68H10.335L7.129,17.474L7.128,17.473L4.268,17.336L4.246,17.203L5.823,16.566V13.338L5.823,13.337L5.857,10.684V6.039L4,5.128L4.03,4.997H5.677L10.516,3.864L11.026,3.5ZM10.28,4.061L5.974,5.069L8.356,5.68L10.28,4.061ZM8.217,5.786L7.488,6.332L6.131,9.944L6.946,5.46L8.217,5.786ZM6.812,5.426L5.781,5.162L5.995,5.988V9.919L6.812,5.426ZM5.993,10.811L5.963,13.146L7.237,11.629L5.993,10.811ZM7.224,11.859L6.154,13.132L7.071,12.683L7.224,11.859ZM7.225,12.608L7.499,12.474L7.352,11.926L7.225,12.608ZM7.343,11.535L8.187,11.099L6.227,10.8L7.343,11.535ZM6.415,10.689L7.729,10.515L8.2,10.962L6.415,10.689ZM8.232,11.231L7.42,11.65L7.603,12.331L8.232,11.231ZM7.894,12.1L7.63,12.563L6.01,13.356L6.97,14.287L7.894,12.1ZM6.935,14.445L6.035,13.573L7.172,17.3L7.841,16.893L6.935,14.445ZM7.99,16.869L10.019,17.449L8.366,16.15L8.365,16.149L7.903,15.796L7.99,16.869ZM7.893,15.615L8.059,13.594L8.486,13.434L8.614,14.679L8.361,15.973L7.893,15.615ZM8.507,16.085L10.162,17.386L9.216,15.258L8.507,16.085ZM9.502,15.56L10.382,17.543H11.06L10.008,16.172L9.502,15.56ZM10.17,16.157L11.127,17.404L11.098,15.752L10.17,16.157ZM11.238,15.86L11.267,17.531L12.351,17.36L11.238,15.86ZM12.505,17.336L13,17.257L12.458,16.818L12.177,16.894L12.505,17.336ZM12.108,16.769L12.326,16.711L12.017,16.46L12.108,16.769ZM12.064,16.321L12.492,16.668L15.158,16.044L12.064,16.321ZM15.522,16.101L12.627,16.778L13.174,17.22L15.522,16.101ZM13.035,17.392L12.078,17.543H12.951L13.035,17.392ZM16.101,15.761L16.448,14.814L17.226,14.125L15.448,15.079L16.101,15.761ZM15.601,14.84L17.641,13.747L17.522,12.446L15.601,14.84ZM15.31,15.135L15.979,15.832L12.097,16.18L12.884,15.505L15.31,15.135ZM13.001,15.348L15.146,15.02L13.24,13.78L13.303,14.908L13.001,15.348ZM13.269,13.634L15.127,14.843L14.596,13.941L13.523,13.355L13.269,13.634ZM14.706,13.856L15.241,14.764L14.976,11.689L14.253,12.994L14.706,13.856ZM14.177,12.848L14.885,11.568L13.678,11.899L14.177,12.848ZM13.693,11.752L13.97,11.059L17.229,10.782L13.693,11.752ZM17.824,10.761L15.099,11.509L15.386,14.842L17.824,10.761ZM15.907,14.238L17.524,12.223L18.087,11.66L17.936,10.842L15.907,14.238ZM17.867,10.465L17.61,9.071L15.769,7.85L15.783,8.619L17.867,10.465ZM15.643,8.517L15.613,6.921L14.49,7.717L15.643,8.517ZM14.37,7.634L15.474,6.851L14.692,6.824L13.725,7.187L14.37,7.634ZM13.477,7.145L11.108,3.816L11.332,8.33L13.477,7.145ZM11.193,8.322L10.972,3.869L9.393,7.312L11.193,8.322ZM9.296,7.192L10.856,3.79L10.587,3.983L8.462,5.77L9.296,7.192ZM8.304,5.893L7.627,6.401L8.304,8.519V5.893ZM8.279,8.892L7.541,6.585L6.674,8.892H8.279ZM6.596,9.099L6.031,10.6L7.623,10.39L6.596,9.099ZM7.817,10.409L8.476,11.033L8.511,12.875L8.166,13.406L8.605,13.242L8.749,14.646L9.284,15.081L10.082,16.045L11.177,15.566L11.889,16.177L12.802,15.394L13.163,14.869L13.094,13.623L13.494,13.182L14.479,13.72L13.502,11.86L13.865,10.953L14.578,10.341L14.613,9.443L15.341,8.861L14.65,9.114L13.552,7.261L11.268,8.522L9.254,7.392L8.442,6.009V8.994L7.689,9.576L7.817,10.409ZM7.609,9.464L8.171,9.029H6.837L7.609,9.464ZM6.847,9.193L7.645,10.196L7.552,9.59L6.847,9.193ZM13.603,7.085L11.3,3.849L13.461,5.289L14.569,6.723L13.603,7.085ZM14.715,6.687L15.584,6.717L14.948,4.743L13.619,5.269L14.715,6.687ZM13.517,5.161L14.694,4.695L12.531,4.503L13.517,5.161ZM12.291,4.344L12.47,4.36L12.163,3.927L11.644,3.912L12.291,4.344ZM15.751,6.907L15.765,7.652L17.159,7.68L15.751,6.907ZM17.38,7.822L15.933,7.793L17.576,8.882L17.38,7.822ZM13.798,7.405L14.71,8.945L15.566,8.631L13.798,7.405ZM15.445,8.954L14.725,10.116L14.748,9.512L15.445,8.954ZM14.764,10.314L15.73,8.756L17.77,10.564L14.764,10.314ZM17.148,10.65L14.669,10.445L14.129,10.907L17.148,10.65ZM11.828,16.306L11.587,16.099L11.886,16.502L11.828,16.306ZM8.729,14.808L8.525,15.852L9.136,15.139L8.729,14.808ZM8.116,13.23L8.373,12.835L8.353,11.81L8.116,13.23ZM8.243,11.628L7.926,13.536L7.769,15.444L7.072,14.399L8.243,11.628ZM7.263,14.933L7.816,16.428L7.754,15.67L7.263,14.933ZM9.701,17.501L7.355,17.35L7.935,16.997L9.701,17.501ZM7.012,17.246L5.96,13.8V16.574L7.012,17.246ZM5.884,16.689L4.589,17.213L6.877,17.323L5.884,16.689ZM5.631,5.135L5.822,5.868L4.327,5.135H5.631ZM9.442,10.098L8.442,9.34H9.752L10.165,9.822V10.374L9.855,9.994H9.717L9.442,10.098ZM12.095,11.201V10.098L12.647,9.34H13.957L13.061,10.029H12.509L12.302,10.305L12.095,11.201ZM10.234,11.476L10.096,11.683L11.13,12.166V12.717L10.131,13.338H9.821V13.786L10.751,13.131H11.647L12.578,13.82V13.338H12.233L11.268,12.717V12.166L12.302,11.683L12.164,11.476H10.234ZM11.13,14.441L10.717,14.854H11.647L11.234,14.441H11.13Z"
android:fillColor="#C79960"
android:fillType="evenOdd"/>
</group>
</vector>

View file

@ -138,7 +138,13 @@ internal class DefaultCurrenciesRepository(
storeAndPushTokens(
userWalletId = userWalletId,
response = savedCurrencies.copy(
tokens = savedCurrencies.tokens.filter { it != token },
tokens = savedCurrencies.tokens.filterNot {
// it's better to compare by fields, to support renaming and etc
it.contractAddress == token.contractAddress &&
it.networkId == token.networkId &&
it.derivationPath == token.derivationPath &&
it.decimals == token.decimals
},
),
)
}

View file

@ -260,5 +260,5 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5
private val excludedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Ducatus,
Blockchain.Playa3ull,
)

View file

@ -321,6 +321,6 @@ data class CardDTO(
}
companion object {
const val RING_BATCH_ID = "AC17"
val RING_BATCH_IDS = listOf("AC17", "BA01")
}
}

View file

@ -12,7 +12,9 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import kotlin.collections.set
class ApplyTokenListSortingUseCase(
private val currenciesRepository: CurrenciesRepository,
@ -25,11 +27,17 @@ class ApplyTokenListSortingUseCase(
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
): Either<TokenListSortingError, Unit> {
return withContext(dispatchers.default) {
either {
return either {
val storedCurrencies = getCurrencies(userWalletId)
val isSortingTypeChanged = checkIsCurrenciesSortedByBalance(userWalletId) != isSortedByBalance
val isGroupingTypeChanged = checkIsCurrenciesGroupedByNetwork(userWalletId) != isGroupedByNetwork
val sortedCurrencies = sortTokens(sortedTokensIds, storedCurrencies)
if (storedCurrencies != sortedCurrencies || isSortingTypeChanged || isGroupingTypeChanged) {
applySorting(
userWalletId = userWalletId,
tokens = sortTokens(sortedTokensIds, getCurrencies(userWalletId)),
currencies = sortedCurrencies,
isGrouped = isGroupedByNetwork,
isSortedByBalance = isSortedByBalance,
)
@ -37,17 +45,29 @@ class ApplyTokenListSortingUseCase(
}
}
private suspend fun Raise<TokenListSortingError>.checkIsCurrenciesSortedByBalance(userWalletId: UserWalletId) =
catch(
block = { currenciesRepository.isTokensSortedByBalance(userWalletId).firstOrNull() ?: false },
catch = { raise(TokenListSortingError.DataError(it)) },
)
private suspend fun Raise<TokenListSortingError>.checkIsCurrenciesGroupedByNetwork(userWalletId: UserWalletId) =
catch(
block = { currenciesRepository.isTokensGrouped(userWalletId).firstOrNull() ?: false },
catch = { raise(TokenListSortingError.DataError(it)) },
)
private suspend fun Raise<TokenListSortingError>.sortTokens(
sortedTokensIds: List<CryptoCurrency.ID>,
unsortedTokens: List<CryptoCurrency>,
sortedCurrenciesIds: List<CryptoCurrency.ID>,
unsortedCurrencies: List<CryptoCurrency>,
): List<CryptoCurrency> = withContext(dispatchers.default) {
val nonEmptySortedTokensIds = ensureNotNull(sortedTokensIds.toNonEmptySetOrNull()) {
val nonEmptySortedTokensIds = ensureNotNull(sortedCurrenciesIds.toNonEmptySetOrNull()) {
TokenListSortingError.TokenListIsEmpty
}
val sortedTokens = sortedMapOf<Int, CryptoCurrency>()
unsortedTokens.distinct().forEach { currency ->
unsortedCurrencies.distinct().forEach { currency ->
val index = nonEmptySortedTokensIds.indexOfFirst { currencyId ->
currencyId == currency.id
}
@ -79,12 +99,12 @@ class ApplyTokenListSortingUseCase(
private suspend fun Raise<TokenListSortingError>.applySorting(
userWalletId: UserWalletId,
tokens: List<CryptoCurrency>,
currencies: List<CryptoCurrency>,
isGrouped: Boolean,
isSortedByBalance: Boolean,
) = withContext(dispatchers.io) {
catch(
block = { currenciesRepository.saveTokens(userWalletId, tokens, isGrouped, isSortedByBalance) },
block = { currenciesRepository.saveTokens(userWalletId, currencies, isGrouped, isSortedByBalance) },
catch = { raise(TokenListSortingError.DataError(it)) },
)
}

View file

@ -54,7 +54,7 @@ internal class ApplyTokenListSortingUseCaseTest {
// When
val result = useCase(
userWalletId = userWalletId,
sortedTokensIds = MockTokens.tokens.map { it.id },
sortedTokensIds = MockTokens.tokens.map { it.id }.sortedByDescending { it.value },
isGroupedByNetwork = false,
isSortedByBalance = false,
)

View file

@ -6,20 +6,24 @@ import android.net.Uri
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.google.mlkit.vision.common.InputImage
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import com.tangem.feature.qrscanning.presentation.QrScanningContent
import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@ -67,22 +71,18 @@ internal class QrScanningFragment : ComposeFragment() {
override fun onDestroy() {
super.onDestroy()
setFitSystemWindows(fit = true)
cameraPermissionLauncher.unregister()
cameraExecutor.shutdown()
}
@Composable
override fun ScreenContent(modifier: Modifier) {
SystemBarsEffect {
setSystemBarsColor(Color.Transparent, darkIcons = false)
}
StatusBarTransparencyDisposable()
QrScanningContent(
executor = { cameraExecutor },
analyzer = { analyzer },
uiState = viewModel.uiState.collectAsStateWithLifecycle().value,
)
setFitSystemWindows(fit = false)
}
private fun requestCameraPermission() {
@ -96,14 +96,33 @@ internal class QrScanningFragment : ComposeFragment() {
}
}
private fun setFitSystemWindows(fit: Boolean) {
activity?.window?.let {
WindowCompat.setDecorFitsSystemWindows(it, fit)
}
}
companion object {
fun create() = QrScanningFragment()
}
}
@Composable
private fun StatusBarTransparencyDisposable() {
val systemUiController = rememberSystemUiController()
val systemBarsColor = TangemTheme.colors.background.secondary
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(Unit) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = false,
)
}
if (event == Lifecycle.Event.ON_STOP) {
systemUiController.setSystemBarsColor(systemBarsColor)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}

View file

@ -665,7 +665,9 @@ internal class SwapInteractorImpl @Inject constructor(
blockchain == Blockchain.Aptos -> {
Fee.Aptos(
amount = feeAmount,
gasUnitPrice = fee.feeValue.toLong() / fee.gasLimit,
gasUnitPrice = fee.feeValue.divide(BigDecimal(fee.gasLimit))
.movePointRight(Blockchain.Aptos.decimals())
.toLong(),
gasLimit = fee.gasLimit.toLong(),
)
}
@ -1358,7 +1360,7 @@ internal class SwapInteractorImpl @Inject constructor(
is Fee.Common -> 0
is Fee.Ethereum -> gasLimit.toInt()
is Fee.VeChain -> gasLimit.toInt()
is Fee.Aptos -> amount.longValue?.div(gasUnitPrice)?.toInt() ?: 0
is Fee.Aptos -> gasLimit.toInt()
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.feature.swap.models.states
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
sealed class ProviderState {
abstract val onProviderClick: ((String) -> Unit)?
@ -26,6 +28,7 @@ sealed class ProviderState {
val selectionType: SelectionType,
val additionalBadge: AdditionalBadge,
val percentLowerThenBest: PercentDifference = PercentDifference.Empty,
val namePrefix: PrefixType,
override val onProviderClick: (String) -> Unit,
) : ProviderState()
@ -39,17 +42,25 @@ sealed class ProviderState {
override val onProviderClick: ((String) -> Unit)? = null,
) : ProviderState()
@Immutable
sealed class AdditionalBadge {
object BestTrade : AdditionalBadge()
object Empty : AdditionalBadge()
object PermissionRequired : AdditionalBadge()
}
@Immutable
enum class SelectionType {
NONE, CLICK, SELECT
}
@Immutable
enum class PrefixType {
NONE, PROVIDED_BY
}
}
@Immutable
sealed class PercentDifference {
data class Value(val value: Float) : PercentDifference()
object Empty : PercentDifference()

View file

@ -116,6 +116,7 @@ private fun ChooseProviderBottomSheet_Preview() {
additionalBadge = ProviderState.AdditionalBadge.BestTrade,
percentLowerThenBest = PercentDifference.Value(-1.0f),
selectionType = ProviderState.SelectionType.SELECT,
namePrefix = ProviderState.PrefixType.NONE,
onProviderClick = {},
),
ProviderState.Unavailable(

View file

@ -17,11 +17,12 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
@ -110,12 +111,14 @@ private fun ProviderContentState(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
) {
Row {
Text(
text = stringResource(id = R.string.express_by_provider),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(end = TangemTheme.dimens.spacing4),
)
if (state.namePrefix == ProviderState.PrefixType.PROVIDED_BY) {
Text(
text = stringResource(id = R.string.express_by_provider),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(end = TangemTheme.dimens.spacing4),
)
}
AnimatedContent(targetState = state.name, label = "") {
Text(
text = it,
@ -390,75 +393,76 @@ private fun PermissionBadgeItem(modifier: Modifier = Modifier) {
}
}
@Preview
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ProviderItem_Loading_Preview() {
Column {
TangemTheme(isDark = false) {
ProviderItemBlock(state = ProviderState.Loading())
}
SpacerH24()
TangemTheme(isDark = true) {
ProviderItemBlock(state = ProviderState.Loading())
}
private fun ProviderItemPreview_Light(
@PreviewParameter(ProviderItemParameterProvider::class) state: Pair<ProviderState, Boolean>,
) {
TangemTheme {
ProviderItem(
modifier = Modifier.background(TangemTheme.colors.background.action),
state = state.first,
isSelected = state.second,
)
}
}
@Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ProviderItem_Content_Preview() {
val state = ProviderState.Content(
id = "1",
name = "1inch",
type = "DEX",
iconUrl = "",
subtitle = stringReference("1 000 000"),
additionalBadge = ProviderState.AdditionalBadge.PermissionRequired,
percentLowerThenBest = PercentDifference.Value(12.0f),
selectionType = ProviderState.SelectionType.SELECT,
onProviderClick = {},
)
Column {
TangemTheme(isDark = false) {
ProviderItemBlock(state = state)
}
SpacerH24()
TangemTheme(isDark = true) {
ProviderItemBlock(state = state)
}
private fun ProviderItemPreview_Dark(
@PreviewParameter(ProviderItemParameterProvider::class) state: Pair<ProviderState, Boolean>,
) {
TangemTheme(isDark = true) {
ProviderItem(
modifier = Modifier.background(TangemTheme.colors.background.action),
state = state.first,
isSelected = state.second,
)
}
}
@Preview
@Composable
private fun ProviderItem_Unavailable_Preview() {
val state = ProviderState.Unavailable(
id = "1",
name = "1inch",
type = "DEX",
iconUrl = "",
selectionType = ProviderState.SelectionType.SELECT,
alertText = stringReference("Unavailable"),
)
Column {
TangemTheme(isDark = false) {
ProviderItemBlock(state = state)
}
private class ProviderItemParameterProvider : CollectionPreviewParameterProvider<Pair<ProviderState, Boolean>>(
collection = buildList {
val contentState = ProviderState.Content(
id = "1",
name = "1inch",
type = "DEX",
iconUrl = "",
subtitle = stringReference(value = "0,64554846 DAI ≈ 1 MATIC"),
additionalBadge = ProviderState.AdditionalBadge.Empty,
percentLowerThenBest = PercentDifference.Value(value = 12.0f),
selectionType = ProviderState.SelectionType.SELECT,
namePrefix = ProviderState.PrefixType.PROVIDED_BY,
onProviderClick = {},
)
val contentState2 = contentState.copy(
subtitle = stringReference(value = "1 132,46 MATIC"),
additionalBadge = ProviderState.AdditionalBadge.PermissionRequired,
percentLowerThenBest = PercentDifference.Value(value = 5f),
)
val unavailableState = ProviderState.Unavailable(
id = "1",
name = "1inch",
type = "DEX",
iconUrl = "",
alertText = stringReference(value = "Not available"),
selectionType = ProviderState.SelectionType.SELECT,
onProviderClick = {},
)
val loadingState = ProviderState.Loading()
SpacerH24()
add(contentState to true)
add(contentState to false)
TangemTheme(isDark = true) {
ProviderItemBlock(state = state)
}
add(contentState2 to true)
add(contentState2 to false)
SpacerH24()
add(unavailableState to true)
add(unavailableState to false)
TangemTheme(isDark = true) {
ProviderItem(state = state, isSelected = true)
}
}
}
add(loadingState to true)
add(loadingState to false)
},
)
// endregion Preview

View file

@ -1287,6 +1287,7 @@ internal class StateBuilder(
additionalBadge = badge,
selectionType = selectionType,
percentLowerThenBest = PercentDifference.Empty,
namePrefix = ProviderState.PrefixType.PROVIDED_BY,
onProviderClick = onProviderClick,
)
}
@ -1318,6 +1319,7 @@ internal class StateBuilder(
percentLowerThenBest = pricesLowerBest[this.providerId]?.let { percent ->
PercentDifference.Value(percent)
} ?: PercentDifference.Value(0f),
namePrefix = ProviderState.PrefixType.NONE,
onProviderClick = onProviderClick,
)
}
@ -1352,6 +1354,11 @@ internal class StateBuilder(
subtitle = alertText,
additionalBadge = ProviderState.AdditionalBadge.Empty,
percentLowerThenBest = PercentDifference.Empty,
namePrefix = if (selectionType != ProviderState.SelectionType.SELECT) {
ProviderState.PrefixType.PROVIDED_BY
} else {
ProviderState.PrefixType.NONE
},
onProviderClick = onProviderClick,
)
}
@ -1377,7 +1384,7 @@ internal class StateBuilder(
}
private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String {
return "${this.formatToUIRepresentation()} ${token.symbol}"
return BigDecimalFormatter.formatCryptoAmount(value, token.symbol, token.decimals)
}
private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal {

View file

@ -147,9 +147,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
ifLeft = stateHolder::updateStateWithError,
ifRight = {
stateHolder.updateStateToHideProgress()
withContext(
dispatchers.main,
) { router.popBackStack() }
withContext(dispatchers.main) { router.popBackStack() }
},
)
}

View file

@ -13,11 +13,14 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
internal typealias MaybeTokenListFlow = Flow<Either<TokenListError, TokenList>>
@ -32,24 +35,25 @@ internal abstract class BasicTokenListSubscriber(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
) : WalletSubscriber() {
protected abstract fun tokenListFlow(): MaybeTokenListFlow
private val sendAnalyticsJobHolder = JobHolder()
private val onTokenListReceivedJobHolder = JobHolder()
protected open suspend fun onTokenListReceived(tokenList: TokenList) { /* no-op */
}
protected abstract fun tokenListFlow(): MaybeTokenListFlow
override fun create(coroutineScope: CoroutineScope): Flow<*> {
return combine(
flow = tokenListFlow()
.onEach { maybeTokenList ->
val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId)
tokenListAnalyticsSender.send(
displayedUiState = displayedState,
userWallet = userWallet,
tokenList = maybeTokenList.getOrElse { return@onEach },
)
coroutineScope.launch {
sendTokenListAnalytics(maybeTokenList)
}.saveIn(sendAnalyticsJobHolder)
}
.distinctUntilChanged(),
.distinctUntilChanged()
.onEach { maybeTokenList ->
coroutineScope.launch {
onTokenListReceived(maybeTokenList)
}.saveIn(onTokenListReceivedJobHolder)
},
flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
transform = { maybeTokenList, maybeAppCurrency ->
val tokenList = maybeTokenList.getOrElse { e ->
@ -64,11 +68,24 @@ internal abstract class BasicTokenListSubscriber(
updateContent(tokenList, appCurrency)
walletWithFundsChecker.check(tokenList)
onTokenListReceived(tokenList)
},
)
}
protected open suspend fun onTokenListReceived(maybeTokenList: Either<TokenListError, TokenList>) {
/* no-op */
}
private suspend fun sendTokenListAnalytics(maybeTokenList: Either<TokenListError, TokenList>) {
val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId)
tokenListAnalyticsSender.send(
displayedUiState = displayedState,
userWallet = userWallet,
tokenList = maybeTokenList.getOrElse { return },
)
}
private fun updateContent(tokenList: TokenList, appCurrency: AppCurrency) {
stateHolder.update(
SetTokenListTransformer(

View file

@ -1,8 +1,12 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
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.TokenList
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
@ -31,28 +35,34 @@ internal class MultiWalletTokenListSubscriber(
override fun tokenListFlow(): MaybeTokenListFlow = getTokenListUseCase(userWallet.walletId)
override suspend fun onTokenListReceived(tokenList: TokenList) {
updateSortingIfNeeded(tokenList)
override suspend fun onTokenListReceived(maybeTokenList: Either<TokenListError, TokenList>) {
updateSortingIfNeeded(maybeTokenList)
}
private suspend fun updateSortingIfNeeded(tokenList: TokenList) {
if (tokenList.totalFiatBalance is TokenList.FiatBalance.Loading ||
tokenList.sortedBy == TokenList.SortType.NONE
) {
return
}
private suspend fun updateSortingIfNeeded(maybeTokenList: Either<TokenListError, TokenList>) {
val tokenList = maybeTokenList.getOrElse { return }
if (!checkNeedSorting(tokenList)) return
applyTokenListSortingUseCase(
userWalletId = userWallet.walletId,
sortedTokensIds = 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 -> return
},
sortedTokensIds = getCurrenciesIds(tokenList),
isGroupedByNetwork = tokenList is TokenList.GroupedByNetwork,
isSortedByBalance = true,
isSortedByBalance = tokenList.sortedBy == TokenList.SortType.BALANCE,
)
}
private fun checkNeedSorting(tokenList: TokenList): Boolean {
return tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading &&
tokenList.sortedBy == TokenList.SortType.BALANCE
}
private fun getCurrenciesIds(tokenList: TokenList): List<CryptoCurrency.ID> {
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()
}
}
}

View file

@ -85,7 +85,7 @@ web3j = "4.10.1"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-503"
tangemBlockchainSdk = "develop-505"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-324"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^