Updated on 2026-08-14
This commit is contained in:
commit
a401075a98
24 changed files with 585 additions and 89 deletions
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@
|
|||
<string name="express_exchange_notification_failed_title">Операция не выполнена провайдером</string>
|
||||
<string name="express_exchange_notification_verification_text">Посетите сайт провайдера для проверки</string>
|
||||
<string name="express_exchange_notification_verification_title">Провайдер: требуется верификация</string>
|
||||
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
|
||||
<string name="express_fetch_best_rates">Получение наилучших курсов...</string>
|
||||
<string name="express_provider">Провайдер</string>
|
||||
<string name="express_provider_best_rate">Лучший курс</string>
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@
|
|||
<string name="express_exchange_status_title">Exchange status</string>
|
||||
<string name="express_exchange_status_verified">Verified</string>
|
||||
<string name="express_exchange_status_verifying">Verification required</string>
|
||||
<string name="express_exchange_token_list_subtitle">List of all tokens added to your wallet</string>
|
||||
<string name="express_fetch_best_rates">Fetching best rates...</string>
|
||||
<string name="express_floating_rate">Floating rate</string>
|
||||
<string name="express_go_to_provider">Go to provider</string>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "")
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TextLayoutResult?>(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<TextEllipsis> {
|
||||
override val values: Sequence<TextEllipsis>
|
||||
get() = sequenceOf(
|
||||
TextEllipsis.Middle,
|
||||
TextEllipsis.End,
|
||||
TextEllipsis.OffsetEnd("TEXT".length),
|
||||
TextEllipsis.OffsetEnd("TEXT".length, false),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -20,4 +20,9 @@ internal class MockQuotesRepository(
|
|||
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -716,7 +716,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -796,7 +803,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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -24,6 +25,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
|
||||
|
|
@ -175,6 +177,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(),
|
||||
|
|
@ -293,6 +296,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,
|
||||
|
|
@ -507,16 +613,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(
|
||||
|
|
@ -735,11 +831,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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -802,10 +898,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
|
||||
|
|
@ -831,10 +926,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
|
||||
|
|
@ -895,6 +989,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
|
||||
|
|
@ -902,5 +1004,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
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -286,9 +286,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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ internal object TokenDetailsPreviewData {
|
|||
),
|
||||
dialogConfig = null,
|
||||
pendingTxs = persistentListOf(),
|
||||
swapTxs = persistentListOf(),
|
||||
pullToRefreshConfig = pullToRefreshConfig,
|
||||
bottomSheetConfig = null,
|
||||
isBalanceHidden = false,
|
||||
|
|
|
|||
|
|
@ -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<ExchangeStatusState>,
|
||||
val statuses: PersistentList<ExchangeStatusState>,
|
||||
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,
|
||||
)
|
||||
|
|
@ -19,6 +19,7 @@ internal data class TokenDetailsState(
|
|||
val marketPriceBlockState: MarketPriceBlockState,
|
||||
val notifications: ImmutableList<TokenDetailsNotification>,
|
||||
val pendingTxs: PersistentList<TransactionState>,
|
||||
val swapTxs: PersistentList<SwapTransactionsState>,
|
||||
val txHistoryState: TxHistoryState,
|
||||
val dialogConfig: TokenDetailsDialogConfig?,
|
||||
val pullToRefreshConfig: TokenDetailsPullToRefreshConfig,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import kotlinx.collections.immutable.PersistentList
|
|||
|
||||
@Composable
|
||||
internal fun ExchangeStatusBlock(
|
||||
status: PersistentList<ExchangeStatusState>,
|
||||
statuses: PersistentList<ExchangeStatusState>,
|
||||
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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue