diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 533cc30161..8be4d56fef 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -500,7 +500,7 @@ Последние Получатель Неверный адрес - Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов + Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов. Отправить Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Мои кошельки @@ -509,7 +509,7 @@ Отправка Нажмите на любое поле, чтобы изменить его Отправка %s - Вы отправляете %1$s, включая комиссию сети %2$s + Вы отправляете **%1$s**, включая комиссию сети %2$s Отправка %s Всего %1$s и %2$s будет отправлено diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 53b1c0d5b8..50421fa1b7 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -499,7 +499,7 @@ Recent Recipient Not a valid address - Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens + Ensure the receiving wallet address is on the %s network to avoid losing your tokens Send to A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds My wallets @@ -508,7 +508,7 @@ Sending... Tap any field to change it Send %s - You are sending %1$s including a network fee of %2$s + You are sending **%1$s** including a network fee of %2$s Sending %s Total %1$s and %2$s will be sent diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index e2abfa0f4b..3129c8917b 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -45,4 +45,5 @@ dependencies { implementation(deps.zxing.qrCore) implementation(deps.jodatime) implementation(deps.timber) + implementation(deps.markdown) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index c095c2b8fd..521245b940 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -94,10 +94,14 @@ fun AmountTextField( SimpleTextField( value = value, onValueChange = { newText -> - if (decimalFormat.isValidSymbols(newText)) { - val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals) - onValueChange(trimmed) - } + onValueChange( + prepareEnter( + oldValue = value, + newValue = newText, + decimalFormat = decimalFormat, + decimals = decimals, + ), + ) }, textStyle = textStyle.copy( fontSize = fontSize, @@ -116,8 +120,37 @@ fun AmountTextField( } } +private fun prepareEnter(oldValue: String, newValue: String, decimalFormat: DecimalFormat, decimals: Int): String { + val decimalSymbol = decimalFormat.decimalFormatSymbols.decimalSeparator + return if (decimalFormat.isValidSymbols(newValue)) { + val parsedValue = newValue.parseBigDecimalOrNull()?.toPlainString() + ?: if (newValue.isBlank()) "" else oldValue + val replacedWithSymbol = if (parsedValue.findLast { it != decimalSymbol } != null) { + when { + parsedValue.findLast { it == COMMA_SEPARATOR } != null -> { + parsedValue.replace(COMMA_SEPARATOR, decimalSymbol) + } + parsedValue.findLast { it == POINT_SEPARATOR } != null -> { + parsedValue.replace(POINT_SEPARATOR, decimalSymbol) + } + else -> parsedValue + } + } else { + parsedValue + } + val joinedSymbol = if (newValue.endsWith(COMMA_SEPARATOR) || newValue.endsWith(POINT_SEPARATOR)) { + replacedWithSymbol.plus(decimalSymbol) + } else { + replacedWithSymbol + } + decimalFormat.getValidatedNumberWithFixedDecimals(joinedSymbol, decimals) + } else { + oldValue + } +} + private fun DecimalFormat.isValidSymbols(text: String): Boolean { - return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text) + return checkDecimalSeparatorDuplicate(text) } // region preview diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt new file mode 100644 index 0000000000..408e9130ee --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt @@ -0,0 +1,56 @@ +package com.tangem.core.ui.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.getTextInNode +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor +import org.intellij.markdown.parser.MarkdownParser + +/** Markdown parser */ +@Composable +fun rememberMarkdownParser() = remember { + MarkdownParser(CommonMarkFlavourDescriptor()) +} + +/** + * Styling markdown tree recursively + * + * @param markdownText original text + * @param node current processed node + */ +@Composable +fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode): AnnotatedString.Builder { + when (node.type) { + MarkdownElementTypes.MARKDOWN_FILE, MarkdownElementTypes.PARAGRAPH -> { + node.children.forEach { childNode -> + appendMarkdown( + markdownText = markdownText, + node = childNode, + ) + } + } + MarkdownElementTypes.STRONG -> { + withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { + node.children + .drop(2) + .dropLast(2) + .forEach { childNode -> + appendMarkdown( + markdownText = markdownText, + node = childNode, + ) + } + } + } + else -> { + append(node.getTextInNode(markdownText).toString()) + } + } + return this +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 766815d328..2f22994f64 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -8,6 +8,9 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import org.intellij.markdown.MarkdownElementTypes /** * Utility class for creating text as [String] or [StringRes]. @@ -160,6 +163,29 @@ fun TextReference.resolveReference(resources: Resources): String { } } +/** Resolve [TextReference] as [AnnotatedString] */ +@Composable +fun TextReference.resolveAnnotatedReference(): AnnotatedString { + return when (this) { + is TextReference.Res -> { + val args = formatArgs + .map { if (it is TextReference) it.resolveReference() else it } + .toTypedArray() + + formatAnnotated(stringResource(id = id, *args)) + } + is TextReference.PluralRes -> formatAnnotated( + pluralStringResource(id, count, *formatArgs.toTypedArray()), + ) + is TextReference.Str -> formatAnnotated(value) + is TextReference.Combined -> buildAnnotatedString { + refs.forEach { + append(formatAnnotated(it.resolveReference())) + } + } + } +} + /** Concatenate [this] reference with [ref] */ operator fun TextReference.plus(ref: TextReference): TextReference { return when (this) { @@ -169,4 +195,14 @@ operator fun TextReference.plus(ref: TextReference): TextReference { is TextReference.Str, -> TextReference.Combined(refs = wrappedList(this, ref)) } +} + +@Composable +private fun formatAnnotated(rawString: String): AnnotatedString { + val markdownDescriptor = rememberMarkdownParser() + val parsedTree = markdownDescriptor.parse(MarkdownElementTypes.MARKDOWN_FILE, rawString, true) + + return buildAnnotatedString { + appendMarkdown(markdownText = rawString, node = parsedTree) + } } \ No newline at end of file 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 5006df1d7a..2d685763e3 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 @@ -10,9 +10,9 @@ import java.text.DecimalFormatSymbols 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 POINT_SEPARATOR = '.' +const val COMMA_SEPARATOR = ',' const val DECIMAL_SEPARATOR_LIMIT = 1 @Composable 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 c4f1b95bdf..c3167929ad 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 @@ -29,7 +29,10 @@ 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.keyboardAsState +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.shareText +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState @@ -185,11 +188,17 @@ private fun SendingText( currencySymbol = feeState.appCurrency.symbol, currencyCode = feeState.appCurrency.code, ) + val textResource = remember(sendingValue, feeValue) { + resourceReference( + id = R.string.send_summary_transaction_description, + formatArgs = wrappedList(sendingValue, feeValue), + ) + } Text( - text = stringResource(id = R.string.send_summary_transaction_description, sendingValue, feeValue), + text = textResource.resolveAnnotatedReference(), textAlign = TextAlign.Center, - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, modifier = Modifier .fillMaxWidth() .padding(TangemTheme.dimens.spacing12), 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 d9c609a5ca..d29fea2394 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,7 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType @@ -114,11 +115,14 @@ private fun FeeError(feeSelectorState: FeeSelectorState) { private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? { val choosableFees = fees as? TransactionFee.Choosable + val decimals = fees.normal.amount.decimals + val customValue = this.customValues.firstOrNull()?.value?.parseToBigDecimal(decimals) + val customAmount = fees.normal.amount.copy(value = customValue) return when (feeType) { FeeType.Slow -> choosableFees?.minimum?.amount FeeType.Market -> fees.normal.amount FeeType.Fast -> choosableFees?.priority?.amount - FeeType.Custom -> null + FeeType.Custom -> customAmount } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt index 8854274b17..f2be7e6ed2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt @@ -11,6 +11,7 @@ import java.math.BigDecimal import java.math.RoundingMode private const val FIAT_DECIMALS = 2 +private const val CRYPTO_FEE_DECIMALS = 6 private const val FEE_MINIMUM_VALUE = 0.01 internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { @@ -21,7 +22,7 @@ internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): Tex BigDecimalFormatter.formatCryptoAmount( cryptoAmount = amount.value, cryptoCurrency = amount.currencySymbol, - decimals = amount.decimals, + decimals = amount.decimals.coerceAtMost(CRYPTO_FEE_DECIMALS), ), ), ) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 303ac01722..af0d8874b0 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -82,6 +82,7 @@ swipeRefreshLayout = "1.1.0" spr-client = "3.6.2" web3j = "4.10.1" leakcanary = "2.13" +markdown = "0.7.2" # endregion Other libraries # region Tangem @@ -249,4 +250,5 @@ camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" } web3j-core = { module = "org.web3j:core", version.ref = "web3j" } leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } +markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" } # endregion Other