diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt
index d5328878eb..e122327ff8 100644
--- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt
+++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt
@@ -4,20 +4,20 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.key
+import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState
import kotlinx.collections.immutable.ImmutableCollection
@@ -41,10 +41,7 @@ internal fun BriefNetworksList(
exit = fadeOut(),
) {
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
- val iterator = networks.iterator()
- var index = 0
- while (iterator.hasNext()) {
- val network = iterator.next()
+ for ((index, network) in networks.withIndex()) {
if (index < MAX_VISIBLE_BRIEF_ICONS) {
key(network.name + network.protocolName) {
BriefNetworkItem(model = network)
@@ -59,7 +56,6 @@ internal fun BriefNetworksList(
break
}
}
- index++
}
}
}
@@ -109,8 +105,16 @@ internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modi
}
}
+@Suppress("MagicNumber")
@Composable
internal fun HasMoreItem(moreCount: Int) {
+ val count = if (moreCount > 99) 99 else moreCount
+ val themeTextStyle = TangemTheme.typography.overline.copy(
+ letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp),
+ )
+ var textStyle by remember(themeTextStyle) { mutableStateOf(themeTextStyle) }
+ var readyToDraw by remember(themeTextStyle) { mutableStateOf(false) }
+
Box(
modifier = Modifier
.size(size = TangemTheme.dimens.size20)
@@ -118,10 +122,20 @@ internal fun HasMoreItem(moreCount: Int) {
.background(TangemTheme.colors.control.unchecked),
) {
Text(
- modifier = Modifier.align(Alignment.Center),
- text = "+$moreCount",
- style = TangemTheme.typography.overline,
- color = TangemTheme.colors.text.tertiary,
+ modifier = Modifier
+ .padding(TangemTheme.dimens.spacing4)
+ .align(Alignment.Center)
+ .drawWithContent { if (readyToDraw) drawContent() },
+ text = "+$count",
+ style = textStyle,
+ overflow = TextOverflow.Clip,
+ onTextLayout = { textLayoutResult ->
+ if (textLayoutResult.didOverflowHeight) {
+ textStyle = textStyle.copy(fontSize = textStyle.fontSize * 0.9)
+ } else {
+ readyToDraw = true
+ }
+ },
)
}
}
\ No newline at end of file
diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml
index b4bd309592..5d25d41f8e 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -427,7 +427,7 @@
Уже содержится в введенном адресе
Вычесть
Недостаточно средств для покрытия комиссии сети. Вычесть недостающую сумму для покрытия комиссии из отправляемой суммы?
- Сумма комиссии в %@ раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.
+ Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.
Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить?
Причина: %1$s\nКод: %2$s
Транзакция не выполнена
@@ -537,9 +537,9 @@
Балансы скрыты
Балансы показаны
Отменить
+ Выбранная операция в данный момент недоступна. Попробуйте позже.
В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением.
У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства.
- Выбранная операция в данный момент недоступна. Попробуйте позже.
Обмен %s не доступен. Но мы работаем над его добавлением.
В данный момент продажа монеты %s недоступна. Но мы работаем над её добавлением.
Сгенерировать XPUB
@@ -548,6 +548,7 @@
Скрыть %s
Скрыть токен
%1$s токен в сети %%image%% %2$s
+ Токен в сети %%image%% %1$s
Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.
Невозможно скрыть %s
Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля.
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index 9310a0153a..a099c577d4 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -534,9 +534,9 @@
Balances hidden
Balances shown
Undo
+ This operation is currently unavailable. Please try again later.
The purchase of the %s is currently unavailable. But we are working on adding it.
You do not have funds to send. Top up your account to be able to send funds from it.
- This operation is currently unavailable. Please try again later.
%s swap is not available. But we are working on adding it.
Sell of the %s coin is currently unavailable. But we are working on adding it.
Generate XPUB
@@ -545,6 +545,7 @@
Hide %s
Hide token
%1$s token in %%image%% %2$s network
+ Token in %%image%% %1$s network
The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list.
Unable to hide %s
Exchange this token for another at %1$s service fees from February %2$s-%3$s.
diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt
index 7db27cf7f4..5006df1d7a 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt
@@ -11,6 +11,9 @@ import java.util.Locale
private const val TEXT_CHUNK_THOUSAND = 3
private const val POINT_SEPARATOR = '.'
+private const val COMMA_SEPARATOR = ','
+private const val SCIENTIFIC_NOTATION = 'e'
+const val DECIMAL_SEPARATOR_LIMIT = 1
@Composable
fun rememberDecimalFormat(): DecimalFormat {
@@ -152,6 +155,47 @@ fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = Round
}
}
+/**
+ * Universal amount string parser to [BigDecimal]
+ * Able to parse values with only ONE separator, assuming separator is COMMA.
+ * Otherwise returns null.
+ */
+fun String.parseBigDecimalOrNull() = runCatching {
+ // Filtering value containing more than one either grouping or decimal separator.
+ // We assume there will be only decimal separator, otherwise parsing will fail.
+
+ // Step 1. Exclude formatted (100,000.0) except scientific notation (100.000e10)
+ val excludeFormatted = this.count {
+ !it.isDigit() && !it.equals(SCIENTIFIC_NOTATION, ignoreCase = true)
+ } > DECIMAL_SEPARATOR_LIMIT
+
+ // Step 2. Exclude wrong scientific notation (100e100e100)
+ val excludeWrongScientific = this.count {
+ it.equals(SCIENTIFIC_NOTATION, ignoreCase = true)
+ } > DECIMAL_SEPARATOR_LIMIT
+ if (excludeFormatted || excludeWrongScientific) return null
+
+ // An attempt to parse value with POINT decimal separator
+ val parsed = this.toBigDecimalOrNull()
+
+ if (parsed == null) {
+ // If parsing with POINT separator fails trying to parse with COMMA separator
+ val decimalFormatSymbol = DecimalFormatSymbols().apply {
+ decimalSeparator = COMMA_SEPARATOR
+ }
+ val decimalFormat = DecimalFormat().apply {
+ decimalFormatSymbols = decimalFormatSymbol
+ isParseBigDecimal = true
+ }
+
+ // Return either number or null if fails
+ decimalFormat.parse(this) as? BigDecimal
+ } else {
+ // If parsing with POINT separator succeeds return number
+ parsed
+ }
+}.getOrNull()
+
private fun Int.getWithIntegerDecimals(before: String, separator: Char, after: String): String = if (this == 0) {
before
} else {
diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt
index 87729e8bc4..cce12822c5 100644
--- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt
+++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt
@@ -1,6 +1,7 @@
package com.tangem.data.qrscanning.repository
import com.tangem.blockchain.common.Blockchain
+import com.tangem.core.ui.utils.parseBigDecimalOrNull
import com.tangem.domain.qrscanning.models.QrResult
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
@@ -44,7 +45,7 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
when (it.key) {
Parameter.Amount -> {
// According to BIP-0021, the value is specified in decimals. No conversion needed
- result.amount = it.value.toBigDecimalOrNull()
+ result.amount = it.value.parseBigDecimalOrNull()
}
Parameter.Message,
Parameter.Memo,
@@ -52,16 +53,17 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
result.memo = URLDecoder.decode(it.value, "UTF-8")
}
Parameter.Address -> {
+ // If 'address' parameter is exists, then currency must be TOKEN.
+ val tokenCurrency = cryptoCurrency as? CryptoCurrency.Token ?: return QrResult()
+
// Overrides destination address for token transfers (ERC-681)
- if (cryptoCurrency is CryptoCurrency.Token) {
- // `address` parameter is used only if the contract address, encoded in the QR,
- // matches the contract address of the token.
- // Otherwise, the scanned string is likely malformed, and we stop the entire parsing routine
- if (cryptoCurrency.contractAddress.equals(address, ignoreCase = true)) {
- result.address = it.value
- } else {
- return QrResult()
- }
+ // `address` parameter is used only if the contract address, encoded in the QR,
+ // matches the contract address of the token.
+ // Otherwise, the scanned string is likely malformed, and we stop the entire parsing routin
+ if (tokenCurrency.contractAddress.equals(address, ignoreCase = true)) {
+ result.address = it.value
+ } else {
+ return QrResult()
}
}
Parameter.Value,
@@ -69,7 +71,7 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
-> {
// Extra convert parses scientific notation to decimal
// This is necessary to be able comparing BigDecimal values
- result.amount = it.value.toBigDecimalOrNull()
+ result.amount = it.value.parseBigDecimalOrNull()
?.toPlainString()?.toBigDecimalOrNull()
?.divide(BigDecimal.TEN.pow(cryptoCurrency.decimals))
}
diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt
index ad1f6244a7..3b3cd6df38 100644
--- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt
+++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt
@@ -152,11 +152,6 @@ internal class DefaultQrScanningEventsRepositoryTest {
QrResult(address = address2),
cryptoCurrency,
)
- positiveCase(
- "$garbage$schema2:$address2$function?$addressParam=$addressParamValue",
- QrResult(address = address2),
- cryptoCurrency,
- )
positiveCase(
"$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$someAmountParamValue",
QrResult(address = address2),
@@ -177,6 +172,11 @@ internal class DefaultQrScanningEventsRepositoryTest {
QrResult(address = address2, amount = BigDecimal("0.000000023")),
cryptoCurrency,
)
+ negativeCase(
+ "$garbage$schema2:$address2$function?$addressParam=$addressParamValue",
+ QrResult(address = address2),
+ cryptoCurrency,
+ )
}
@Test
@@ -232,11 +232,21 @@ internal class DefaultQrScanningEventsRepositoryTest {
QrResult(address = address2, amount = BigDecimal("2300")),
tokenCryptoCurrency,
)
+ positiveCase(
+ "$address4?$addressParam=$addressParamValue",
+ QrResult(address = addressParamValue),
+ tokenCryptoCurrency,
+ )
negativeCase(
"$address2?$someParam=$someParamValue&$amountParam=$amountParamValue",
QrResult(address = address2, amount = BigDecimal("123.123"), memo = memoParamValueUtf8),
tokenCryptoCurrency,
)
+ negativeCase(
+ "$address4?$addressParam=$addressParamValue",
+ QrResult(address = addressParamValue),
+ cryptoCurrency,
+ )
}
private fun positiveCase(input: String, expected: QrResult, cryptoCurrency: CryptoCurrency) {
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt
index cebe88712f..64e8f1e992 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt
@@ -92,9 +92,9 @@ internal class SendFragment : ComposeFragment() {
delay(QR_SCAN_DELAY)
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
- // If do not use the delay, then etAmount error field is not displayed when
+ // If do not use the delay, then error field is not displayed when
// inserting an incorrect amount by shareUri
- viewModel.onRecipientAddressScanned(it)
+ viewModel.onQrCodeScanned(it)
}
}
}
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt
index 2d397422cf..ffad3f3633 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt
@@ -569,12 +569,14 @@ internal class SendViewModel @Inject constructor(
// endregion
// region recipient state clicks
- fun onRecipientAddressScanned(address: String) {
+ fun onQrCodeScanned(address: String) {
viewModelScope.launch(dispatchers.main) {
parseQrCodeUseCase(address, cryptoCurrency).fold(
ifRight = { parsedCode ->
onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode)
- parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) }
+ parsedCode.amount?.let {
+ onAmountValueChange(it.parseBigDecimal(decimals = cryptoCurrency.decimals))
+ }
parsedCode.memo?.let { onRecipientMemoValueChange(it) }
},
ifLeft = {
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt
index 77f27a4cac..69d878168f 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt
@@ -57,6 +57,21 @@ internal object TokenDetailsPreviewData {
),
)
+ val tokenInfoBlockStateWithLongNameNoStandard = TokenInfoBlockState(
+ name = "Tether (USDT) with long name test",
+ iconState = TokenInfoBlockState.IconState.TokenIcon(
+ url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
+ fallbackTint = Color.Cyan,
+ fallbackBackground = Color.Blue,
+ isGrayscale = false,
+ ),
+ currency = TokenInfoBlockState.Currency.Token(
+ standardName = null,
+ networkIcon = R.drawable.img_shibarium_22,
+ networkName = "Shibarium",
+ ),
+ )
+
val tokenInfoBlockState = TokenInfoBlockState(
name = "Tether USDT",
iconState = TokenInfoBlockState.IconState.CustomTokenIcon(
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt
index 7c1e1eda2b..f0ec9e8d43 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt
@@ -19,7 +19,7 @@ internal data class TokenInfoBlockState(
* @param networkIcon - token's network icon.
*/
data class Token(
- val standardName: String,
+ val standardName: String?,
val networkName: String,
@DrawableRes val networkIcon: Int,
) : Currency()
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt
index dbc7ff632b..163a1516ae 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt
@@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.model.CryptoCurrency
+import com.tangem.domain.tokens.model.Network
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig
@@ -40,7 +41,7 @@ internal class TokenDetailsSkeletonStateConverter(
currency = when (value) {
is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native
is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token(
- standardName = value.network.standardType.name,
+ standardName = value.network.standardType.getSpecifiedNameOrNull(),
networkName = value.network.name,
networkIcon = value.networkIconResId,
)
@@ -65,6 +66,9 @@ internal class TokenDetailsSkeletonStateConverter(
)
}
+ private fun Network.StandardType.getSpecifiedNameOrNull(): String? =
+ name.takeIf { this !is Network.StandardType.Unspecified }
+
private fun createMenu(cryptoCurrency: CryptoCurrency): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig(
items = buildList {
if (featureToggles.isGenerateXPubEnabled() && isBitcoin(cryptoCurrency.network.id.value)) {
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt
index 3254286ed2..3982a8dc21 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt
@@ -105,13 +105,22 @@ private const val SEPARATOR = " %image% "
@Composable
private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): ExtractedTokenNetworkText {
- val splitString = stringResource(
- id = R.string.token_details_token_type_subtitle,
- formatArgs = arrayOf(
- tokenCurrency.standardName,
- tokenCurrency.networkName,
- ),
- ).split(SEPARATOR)
+ val splitString = if (tokenCurrency.standardName != null) {
+ stringResource(
+ id = R.string.token_details_token_type_subtitle,
+ formatArgs = arrayOf(
+ tokenCurrency.standardName,
+ tokenCurrency.networkName,
+ ),
+ ).split(SEPARATOR)
+ } else {
+ stringResource(
+ id = R.string.token_details_token_type_subtitle_no_standard,
+ formatArgs = arrayOf(
+ tokenCurrency.networkName,
+ ),
+ ).split(SEPARATOR)
+ }
return remember(splitString) {
ExtractedTokenNetworkText(
@@ -153,5 +162,6 @@ private class TokenInfoStateProvider : CollectionPreviewParameterProvider