From f0886b4b74b6feb5f592049d7d64554088db1f99 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 16:07:00 +0300 Subject: [PATCH 1/2] Updated on 2026-08-14 --- .../core/ui/components/MiddleEllipsisText.kt | 39 +-- .../ui/components/atoms/text/BoundCounter.kt | 38 +++ .../ui/components/atoms/text/EllipsisText.kt | 242 ++++++++++++++++++ .../ui/components/inputrow/InputRowApprox.kt | 12 +- .../tokens/repository/MockQuotesRepository.kt | 5 + .../tokendetails/TokenDetailsPreviewData.kt | 1 + .../state/SwapTransactionsState.kt | 13 +- .../tokendetails/state/TokenDetailsState.kt | 1 + .../components/ExchangeStatusNotifications.kt | 38 +++ .../TokenDetailsSkeletonStateConverter.kt | 1 + .../tokendetails/ui/TokenDetailsScreen.kt | 15 +- .../exchange/ExchangeStatusBlock.kt | 38 ++- .../exchange/ExchangeStatusBottomSheet.kt | 20 +- .../exchange/ExchangeStatusItems.kt | 21 +- 14 files changed, 425 insertions(+), 59 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt index 9bdff5f987..e67fbf3de2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.text.TextLayoutResult @@ -18,10 +17,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.TextUnit +import com.tangem.core.ui.components.atoms.text.BoundCounter /** * https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose */ +@Deprecated("Use EllipsisText with TextEllipsis.Middle ellipsis instead") @Suppress("LongMethod") @Composable fun MiddleEllipsisText( @@ -138,38 +139,4 @@ fun MiddleEllipsisText( private const val ELLIPSIS_CHARACTERS_COUNT = 3 private const val ELLIPSIS_CHARACTER = '.' -private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "") - -private class BoundCounter( - private val text: String, - private val textLayoutResult: TextLayoutResult, - private val charPosition: (Int) -> Int, -) { - var string = "" - private set - var width = 0f - private set - - private var _nextCharWidth: Float? = null - private var invalidCharsCount = 0 - - fun widthWithNextChar(): Float = width + nextCharWidth() - - private fun nextCharWidth(): Float = _nextCharWidth ?: run { - var boundingBox: Rect - // invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630 - invalidCharsCount-- - do { - boundingBox = textLayoutResult - .getBoundingBox(charPosition(string.count() + ++invalidCharsCount)) - } while (boundingBox.right == 0f) - _nextCharWidth = boundingBox.width - boundingBox.width - } - - fun addNextChar() { - string += text[charPosition(string.count())] - width += nextCharWidth() - _nextCharWidth = null - } -} \ No newline at end of file +private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "") \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt new file mode 100644 index 0000000000..6a97038b3d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt @@ -0,0 +1,38 @@ +package com.tangem.core.ui.components.atoms.text + +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.text.TextLayoutResult + +internal class BoundCounter( + private val text: String, + private val textLayoutResult: TextLayoutResult, + private val charPosition: (Int) -> Int, +) { + var string = "" + private set + var width = 0f + private set + + private var _nextCharWidth: Float? = null + private var invalidCharsCount = 0 + + fun widthWithNextChar(): Float = width + nextCharWidth() + + private fun nextCharWidth(): Float = _nextCharWidth ?: run { + var boundingBox: Rect + // invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630 + invalidCharsCount-- + do { + boundingBox = textLayoutResult + .getBoundingBox(charPosition(string.count() + ++invalidCharsCount)) + } while (boundingBox.right == 0f) + _nextCharWidth = boundingBox.width + boundingBox.width + } + + fun addNextChar() { + string += text[charPosition(string.count())] + width += nextCharWidth() + _nextCharWidth = null + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt new file mode 100644 index 0000000000..d95b813ee1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt @@ -0,0 +1,242 @@ +package com.tangem.core.ui.components.atoms.text + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.TextUnit +import com.tangem.core.ui.res.TangemTheme + +sealed class TextEllipsis { + + object Middle : TextEllipsis() + + object End : TextEllipsis() + + data class OffsetEnd( + val offsetEnd: Int = 0, + val hasSeparator: Boolean = true, + ) : TextEllipsis() +} + +/** + * https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose + * + * Customized Text with ellipsis. Ellipsis can be placed in: Middle, End or OffsetEnd (OffsetEnd with separator). + * + * * OffsetEnd can be useful to display big amounts with currency symbol. OffsetEnd 0 is equal to End. + */ +@Suppress("LongMethod") +@Composable +fun EllipsisText( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + softWrap: Boolean = true, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + ellipsis: TextEllipsis = TextEllipsis.End, +) { + val ellipsisText = remember(text) { + if (ellipsis is TextEllipsis.OffsetEnd && ellipsis.hasSeparator) { + ELLIPSIS_TEXT_WITH_SEPARATOR + } else { + ELLIPSIS_TEXT + } + } + + // some letters, like "r", will have less width when placed right before "." + // adding a space to prevent such case + val layoutText = remember(text) { "$text $ellipsisText" } + val textLayoutResultState = remember(layoutText) { + mutableStateOf(null) + } + SubcomposeLayout(modifier) { constraints -> + // result is ignored - we only need to fill our textLayoutResult + subcompose("measure") { + Text( + text = layoutText, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + softWrap = softWrap, + maxLines = 1, + onTextLayout = { textLayoutResultState.value = it }, + style = style, + ) + }.first().measure(Constraints()) + // to allow smart cast + val textLayoutResult = textLayoutResultState.value + ?: // shouldn't happen - onTextLayout is called before subcompose finishes + return@SubcomposeLayout layout(0, 0) {} + val placeable = subcompose("visible") { + val finalText = remember(text, textLayoutResult, constraints.maxWidth) { + if ( + text.isEmpty() || + textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth + ) { + // text not including ellipsis fits on the first line. + return@remember text + } + + var ellipsisWidth = 0f + layoutText.indices.toList() + .takeLast(ellipsisText.length) + .forEach widthLet@{ + ellipsisWidth += textLayoutResult.getBoundingBox(it).width + } + + val availableWidth = constraints.maxWidth - ellipsisWidth + val startCounter = BoundCounter(text, textLayoutResult) { it } + val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it } + + when (ellipsis) { + TextEllipsis.Middle -> { + middleEllipsisText( + availableWidth, + startCounter, + endCounter, + ) + } + TextEllipsis.End -> { + offsetEndEllipsisText( + availableWidth = availableWidth, + startCounter = startCounter, + endCounter = endCounter, + ) + } + is TextEllipsis.OffsetEnd -> { + offsetEndEllipsisText( + availableWidth = availableWidth, + startCounter = startCounter, + endCounter = endCounter, + offsetEnd = ellipsis.offsetEnd, + withSeparator = ellipsis.hasSeparator, + ) + } + } + } + Text( + text = finalText, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + softWrap = softWrap, + onTextLayout = onTextLayout, + style = style, + ) + }[0].measure(constraints) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } +} + +private const val ELLIPSIS_SEPARATOR = " " +private const val ELLIPSIS_TEXT = "..." +private const val ELLIPSIS_TEXT_WITH_SEPARATOR = ELLIPSIS_TEXT.plus(ELLIPSIS_SEPARATOR) + +private fun middleEllipsisText(availableWidth: Float, startCounter: BoundCounter, endCounter: BoundCounter): String { + while (availableWidth - startCounter.width - endCounter.width > 0) { + val possibleEndWidth = endCounter.widthWithNextChar() + if ( + startCounter.width >= possibleEndWidth && + availableWidth - startCounter.width - possibleEndWidth >= 0 + ) { + endCounter.addNextChar() + } else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) { + startCounter.addNextChar() + } else { + break + } + } + return startCounter.string.trimEnd() + ELLIPSIS_TEXT + endCounter.string.reversed().trimStart() +} + +private fun offsetEndEllipsisText( + availableWidth: Float, + startCounter: BoundCounter, + endCounter: BoundCounter, + offsetEnd: Int = 0, + withSeparator: Boolean = false, +): String { + while (availableWidth - startCounter.width - endCounter.width > 0) { + val possibleEndWidth = endCounter.widthWithNextChar() + if ( + offsetEnd > endCounter.string.length && + availableWidth - startCounter.width - possibleEndWidth >= 0 + ) { + endCounter.addNextChar() + } else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) { + startCounter.addNextChar() + } else { + break + } + } + val ellipsis = if (withSeparator) ELLIPSIS_TEXT_WITH_SEPARATOR else ELLIPSIS_TEXT + + return startCounter.string.trimEnd() + ellipsis + endCounter.string.reversed().trimStart() +} + +//region Preview +@Preview(widthDp = 200) +@Composable +private fun EllipsisTexPreview(@PreviewParameter(EllipsisTexPreviewParameterProvider::class) ellipsis: TextEllipsis) { + TangemTheme { + EllipsisText( + text = "11111111111111111111111111111111111111111111111111 END", + ellipsis = ellipsis, + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .fillMaxWidth(), + ) + } +} + +private class EllipsisTexPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TextEllipsis.Middle, + TextEllipsis.End, + TextEllipsis.OffsetEnd("TEXT".length), + TextEllipsis.OffsetEnd("TEXT".length, false), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt index 3b085833a3..bd69b6feb9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt @@ -3,13 +3,13 @@ package com.tangem.core.ui.components.inputrow import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.components.inputrow.inner.DividerContainer @@ -56,6 +56,7 @@ fun InputRowApprox( iconState = leftIcon, title = leftTitle, subtitle = leftSubtitle, + modifier = Modifier.weight(1f), ) Icon( painter = painterResource(id = R.drawable.ic_approx_24), @@ -71,6 +72,7 @@ fun InputRowApprox( iconState = rightIcon, title = rightTitle, subtitle = rightSubtitle, + modifier = Modifier.weight(1f), ) } } @@ -97,12 +99,12 @@ private fun InputRowApproxItem( start = TangemTheme.dimens.spacing12, ), ) { - Text( + EllipsisText( text = title.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) - Text( + EllipsisText( text = subtitle.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, @@ -123,7 +125,7 @@ private fun InputRowApproxPreview_Light() { leftTitle = TextReference.Str("Left title"), leftSubtitle = TextReference.Str("Left subtitle"), rightIcon = TokenIconState.Loading, - rightTitle = TextReference.Str("Right title"), + rightTitle = TextReference.Str("Right title Right title Right title Right title Right title"), rightSubtitle = TextReference.Str("Right subtitle"), modifier = Modifier .background(TangemTheme.colors.background.action), @@ -137,7 +139,7 @@ private fun InputRowApproxPreview_Dark() { TangemTheme(isDark = true) { InputRowApprox( leftIcon = TokenIconState.Loading, - leftTitle = TextReference.Str("Left title"), + leftTitle = TextReference.Str("Left title Left title Left title Left title Left title"), leftSubtitle = TextReference.Str("Left subtitle"), rightIcon = TokenIconState.Loading, rightTitle = TextReference.Str("Right title"), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index 6d1c1e1a27..ae960f191a 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -20,4 +20,9 @@ internal class MockQuotesRepository( override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { return getQuotesUpdates(currenciesIds).first() } + + override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote { + return quotes.map { it.getOrElse { e -> throw e } }.first() + .first { it.rawCurrencyId == currencyId.rawCurrencyId } + } } \ No newline at end of file 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 2a25900715..458e547586 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 @@ -101,6 +101,7 @@ internal object TokenDetailsPreviewData { ), dialogConfig = null, pendingTxs = persistentListOf(), + swapTxs = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, bottomSheetConfig = null, isBalanceHidden = false, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 8b4fb5c8a5..9941c1bbda 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -2,21 +2,25 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications import kotlinx.collections.immutable.PersistentList -import java.math.BigDecimal internal data class SwapTransactionsState( val txId: String, - val providerId: Int, + val provider: SwapProvider, val txUrl: String? = null, - val rate: BigDecimal, val timestamp: Long, - val status: PersistentList, + val statuses: PersistentList, val activeStatus: ExchangeStatus?, + val fiatSymbol: String, + val notification: ExchangeStatusNotifications? = null, val toCryptoAmount: String, + val toCryptoSymbol: String, val toFiatAmount: String, val toCurrencyIcon: TokenIconState, val fromCryptoAmount: String, + val fromCryptoSymbol: String, val fromFiatAmount: String, val fromCurrencyIcon: TokenIconState, val onClick: () -> Unit, @@ -25,7 +29,6 @@ internal data class SwapTransactionsState( internal class ExchangeStatusState( val status: ExchangeStatus, - val text: String, val isActive: Boolean, val isDone: Boolean, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 15f290adde..f9f5dfcabd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -19,6 +19,7 @@ internal data class TokenDetailsState( val marketPriceBlockState: MarketPriceBlockState, val notifications: ImmutableList, val pendingTxs: PersistentList, + val swapTxs: PersistentList, val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, val pullToRefreshConfig: TokenDetailsPullToRefreshConfig, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt new file mode 100644 index 0000000000..1da8fd0672 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.components + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.tokendetails.impl.R + +@Immutable +internal sealed class ExchangeStatusNotifications(val config: NotificationConfig) { + + data class NeedVerification( + val onGoToProviderClick: () -> Unit, + ) : ExchangeStatusNotifications( + config = NotificationConfig( + title = TextReference.Res(R.string.express_exchange_notification_verification_title), + subtitle = TextReference.Res(R.string.express_exchange_notification_verification_text), + iconResId = R.drawable.ic_alert_triangle_20, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = TextReference.Res(R.string.express_go_to_provider), + onClick = onGoToProviderClick, + ), + ), + ) + + data class Failed( + val onGoToProviderClick: () -> Unit, + ) : ExchangeStatusNotifications( + config = NotificationConfig( + title = TextReference.Res(R.string.express_exchange_notification_failed_title), + subtitle = TextReference.Res(R.string.express_exchange_notification_failed_text), + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = TextReference.Res(R.string.express_go_to_provider), + onClick = onGoToProviderClick, + ), + ), + ) +} \ No newline at end of file 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 e92142e90c..30d410450e 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 @@ -45,6 +45,7 @@ internal class TokenDetailsSkeletonStateConverter( marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol), notifications = persistentListOf(), pendingTxs = persistentListOf(), + swapTxs = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 9b4e73ef9a..57f5e40688 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -2,7 +2,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.ExperimentalMaterialApi @@ -40,6 +42,9 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheet +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.swapTransactionsItems // TODO: Split to blocks [REDACTED_JIRA] @Suppress("LongMethod") @@ -117,6 +122,11 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) } + swapTransactionsItems( + state.swapTxs, + itemModifier, + ) + txHistoryItems( state = state.txHistoryState, isBalanceHidden = state.isBalanceHidden, @@ -141,6 +151,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { is ChooseAddressBottomSheetConfig -> { ChooseAddressBottomSheet(config = config) } + is ExchangeStatusBottomSheetConfig -> { + ExchangeStatusBottomSheet(config = config) + } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index a583d1cb56..b9d7dd77ee 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -26,7 +26,7 @@ import kotlinx.collections.immutable.PersistentList @Composable internal fun ExchangeStatusBlock( - status: PersistentList, + statuses: PersistentList, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -70,10 +70,10 @@ internal fun ExchangeStatusBlock( } } - status.forEachIndexed { index, item -> + statuses.forEachIndexed { index, item -> ExchangeStatusStep( stepStatus = item, - isLast = index == status.lastIndex, + isLast = index == statuses.lastIndex, ) } } @@ -128,7 +128,7 @@ private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) { } Text( - text = stepStatus.text, + text = getStatusText(stepStatus)?.let { stringResource(it) }.orEmpty(), style = TangemTheme.typography.body2, color = textColor, modifier = Modifier @@ -206,4 +206,34 @@ private fun ExchangeStepSeparator() { shape = CircleShape, ), ) +} + +private fun getStatusText(stepStatus: ExchangeStatusState) = when (stepStatus.status) { + ExchangeStatus.Failed -> R.string.express_exchange_status_failed + ExchangeStatus.Verifying -> if (stepStatus.isDone) { + R.string.express_exchange_status_verified + } else { + R.string.express_exchange_status_verifying + } + ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) { + R.string.express_exchange_status_received + } else { + R.string.express_exchange_status_receiving + } + ExchangeStatus.Confirming -> if (stepStatus.isDone) { + R.string.express_exchange_status_confirmed + } else { + R.string.express_exchange_status_confirming + } + ExchangeStatus.Exchanging -> if (stepStatus.isDone) { + R.string.express_exchange_status_exchanged + } else { + R.string.express_exchange_status_exchanging + } + ExchangeStatus.Sending -> if (stepStatus.isDone) { + R.string.express_exchange_status_sent + } else { + R.string.express_exchange_status_sending + } + else -> null } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index 5705f00a63..b32c69f48e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -15,6 +16,7 @@ import com.tangem.core.ui.components.SpacerH24 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.components.notifications.Notification import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.toDateFormat @@ -64,17 +66,25 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC toFiatAmount = TextReference.Str(config.toFiatAmount), ) SpacerH12() - // todo replace with real provider data ExchangeProvider( - providerName = TextReference.Str(config.providerId.toString()), - providerType = TextReference.Str("CEX"), - imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/changenow_512.png", + providerName = TextReference.Str(config.provider.name), + providerType = TextReference.Str(config.provider.type.name), + imageUrl = config.provider.imageLarge, ) SpacerH12() ExchangeStatusBlock( - status = config.status, + statuses = config.statuses, onClick = config.onGoToProviderClick, ) + AnimatedContent( + targetState = config.notification, + label = "Exchange Status Notification Change", + ) { + it?.let { + SpacerH12() + Notification(config = it.config) + } + } SpacerH24() } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index 95ac42480e..4ea9a31b08 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -17,7 +17,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension import androidx.constraintlayout.compose.Visibility +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.res.TangemTheme @@ -46,11 +49,13 @@ internal fun LazyListScope.swapTransactionsItems( } ExchangeStatusItem( - providerName = item.providerId.toString(), + providerName = item.provider.name, fromTokenIconState = item.fromCurrencyIcon, toTokenIconState = item.toCurrencyIcon, fromAmount = item.fromCryptoAmount, + fromSymbol = item.fromCryptoSymbol, toAmount = item.toCryptoAmount, + toSymbol = item.toCryptoSymbol, onClick = item.onClick, infoIconRes = iconRes, infoIconTint = tint, @@ -67,7 +72,9 @@ private fun ExchangeStatusItem( fromTokenIconState: TokenIconState, toTokenIconState: TokenIconState, fromAmount: String, + fromSymbol: String, toAmount: String, + toSymbol: String, onClick: () -> Unit, modifier: Modifier = Modifier, @DrawableRes infoIconRes: Int? = null, @@ -104,14 +111,17 @@ private fun ExchangeStatusItem( bottom.linkTo(parent.bottom) }, ) - Text( + EllipsisText( text = fromAmount, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + ellipsis = TextEllipsis.OffsetEnd(fromSymbol.length), modifier = Modifier.constrainAs(fromRef) { start.linkTo(fromIconRef.end, padding6) top.linkTo(titleRef.bottom, padding6) + end.linkTo(swapIconRef.start) bottom.linkTo(parent.bottom) + width = Dimension.fillToConstraints }, ) Icon( @@ -123,6 +133,7 @@ private fun ExchangeStatusItem( .constrainAs(swapIconRef) { start.linkTo(fromRef.end, padding6) top.linkTo(titleRef.bottom, padding6) + end.linkTo(toIconRef.start) bottom.linkTo(parent.bottom) }, ) @@ -134,17 +145,21 @@ private fun ExchangeStatusItem( .constrainAs(toIconRef) { start.linkTo(swapIconRef.end, padding6) top.linkTo(titleRef.bottom, padding6) + end.linkTo(toRef.start) bottom.linkTo(parent.bottom) }, ) - Text( + EllipsisText( text = toAmount, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + ellipsis = TextEllipsis.OffsetEnd(toSymbol.length), modifier = Modifier.constrainAs(toRef) { start.linkTo(toIconRef.end, padding6) top.linkTo(titleRef.bottom, padding6) + end.linkTo(infoIconRef.start, padding6) bottom.linkTo(parent.bottom) + width = Dimension.fillToConstraints }, ) Icon( From 62f7c337ff8cabb9b0e0384b1ed87d1c1dc63f71 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 18:45:53 +0300 Subject: [PATCH 2/2] Updated on 2026-08-14 --- .../models/response/ExpressErrorResponse.kt | 2 +- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../swap/converters/ErrorsDataConverter.kt | 4 +- .../feature/swap/domain/SwapInteractorImpl.kt | 42 +++-- .../swap/domain/models/ui/SwapState.kt | 5 +- .../feature/swap/models/SwapStateHolder.kt | 1 + .../tangem/feature/swap/ui/StateBuilder.kt | 146 +++++++++++++++--- .../feature/swap/ui/SwapSelectTokenScreen.kt | 4 +- .../feature/swap/viewmodels/SwapViewModel.kt | 10 +- 10 files changed, 174 insertions(+), 42 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt index c0c39b0fb2..ea8a552360 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt @@ -21,7 +21,7 @@ data class ExpressError( data class ExpressErrorValue( @Json(name = "minAmount") - val minAmount: BigDecimal?, + val minAmount: String?, @Json(name = "decimals") val decimals: Int?, diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 194da3dfb2..8805ebe16d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -201,6 +201,7 @@ Операция не выполнена провайдером Посетите сайт провайдера для проверки Провайдер: требуется верификация + Список токенов в вашем кошельке Получение наилучших курсов... Провайдер Лучший курс diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8d4ff0b882..be887dc4ea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -215,6 +215,7 @@ Exchange status Verified Verification required + List of all tokens added to your wallet Fetching best rates... Floating rate Go to provider diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index 5330219b1e..2068fc982a 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -3,7 +3,7 @@ package com.tangem.feature.swap.converters import com.squareup.moshi.JsonAdapter import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.feature.swap.domain.models.DataError -import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.utils.converter.Converter internal class ErrorsDataConverter( @@ -23,7 +23,7 @@ internal class ErrorsDataConverter( 2240 -> DataError.ExchangeNotPossibleError(code = error.code) 2250 -> DataError.ExchangeTooSmallAmountError( code = error.code, - amount = SwapAmount( + amount = createFromAmountWithOffset( requireNotNull(error.value?.minAmount), requireNotNull(error.value?.decimals), ), 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 da5dd7b595..afc2b3787b 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 @@ -501,14 +501,16 @@ internal class SwapInteractorImpl @Inject constructor( network = currencyToSend.currency.network, ) - return result.fold(ifLeft = { - when (it) { - is SendTransactionError.NetworkError -> TxState.NetworkError - is SendTransactionError.DataError -> TxState.BlockchainError - SendTransactionError.DemoCardError -> TxState.UnknownError - else -> TxState.UnknownError - } - }, ifRight = { + return result.fold( + ifLeft = { + when (it) { + is SendTransactionError.NetworkError -> TxState.NetworkError + is SendTransactionError.DataError -> TxState.BlockchainError + SendTransactionError.DemoCardError -> TxState.UnknownError + else -> TxState.UnknownError + } + }, + ifRight = { TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, @@ -523,7 +525,8 @@ internal class SwapInteractorImpl @Inject constructor( derivationPath, ) ?: "", ) - },) + }, + ) } @Deprecated("used in old swap mechanism") @@ -707,7 +710,14 @@ internal class SwapInteractorImpl @Inject constructor( } } } else { - return SwapState.SwapError(quoteDataModel.error) + val rates = getQuotes(fromToken.currency.id) + val fromTokenSwapInfo = TokenSwapInfo( + tokenAmount = amount, + amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + ?: BigDecimal.ZERO, + cryptoCurrencyStatus = fromToken, + ) + return SwapState.SwapError(fromTokenSwapInfo, quoteDataModel.error) } } @@ -787,7 +797,17 @@ internal class SwapInteractorImpl @Inject constructor( ), ) } else { - return SwapState.SwapError(it.error) + val rates = getQuotes(fromToken.currency.id) + val fromTokenSwapInfo = TokenSwapInfo( + tokenAmount = amount, + amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + ?: BigDecimal.ZERO, + cryptoCurrencyStatus = fromToken, + ) + return SwapState.SwapError( + fromTokenSwapInfo, + it.error, + ) } } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 0c98986dc1..b49765d79d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -31,7 +31,10 @@ sealed interface SwapState { val zeroAmountEquivalent: String, ) : SwapState - data class SwapError(val error: DataError) : SwapState + data class SwapError( + val fromTokenInfo: TokenSwapInfo, + val error: DataError, + ) : SwapState } sealed class PermissionDataState { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 37ef409ce5..9fc0d8a9c1 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -123,6 +123,7 @@ sealed interface SwapWarning { data class HighPriceImpact(val priceImpact: Int, val notificationConfig: NotificationConfig) : SwapWarning data class TooSmallAmountWarning(val notificationConfig: NotificationConfig) : SwapWarning data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning + data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning } enum class GenericWarningType { 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 7d965aa10f..4a90e77fab 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 @@ -13,6 +13,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.NetworkInfo import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation @@ -25,6 +26,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import java.math.RoundingMode +import kotlin.math.min /** * State builder creates a specific states for SwapScreen @@ -176,6 +178,7 @@ internal class StateBuilder( balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", isBalanceHidden = isBalanceHiddenProvider(), ), + warnings = emptyList(), fee = FeeItemState.Empty, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), providerState = ProviderState.Loading(), @@ -294,6 +297,109 @@ internal class StateBuilder( ) } + fun createQuotesErrorState( + uiStateHolder: SwapStateHolder, + swapProvider: SwapProvider, + fromToken: TokenSwapInfo, + toToken: CryptoCurrencyStatus?, + dataError: DataError, + ): SwapStateHolder { + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder + val warning = getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency) + val providerState = getProviderStateForError( + swapProvider = swapProvider, + fromToken = fromToken.cryptoCurrencyStatus.currency, + dataError = dataError, + selectionType = ProviderState.SelectionType.CLICK, + ) + val receiveCardData = toToken?.let { + SwapCardState.SwapCardData( + type = TransactionCardType.ReceiveCard(), + amountTextFieldValue = TextFieldValue( + text = "0", + ), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = toToken, + tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, + coinId = toToken.currency.network.backendId, + isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, + tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, + canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, + balance = toToken.getFormattedAmount(), + isBalanceHidden = isBalanceHiddenProvider(), + ) + } ?: SwapCardState.Empty( + type = TransactionCardType.ReceiveCard(), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + amountTextFieldValue = TextFieldValue( + text = "0", + ), + canSelectAnotherToken = true, + ) + return uiStateHolder.copy( + sendCardData = uiStateHolder.sendCardData.copy( + amountEquivalent = getFormattedFiatAmount(fromToken.amountFiat), + ), + receiveCardData = receiveCardData, + warnings = listOf(warning), + permissionState = SwapPermissionState.Empty, + fee = FeeItemState.Empty, + swapButton = SwapButton( + enabled = false, + loading = false, + onClick = actions.onSwapClick, + ), + updateInProgress = false, + providerState = providerState, + ) + } + + private fun getProviderStateForError( + swapProvider: SwapProvider, + fromToken: CryptoCurrency, + dataError: DataError, + selectionType: ProviderState.SelectionType, + ): ProviderState { + return when (dataError) { + is DataError.ExchangeTooSmallAmountError -> { + swapProvider.convertToUnavailableProviderState( + alertText = resourceReference( + R.string.express_provider_min_amount, + wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), + ), + selectionType = selectionType, + onProviderClick = actions.onProviderClick, + ) + } + else -> { + ProviderState.Empty() + } + } + } + + private fun getWarningForError(dataError: DataError, fromToken: CryptoCurrency): SwapWarning { + return when (dataError) { + is DataError.ExchangeTooSmallAmountError -> SwapWarning.TooSmallAmountWarning( + notificationConfig = NotificationConfig( + title = resourceReference( + id = R.string.warning_express_too_minimal_amount_title, + formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), + ), + subtitle = resourceReference(R.string.warning_express_too_minimal_amount_description), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + else -> SwapWarning.GeneralWarning( + notificationConfig = NotificationConfig( + title = resourceReference(R.string.common_error), + subtitle = resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + } + } + fun createQuotesEmptyAmountState( uiStateHolder: SwapStateHolder, emptyAmountState: SwapState.EmptyAmountState, @@ -473,10 +579,9 @@ internal class StateBuilder( val fee = uiState.fee as? FeeItemState.Content ?: return uiState val fromCryptoCurrencyStatus = requireNotNull(fromToken.token) val toCryptoCurrencyStatus = requireNotNull(toToken.token) - val rate = txState.toAmount?.toBigDecimal()?.divide( - txState.fromAmount?.toBigDecimal(), + val rate = txState.toAmount?.toBigDecimal()?.calculateRate( + txState.fromAmount?.toBigDecimal() ?: BigDecimal.ZERO, toCryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, ) val fromCurrencySymbol = fromCryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toCryptoCurrencyStatus.currency.symbol @@ -510,16 +615,6 @@ internal class StateBuilder( ) } - fun mapError(uiState: SwapStateHolder, error: DataError, onClick: () -> Unit): SwapStateHolder { - return when (error) { - // todo use if needed later - // DataError.InsufficientLiquidity -> TODO() - // DataError.NoError -> TODO() - is DataError.ExchangeTooSmallAmountError -> addWarning(uiState, error.amount.toString(), true, onClick) - else -> addWarning(uiState, null, false) {} - } - } - fun addAlert(uiState: SwapStateHolder, onClick: () -> Unit): SwapStateHolder { return uiState.copy( alert = SwapWarning.GenericWarning( @@ -738,11 +833,11 @@ internal class StateBuilder( onProviderClick = onProviderSelect, selectionType = ProviderState.SelectionType.SELECT, ) - // todo handle error - is SwapState.SwapError -> provider.convertToUnavailableProviderState( - alertText = resourceReference(R.string.express_provider_min_amount, wrappedList("10")), - selectionType = ProviderState.SelectionType.NONE, - onProviderClick = onProviderSelect, + is SwapState.SwapError -> getProviderStateForError( + swapProvider = provider, + fromToken = state.fromTokenInfo.cryptoCurrencyStatus.currency, + dataError = state.error, + selectionType = ProviderState.SelectionType.SELECT, ) } } @@ -805,10 +900,9 @@ internal class StateBuilder( selectionType: ProviderState.SelectionType, onProviderClick: (String) -> Unit, ): ProviderState { - val rate = toTokenInfo.tokenAmount.value.divide( + val rate = toTokenInfo.tokenAmount.value.calculateRate( fromTokenInfo.tokenAmount.value, toTokenInfo.cryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, ) val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol @@ -834,10 +928,9 @@ internal class StateBuilder( ): ProviderState { val fromTokenInfo = state.fromTokenInfo val toTokenInfo = state.toTokenInfo - val rate = toTokenInfo.tokenAmount.value.divide( + val rate = toTokenInfo.tokenAmount.value.calculateRate( fromTokenInfo.tokenAmount.value, toTokenInfo.cryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, ) val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol @@ -898,6 +991,14 @@ internal class StateBuilder( return BigDecimalFormatter.formatFiatAmount(amount, appCurrency.code, appCurrency.symbol) } + private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { + return "${this.formatToUIRepresentation()} ${token.network.currencySymbol}" + } + + private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal { + return this.divide(to, min(decimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP) + } + private companion object { const val ADDRESS_MIN_LENGTH = 11 const val ADDRESS_FIRST_PART_LENGTH = 7 @@ -905,5 +1006,6 @@ internal class StateBuilder( private const val PRICE_IMPACT_THRESHOLD = 0.1 private const val HUNDRED_PERCENTS = 100 private const val UNKNOWN_AMOUNT_SIGN = "—" + private const val MAX_DECIMALS_TO_SHOW = 8 } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 6e89f78c3a..cde932bf0d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -43,7 +43,7 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) placeholderSearchText = stringResource(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered, onSearchDisplayClose = { state.onSearchEntered("") }, - subtitle = "", // todo add title + subtitle = stringResource(id = R.string.express_exchange_token_list_subtitle), ) }, ) @@ -55,7 +55,7 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = LazyColumn( modifier = modifier .background(color = screenBackgroundColor) - .fillMaxWidth(), + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { item { SpacerH8() } 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 a577a98dfe..4fb07606b9 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 @@ -288,9 +288,13 @@ internal class SwapViewModel @Inject constructor( ) } is SwapState.SwapError -> { - Timber.e("SwapError when loading quotes ${state.error}") - // todo handle when change token and error - uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() } + uiState = stateBuilder.createQuotesErrorState( + uiStateHolder = uiState, + swapProvider = provider, + fromToken = state.fromTokenInfo, + toToken = dataState.toCryptoCurrency, + dataError = state.error, + ) } } }