Updated on 2026-08-14
This commit is contained in:
commit
6b9fc4a3ce
1058 changed files with 48197 additions and 12718 deletions
|
|
@ -12,6 +12,7 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -31,6 +32,7 @@ import com.tangem.core.ui.res.LocalRedesignEnabled
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
|
||||
@Composable
|
||||
|
|
@ -48,7 +50,9 @@ fun PortfolioSelectRow(
|
|||
leftContent()
|
||||
val leftText = if (state.isAccountMode) R.string.account_details_title else R.string.wc_common_wallet
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.testTag(WalletConnectBottomSheetTestTags.WALLET_NAME_TITLE),
|
||||
text = stringResourceSafe(leftText),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
|
@ -66,7 +70,9 @@ fun PortfolioSelectRow(
|
|||
Text(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.testTag(WalletConnectBottomSheetTestTags.WALLET_NAME),
|
||||
text = state.name.resolveReference(),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class AmountStateConverter(
|
|||
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val accountTitleUM: AccountTitleUM,
|
||||
private val isMaxButtonVisible: Boolean = true,
|
||||
) : Converter<AmountParameters, AmountState> {
|
||||
|
||||
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
|
|
@ -72,6 +73,7 @@ class AmountStateConverter(
|
|||
amountTextField = amountFieldConverter.convert(value.value),
|
||||
isPrimaryButtonEnabled = false,
|
||||
appCurrency = appCurrency,
|
||||
isMaxButtonVisible = isMaxButtonVisible,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ sealed class AmountState {
|
|||
* @param isEditingDisabled indicated whether amount is editable
|
||||
* @param reduceAmountBy reduces amount to be sent by specified value
|
||||
* @param isIgnoreReduce ignores reduce amount value
|
||||
* @param isMaxButtonVisible indicates whether the "Max" button is shown
|
||||
*/
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
|
|
@ -37,6 +38,7 @@ sealed class AmountState {
|
|||
val isEditingDisabled: Boolean = false,
|
||||
val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
|
||||
val isIgnoreReduce: Boolean = false,
|
||||
val isMaxButtonVisible: Boolean = true,
|
||||
) : AmountState()
|
||||
|
||||
data object Empty : AmountState() {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ internal fun LazyListScope.amountFieldV2(
|
|||
@Composable
|
||||
private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val tokenIconState = (amountUM as? AmountState.Data)?.tokenIconState ?: CurrencyIconState.Loading
|
||||
val isMaxButtonVisible = (amountUM as? AmountState.Data)?.isMaxButtonVisible != false
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
|
|
@ -106,22 +107,24 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi
|
|||
amountUM = amountUM,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.send_max_amount),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = onMaxAmountClick,
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
.testTag(SendScreenTestTags.MAX_BUTTON),
|
||||
)
|
||||
if (isMaxButtonVisible) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.send_max_amount),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = onMaxAmountClick,
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
.testTag(SendScreenTestTags.MAX_BUTTON),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
|
@ -123,10 +124,9 @@ private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Mo
|
|||
.padding(end = TangemTheme.dimens2.x3),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = state.titleUM.text.resolveReference(),
|
||||
style = state.titleUM.style.textStyle,
|
||||
color = state.titleUM.tone.color(state.type),
|
||||
EarnBlockTitle(
|
||||
titleUM = state.titleUM,
|
||||
type = state.type,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_TOP)
|
||||
.padding(end = TangemTheme.dimens2.x2),
|
||||
|
|
@ -182,7 +182,7 @@ private fun EarnBlockTrailing(type: Type, trailingUM: EarnBlockUM.TrailingUM?, o
|
|||
TangemButton(
|
||||
buttonUM = TangemButtonUM(
|
||||
text = trailingUM.text,
|
||||
type = type.buttonType(),
|
||||
type = trailingUM.style.buttonType(type),
|
||||
size = TangemButtonSize.X9,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
isEnabled = trailingUM.isEnabled,
|
||||
|
|
@ -207,21 +207,37 @@ private fun EarnBlockTrailing(type: Type, trailingUM: EarnBlockUM.TrailingUM?, o
|
|||
)
|
||||
}
|
||||
}
|
||||
is EarnBlockUM.TrailingUM.Icon -> {
|
||||
TangemIcon(
|
||||
tangemIconUM = TangemIconUM.Icon(
|
||||
iconRes = trailingUM.tone.iconRes(),
|
||||
tintReference = { trailingUM.tone.tint() },
|
||||
),
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.TAIL)
|
||||
.size(TangemTheme.dimens2.x6),
|
||||
)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EarnBlockTitle(titleUM: EarnBlockUM.TitleUM, type: Type, modifier: Modifier = Modifier) {
|
||||
val titleText: @Composable () -> Unit = {
|
||||
Text(
|
||||
text = titleUM.text.resolveReference(),
|
||||
style = titleUM.style.textStyle,
|
||||
color = titleUM.tone.color(type),
|
||||
)
|
||||
}
|
||||
val icon = titleUM.iconUM
|
||||
if (icon == null) {
|
||||
Box(modifier = modifier) { titleText() }
|
||||
return
|
||||
}
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
titleText()
|
||||
Spacer(modifier = Modifier.width(TangemTheme.dimens2.x1))
|
||||
TangemIcon(
|
||||
tangemIconUM = TangemIconUM.Icon(
|
||||
iconRes = icon.tone.iconRes(),
|
||||
tintReference = { icon.tone.tint() },
|
||||
),
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x4),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EarnBlockSubtitle(subtitle: EarnBlockUM.SubtitleUM.Text, type: Type, modifier: Modifier = Modifier) {
|
||||
val textStyle = subtitle.style.textStyle
|
||||
|
|
@ -297,9 +313,12 @@ private fun Type.accentStrongTint(): Color = when (this) {
|
|||
Type.YieldSupply -> TangemTheme.colors2.text.status.positive
|
||||
}
|
||||
|
||||
private fun Type.buttonType(): TangemButtonType = when (this) {
|
||||
Type.Staking -> TangemButtonType.Accent
|
||||
Type.YieldSupply -> TangemButtonType.Positive
|
||||
private fun EarnBlockUM.TrailingUM.Button.Style.buttonType(type: Type): TangemButtonType = when (this) {
|
||||
EarnBlockUM.TrailingUM.Button.Style.Default -> when (type) {
|
||||
Type.Staking -> TangemButtonType.Accent
|
||||
Type.YieldSupply -> TangemButtonType.Positive
|
||||
}
|
||||
EarnBlockUM.TrailingUM.Button.Style.Secondary -> TangemButtonType.Secondary
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -319,16 +338,16 @@ private fun EarnBlockUM.SubtitleUM.Tone.color(type: Type): Color = when (this) {
|
|||
EarnBlockUM.SubtitleUM.Tone.Accent -> type.accentText()
|
||||
}
|
||||
|
||||
private fun EarnBlockUM.TrailingUM.IconTone.iconRes(): Int = when (this) {
|
||||
EarnBlockUM.TrailingUM.IconTone.Warning -> R.drawable.ic_alert_triangle_20
|
||||
EarnBlockUM.TrailingUM.IconTone.Info -> R.drawable.ic_alert_circle_red_20
|
||||
private fun EarnBlockUM.TitleUM.IconTone.iconRes(): Int = when (this) {
|
||||
EarnBlockUM.TitleUM.IconTone.Warning -> R.drawable.ic_attention_default_24
|
||||
EarnBlockUM.TitleUM.IconTone.Info -> R.drawable.ic_alert_circle_24
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun EarnBlockUM.TrailingUM.IconTone.tint(): Color = when (this) {
|
||||
EarnBlockUM.TrailingUM.IconTone.Warning -> TangemTheme.colors2.graphic.status.attention
|
||||
EarnBlockUM.TrailingUM.IconTone.Info -> TangemTheme.colors2.fill.neutral.secondary
|
||||
private fun EarnBlockUM.TitleUM.IconTone.tint(): Color = when (this) {
|
||||
EarnBlockUM.TitleUM.IconTone.Warning -> TangemTheme.colors2.graphic.status.attention
|
||||
EarnBlockUM.TitleUM.IconTone.Info -> TangemTheme.colors2.graphic.neutral.secondary
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -359,7 +378,7 @@ private val EarnBlockUM.SubtitleUM.Style.textStyle: TextStyle
|
|||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun EarnBlock_Preview(@PreviewParameter(EarnBlockPreviewProvider::class) state: EarnBlockUM) {
|
||||
private fun EarnBlock_Staking_Preview(@PreviewParameter(EarnBlockStakingPreviewProvider::class) state: EarnBlockUM) {
|
||||
TangemThemePreviewRedesign {
|
||||
EarnBlock(
|
||||
state = state,
|
||||
|
|
@ -368,7 +387,21 @@ private fun EarnBlock_Preview(@PreviewParameter(EarnBlockPreviewProvider::class)
|
|||
}
|
||||
}
|
||||
|
||||
private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<EarnBlockUM>(
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun EarnBlock_YieldSupply_Preview(
|
||||
@PreviewParameter(EarnBlockYieldSupplyPreviewProvider::class) state: EarnBlockUM,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
EarnBlock(
|
||||
state = state,
|
||||
modifier = Modifier.padding(TangemTheme.dimens2.x4),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class EarnBlockStakingPreviewProvider : CollectionPreviewParameterProvider<EarnBlockUM>(
|
||||
collection = listOf(
|
||||
EarnBlockUM.Loading,
|
||||
EarnBlockUM.Content(
|
||||
|
|
@ -376,7 +409,7 @@ private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<Earn
|
|||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_staking_disable_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.staking_native),
|
||||
text = resourceReference(CoreResR.string.common_stake),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Disabled,
|
||||
),
|
||||
|
|
@ -392,17 +425,17 @@ private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<Earn
|
|||
backgroundUM = EarnBlockUM.BackgroundUM.AccentSoft,
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_staking_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.token_details_staking_block_title),
|
||||
style = EarnBlockUM.TitleUM.Style.Small,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Accent,
|
||||
text = resourceReference(CoreResR.string.common_staking),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = stringReference("Average APR 5.24%"),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Primary,
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||
),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||
text = stringReference("Stake"),
|
||||
text = resourceReference(CoreResR.string.common_stake),
|
||||
),
|
||||
onClick = {},
|
||||
),
|
||||
|
|
@ -411,7 +444,7 @@ private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<Earn
|
|||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_staking_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = stringReference("Native staking"),
|
||||
text = resourceReference(CoreResR.string.staking_enabled),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
|
|
@ -427,25 +460,146 @@ private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<Earn
|
|||
),
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterProvider<EarnBlockUM>(
|
||||
collection = listOf(
|
||||
// Available — promo entry: AccentSoft background, "More" button
|
||||
EarnBlockUM.Content(
|
||||
type = Type.YieldSupply,
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.AccentSoft,
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = stringReference("Earn yield"),
|
||||
style = EarnBlockUM.TitleUM.Style.Small,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Accent,
|
||||
text = resourceReference(
|
||||
id = CoreResR.string.yield_module_token_details_earn_notification_subtitle,
|
||||
formatArgs = wrappedList("5.24"),
|
||||
),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = stringReference("Start earning · 5.24%"),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Primary,
|
||||
text = resourceReference(
|
||||
CoreResR.string.yield_module_token_details_earn_notification_description,
|
||||
),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||
),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||
text = stringReference("More"),
|
||||
text = resourceReference(CoreResR.string.common_more),
|
||||
),
|
||||
onClick = {},
|
||||
),
|
||||
// Content — yield enabled, "Details" button
|
||||
EarnBlockUM.Content(
|
||||
type = Type.YieldSupply,
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.yield_module_transaction_enter),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = resourceReference(
|
||||
id = CoreResR.string.yield_module_average_apy,
|
||||
formatArgs = wrappedList("5.24"),
|
||||
),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||
),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||
text = resourceReference(CoreResR.string.details_title),
|
||||
style = EarnBlockUM.TrailingUM.Button.Style.Secondary,
|
||||
),
|
||||
onClick = {},
|
||||
),
|
||||
// Content with Warning title icon
|
||||
EarnBlockUM.Content(
|
||||
type = Type.YieldSupply,
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.common_yield_mode),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
iconUM = EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Warning),
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = resourceReference(
|
||||
id = CoreResR.string.yield_module_average_apy,
|
||||
formatArgs = wrappedList("5.24"),
|
||||
),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||
),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||
text = resourceReference(CoreResR.string.details_title),
|
||||
style = EarnBlockUM.TrailingUM.Button.Style.Secondary,
|
||||
),
|
||||
onClick = {},
|
||||
),
|
||||
// Content with Info title icon
|
||||
EarnBlockUM.Content(
|
||||
type = Type.YieldSupply,
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.common_yield_mode),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
iconUM = EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Info),
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = resourceReference(
|
||||
id = CoreResR.string.yield_module_average_apy,
|
||||
formatArgs = wrappedList("5.24"),
|
||||
),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||
),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||
text = resourceReference(CoreResR.string.details_title),
|
||||
style = EarnBlockUM.TrailingUM.Button.Style.Secondary,
|
||||
),
|
||||
onClick = {},
|
||||
),
|
||||
// Processing.Enter — enabling, no trailing
|
||||
EarnBlockUM.Content(
|
||||
type = Type.YieldSupply,
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.common_yield_mode),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = resourceReference(CoreResR.string.common_enabling),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
|
||||
loader = EarnBlockUM.SubtitleUM.Loader(tone = EarnBlockUM.SubtitleUM.LoaderTone.Positive),
|
||||
),
|
||||
trailingUM = null,
|
||||
),
|
||||
// Processing.Exit — disabling, no trailing, plain icon
|
||||
EarnBlockUM.Content(
|
||||
type = Type.YieldSupply,
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_yield_disabling_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.common_yield_mode),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = resourceReference(CoreResR.string.common_disabling),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||
loader = EarnBlockUM.SubtitleUM.Loader(tone = EarnBlockUM.SubtitleUM.LoaderTone.Muted),
|
||||
),
|
||||
trailingUM = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
// endregion
|
||||
|
|
@ -39,9 +39,13 @@ sealed interface EarnBlockUM {
|
|||
val text: TextReference,
|
||||
val style: Style,
|
||||
val tone: Tone,
|
||||
val iconUM: IconUM? = null,
|
||||
) {
|
||||
enum class Style { Large, Small }
|
||||
enum class Tone { Primary, Secondary, Disabled, Accent }
|
||||
|
||||
data class IconUM(val tone: IconTone)
|
||||
enum class IconTone { Warning, Info }
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
@ -65,18 +69,15 @@ sealed interface EarnBlockUM {
|
|||
data class Button(
|
||||
val text: TextReference,
|
||||
val isEnabled: Boolean = true,
|
||||
) : TrailingUM
|
||||
val style: Style = Style.Default,
|
||||
) : TrailingUM {
|
||||
enum class Style { Default, Secondary }
|
||||
}
|
||||
|
||||
data class Balance(
|
||||
val fiatValue: TextReference,
|
||||
val cryptoValue: TextReference,
|
||||
val isBalanceHidden: Boolean,
|
||||
) : TrailingUM
|
||||
|
||||
data class Icon(
|
||||
val tone: IconTone,
|
||||
) : TrailingUM
|
||||
|
||||
enum class IconTone { Warning, Info }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,42 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
|
||||
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.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.isNullOrEmpty
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
fun LazyListScope.expressTransactionsItems(
|
||||
expressTxs: PersistentList<ExpressTransactionStateUM>,
|
||||
|
|
@ -19,28 +49,170 @@ fun LazyListScope.expressTransactionsItems(
|
|||
) { index ->
|
||||
val itemInfo = expressTxs[index].info
|
||||
val (iconRes, tint) = when (itemInfo.iconState) {
|
||||
ExpressTransactionStateIconUM.Warning -> {
|
||||
R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
|
||||
}
|
||||
ExpressTransactionStateIconUM.Error -> {
|
||||
R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
|
||||
}
|
||||
ExpressTransactionStateIconUM.Warning ->
|
||||
R.drawable.ic_attention_default_24 to TangemTheme.colors2.graphic.status.attention
|
||||
ExpressTransactionStateIconUM.Error ->
|
||||
R.drawable.ic_alert_circle_24 to TangemTheme.colors2.graphic.status.warning
|
||||
ExpressTransactionStateIconUM.None -> null to null
|
||||
}
|
||||
|
||||
ExpressStatusItem(
|
||||
title = itemInfo.title,
|
||||
fromTokenIconState = itemInfo.fromCurrencyIcon,
|
||||
toTokenIconState = itemInfo.toCurrencyIcon,
|
||||
fromAmount = itemInfo.fromAmount,
|
||||
fromSymbol = itemInfo.fromAmountSymbol,
|
||||
toAmount = itemInfo.toAmount,
|
||||
toSymbol = itemInfo.toAmountSymbol,
|
||||
subtitle = itemInfo.subtitle,
|
||||
onClick = itemInfo.onClick,
|
||||
ExpressTransactionItem(
|
||||
state = expressTxs[index],
|
||||
infoIconRes = iconRes,
|
||||
infoIconTint = tint,
|
||||
modifier = modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpressTransactionItem(
|
||||
state: ExpressTransactionStateUM,
|
||||
infoIconRes: Int?,
|
||||
infoIconTint: Color?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val info = state.info
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors2.surface.level3)
|
||||
.clickable(onClick = info.onClick)
|
||||
.padding(TangemTheme.dimens2.x4),
|
||||
) {
|
||||
TitleRow(
|
||||
title = info.title.resolveReference(),
|
||||
infoIconRes = infoIconRes,
|
||||
infoIconTint = infoIconTint,
|
||||
)
|
||||
if (!info.subtitle.isNullOrEmpty()) {
|
||||
Text(
|
||||
text = info.subtitle.resolveReference(),
|
||||
style = TangemTheme.typography2.captionRegular13,
|
||||
color = TangemTheme.colors3.text.tertiary,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.size(TangemTheme.dimens2.x3))
|
||||
AmountsRow(info = info)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleRow(title: String, infoIconRes: Int?, infoIconTint: Color?) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (infoIconRes != null && infoIconTint != null) {
|
||||
Icon(
|
||||
painter = painterResource(infoIconRes),
|
||||
contentDescription = null,
|
||||
tint = infoIconTint,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x3),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountsRow(info: ExpressTransactionStateInfoUM) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1_5),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
state = info.fromCurrencyIcon,
|
||||
shouldDisplayNetwork = false,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
EllipsisText(
|
||||
text = info.fromAmount.resolveReference(),
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
ellipsis = TextEllipsis.OffsetEnd(info.fromAmountSymbol.length),
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_forward_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.tertiary,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
CurrencyIcon(
|
||||
state = info.toCurrencyIcon,
|
||||
shouldDisplayNetwork = false,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
EllipsisText(
|
||||
text = info.toAmount.resolveReference(),
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
ellipsis = TextEllipsis.OffsetEnd(info.toAmountSymbol.length),
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ExpressTransactionItemPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||
modifier = Modifier.padding(TangemTheme.dimens2.x4),
|
||||
) {
|
||||
ExpressTransactionItem(
|
||||
state = PreviewExpressTransactionState,
|
||||
infoIconRes = null,
|
||||
infoIconTint = null,
|
||||
)
|
||||
ExpressTransactionItem(
|
||||
state = PreviewExpressTransactionState,
|
||||
infoIconRes = R.drawable.ic_attention_default_24,
|
||||
infoIconTint = TangemTheme.colors2.graphic.status.attention,
|
||||
)
|
||||
ExpressTransactionItem(
|
||||
state = PreviewExpressTransactionState,
|
||||
infoIconRes = R.drawable.ic_alert_circle_24,
|
||||
infoIconTint = TangemTheme.colors2.graphic.status.warning,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val PreviewExpressTransactionState: ExpressTransactionStateUM = object : ExpressTransactionStateUM {
|
||||
override val info = ExpressTransactionStateInfoUM(
|
||||
title = stringReference("Exchange by ChangeHero"),
|
||||
status = ExpressStatusUM(
|
||||
title = stringReference(""),
|
||||
link = ExpressLinkUM.Empty,
|
||||
statuses = persistentListOf(),
|
||||
),
|
||||
notification = null,
|
||||
txId = "preview",
|
||||
txExternalId = null,
|
||||
txExternalUrl = null,
|
||||
timestamp = 0L,
|
||||
timestampFormatted = stringReference(""),
|
||||
timestampAgoFormatted = stringReference("Confirming ~ 59 min ago"),
|
||||
activeStatus = stringReference(""),
|
||||
onGoToProviderClick = {},
|
||||
onClick = {},
|
||||
onDisposeExpressStatus = {},
|
||||
iconState = ExpressTransactionStateIconUM.None,
|
||||
toAmount = stringReference("0,11441958 BTC"),
|
||||
toFiatAmount = null,
|
||||
toAmountSymbol = "BTC",
|
||||
toCurrencyIcon = CurrencyIconState.Loading,
|
||||
fromAmount = stringReference("100 SOL"),
|
||||
fromFiatAmount = null,
|
||||
fromAmountSymbol = "SOL",
|
||||
fromCurrencyIcon = CurrencyIconState.Loading,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
fun LazyListScope.expressTransactionsItemsLegacy(
|
||||
expressTxs: PersistentList<ExpressTransactionStateUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
items(
|
||||
count = expressTxs.size,
|
||||
key = { index -> expressTxs[index].info.txId },
|
||||
contentType = { index -> expressTxs[index]::class.java },
|
||||
) { index ->
|
||||
val itemInfo = expressTxs[index].info
|
||||
val (iconRes, tint) = when (itemInfo.iconState) {
|
||||
ExpressTransactionStateIconUM.Warning -> {
|
||||
R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
|
||||
}
|
||||
ExpressTransactionStateIconUM.Error -> {
|
||||
R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
|
||||
}
|
||||
ExpressTransactionStateIconUM.None -> null to null
|
||||
}
|
||||
|
||||
ExpressStatusItem(
|
||||
title = itemInfo.title,
|
||||
fromTokenIconState = itemInfo.fromCurrencyIcon,
|
||||
toTokenIconState = itemInfo.toCurrencyIcon,
|
||||
fromAmount = itemInfo.fromAmount,
|
||||
fromSymbol = itemInfo.fromAmountSymbol,
|
||||
toAmount = itemInfo.toAmount,
|
||||
toSymbol = itemInfo.toAmountSymbol,
|
||||
subtitle = itemInfo.subtitle,
|
||||
onClick = itemInfo.onClick,
|
||||
infoIconRes = iconRes,
|
||||
infoIconTint = tint,
|
||||
modifier = modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,5 +12,5 @@ data class ExpressTransactionsBlockState(
|
|||
|
||||
data class BottomSheetSlot(
|
||||
val config: TangemBottomSheetConfig,
|
||||
val content: @Composable () -> Unit,
|
||||
val content: @Composable (extraContent: (@Composable () -> Unit)?) -> Unit,
|
||||
)
|
||||
|
|
@ -196,6 +196,9 @@ private fun iconSetOf(blockchain: Blockchain): IconSet? = when (blockchain) {
|
|||
Blockchain.Plasma,
|
||||
Blockchain.PlasmaTestnet,
|
||||
-> IconSet(active = R.drawable.img_plasma_22, greyedOut = R.drawable.ic_plasma_22)
|
||||
Blockchain.Adi,
|
||||
Blockchain.AdiTestnet,
|
||||
-> IconSet(active = R.drawable.img_adi_22, greyedOut = R.drawable.ic_adi_22)
|
||||
Blockchain.Playa3ull,
|
||||
-> IconSet(active = R.drawable.img_playa3ull_22, greyedOut = R.drawable.ic_playa3ull_22)
|
||||
Blockchain.Polkadot,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
package com.tangem.common.ui.swap
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Resolves which currency goes first (base) and which goes second (quote) when displaying an
|
||||
* exchange rate for a swap pair ([REDACTED_TASK_KEY]).
|
||||
*
|
||||
* Categories used by the rules:
|
||||
* - **Stable** — a [CryptoCurrency.Token] whose normalized symbol is in [STABLECOIN_RANKS].
|
||||
* The symbol is normalized to uppercase and the suffix after `.` is stripped, so bridged
|
||||
* variants (`USDC.E`, `USDT.e`, etc.) are matched against their underlying asset.
|
||||
* - **Coin** — a [CryptoCurrency.Coin] (any native coin: BTC, ETH, SOL, TRX, ...).
|
||||
* - Anything else (a [CryptoCurrency.Token] outside the stable list) falls into the default
|
||||
* branch and is treated as a regular token.
|
||||
*
|
||||
* Rules:
|
||||
* - Stable ↔ Stable: base = the one ranked higher in [STABLECOIN_RANKS].
|
||||
* - Coin ↔ Stable / Stable ↔ Coin: base is the coin.
|
||||
* - Coin ↔ Coin with BTC or ETH: base is the other coin, quote is BTC/ETH.
|
||||
* - ETH ↔ BTC (both directions): base = ETH, quote = BTC.
|
||||
* - Otherwise (regular Coin↔Coin, any pair involving a non-stable Token): base = TO, quote = FROM.
|
||||
*/
|
||||
internal object SwapRateDirectionResolver {
|
||||
|
||||
private val STABLECOIN_RANKS: Map<String, Int> = listOf(
|
||||
"USDT", "USDC", "USDe", "DAI", "USD1", "PYUSD", "RLUSD", "USDG", "USDf", "USDD",
|
||||
).withIndex().associate { (rank, symbol) -> symbol.uppercase(Locale.ROOT) to rank }
|
||||
|
||||
private const val BTC_SYMBOL = "BTC"
|
||||
private const val ETH_SYMBOL = "ETH"
|
||||
|
||||
fun resolve(from: CryptoCurrency, to: CryptoCurrency): SwapRateDirection {
|
||||
val isFromStable = from.isStable()
|
||||
val isToStable = to.isStable()
|
||||
|
||||
return when {
|
||||
isFromStable && isToStable -> resolveStableToStable(from, to)
|
||||
isFromStable -> SwapRateDirection(base = to, quote = from)
|
||||
isToStable -> SwapRateDirection(base = from, quote = to)
|
||||
from.isCoin() && to.isCoin() -> resolveCoinToCoin(from, to)
|
||||
else -> SwapRateDirection(base = to, quote = from)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveStableToStable(from: CryptoCurrency, to: CryptoCurrency): SwapRateDirection {
|
||||
val fromRank = stableRank(from.symbol.normalizeStableSymbol())
|
||||
val toRank = stableRank(to.symbol.normalizeStableSymbol())
|
||||
return if (fromRank <= toRank) {
|
||||
SwapRateDirection(base = from, quote = to)
|
||||
} else {
|
||||
SwapRateDirection(base = to, quote = from)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveCoinToCoin(from: CryptoCurrency, to: CryptoCurrency): SwapRateDirection {
|
||||
val fromSymbol = from.symbol.uppercaseRoot()
|
||||
val toSymbol = to.symbol.uppercaseRoot()
|
||||
val isFromBtcOrEth = fromSymbol.isBtcOrEth()
|
||||
val isToBtcOrEth = toSymbol.isBtcOrEth()
|
||||
|
||||
return when {
|
||||
isFromBtcOrEth && isToBtcOrEth -> resolveBtcEth(from, to, fromSymbol)
|
||||
isFromBtcOrEth -> SwapRateDirection(base = to, quote = from)
|
||||
isToBtcOrEth -> SwapRateDirection(base = from, quote = to)
|
||||
else -> SwapRateDirection(base = to, quote = from)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveBtcEth(from: CryptoCurrency, to: CryptoCurrency, fromSymbol: String): SwapRateDirection {
|
||||
return if (fromSymbol == ETH_SYMBOL) {
|
||||
SwapRateDirection(base = from, quote = to)
|
||||
} else {
|
||||
SwapRateDirection(base = to, quote = from)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stableRank(symbol: String): Int = STABLECOIN_RANKS[symbol] ?: Int.MAX_VALUE
|
||||
|
||||
private fun CryptoCurrency.isStable(): Boolean {
|
||||
return this is CryptoCurrency.Token && STABLECOIN_RANKS.containsKey(symbol.normalizeStableSymbol())
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.isCoin(): Boolean = this is CryptoCurrency.Coin
|
||||
|
||||
private fun String.isBtcOrEth(): Boolean = this == BTC_SYMBOL || this == ETH_SYMBOL
|
||||
|
||||
private fun String.uppercaseRoot(): String = uppercase(Locale.ROOT)
|
||||
|
||||
/**
|
||||
* Drops bridge/wrapped suffix (e.g. `USDC.E` → `USDC`, `USDT.e` → `USDT`) before stable lookup.
|
||||
* Bridged variants share the underlying asset's rank.
|
||||
*/
|
||||
private fun String.normalizeStableSymbol(): String = substringBefore('.').uppercaseRoot()
|
||||
}
|
||||
|
||||
internal data class SwapRateDirection(val base: CryptoCurrency, val quote: CryptoCurrency)
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.common.ui.swap
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import com.tangem.core.ui.extensions.appendSpace
|
||||
import com.tangem.core.ui.format.bigdecimal.anyDecimals
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.utils.StringsSigns
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Formats a swap exchange rate as `1 {base} ≈ {rate} {quote}`.
|
||||
*
|
||||
* The base/quote choice follows the rules in [SwapRateDirectionResolver] ([REDACTED_TASK_KEY]).
|
||||
*/
|
||||
object SwapRateFormatter {
|
||||
|
||||
private const val MAX_DECIMALS_TO_SHOW = 8
|
||||
private const val IF_ZERO_DECIMALS_TO_SHOW = 2
|
||||
|
||||
fun formatRate(from: CryptoCurrency, to: CryptoCurrency, fromAmount: BigDecimal, toAmount: BigDecimal): String {
|
||||
val (base, quote, rate) = computeRate(
|
||||
from = from,
|
||||
to = to,
|
||||
fromAmount = fromAmount,
|
||||
toAmount = toAmount,
|
||||
)
|
||||
return buildString {
|
||||
append(BigDecimal.ONE.format { crypto(symbol = base.symbol, decimals = 0).anyDecimals() })
|
||||
append(StringsSigns.WHITE_SPACE)
|
||||
append(StringsSigns.APPROXIMATE)
|
||||
append(StringsSigns.WHITE_SPACE)
|
||||
append(rate.format { crypto(quote) })
|
||||
}
|
||||
}
|
||||
|
||||
fun formatRateAnnotated(
|
||||
from: CryptoCurrency,
|
||||
to: CryptoCurrency,
|
||||
fromAmount: BigDecimal,
|
||||
toAmount: BigDecimal,
|
||||
): AnnotatedString {
|
||||
val (base, quote, rate) = computeRate(
|
||||
from = from,
|
||||
to = to,
|
||||
fromAmount = fromAmount,
|
||||
toAmount = toAmount,
|
||||
)
|
||||
return buildAnnotatedString {
|
||||
append(BigDecimal.ONE.format { crypto(symbol = base.symbol, decimals = 0).anyDecimals() })
|
||||
appendSpace()
|
||||
append(StringsSigns.APPROXIMATE)
|
||||
appendSpace()
|
||||
append(rate.format { crypto(quote) })
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeRate(
|
||||
from: CryptoCurrency,
|
||||
to: CryptoCurrency,
|
||||
fromAmount: BigDecimal,
|
||||
toAmount: BigDecimal,
|
||||
): RateComputation {
|
||||
val direction = SwapRateDirectionResolver.resolve(from, to)
|
||||
val baseAmount: BigDecimal
|
||||
val quoteAmount: BigDecimal
|
||||
if (direction.base == from) {
|
||||
baseAmount = fromAmount
|
||||
quoteAmount = toAmount
|
||||
} else {
|
||||
baseAmount = toAmount
|
||||
quoteAmount = fromAmount
|
||||
}
|
||||
val rate = if (baseAmount.signum() == 0) {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
val rateDecimals = if (direction.quote.decimals == 0) IF_ZERO_DECIMALS_TO_SHOW else direction.quote.decimals
|
||||
quoteAmount.divide(baseAmount, min(rateDecimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP)
|
||||
}
|
||||
return RateComputation(direction.base, direction.quote, rate)
|
||||
}
|
||||
|
||||
private data class RateComputation(val base: CryptoCurrency, val quote: CryptoCurrency, val rate: BigDecimal)
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
package com.tangem.common.ui.tokenaction
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.ds.row.TangemRowContainer
|
||||
import com.tangem.core.ui.ds.row.TangemRowLayoutId
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.haptic.HapticManager
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
private const val ACTION_BACKGROUND_ALPHA = .1f
|
||||
|
||||
/**
|
||||
* Single-row token action ("Buy", "Receive", etc.) with accent icon, title, description and a
|
||||
* customizable tail. Used in bottom sheets like Get Token / Add Funds and Add To Portfolio.
|
||||
*
|
||||
* @param iconRes leading 20dp icon drawn over an accent-colored circle
|
||||
* @param title row primary text
|
||||
* @param description row secondary text
|
||||
* @param onClick single-click callback; row is non-interactive if `null`. Fires regardless
|
||||
* of [isEnabled] — gating is the caller's responsibility (pass `null` to
|
||||
* make the row non-interactive)
|
||||
* @param onLongClick long-press callback; pass `null` to disable long-press. Fires regardless
|
||||
* of [isEnabled]
|
||||
* @param isEnabled controls visual styling only (disabled-tier colors when `false`)
|
||||
* @param tailContent content placed at the row's end. Defaults to a chevron-right icon.
|
||||
*/
|
||||
@Composable
|
||||
fun TokenActionRow(
|
||||
@DrawableRes iconRes: Int,
|
||||
title: TextReference,
|
||||
description: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
isEnabled: Boolean = true,
|
||||
tailContent: @Composable () -> Unit = { DefaultTokenActionRowChevron(isEnabled = isEnabled) },
|
||||
) {
|
||||
val hapticManager = LocalHapticManager.current
|
||||
val accentColor = accentColor(isEnabled)
|
||||
TangemRowContainer(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = TangemTheme.colors2.surface.level3,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
|
||||
)
|
||||
.clickableWithHaptic(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
hapticManager = hapticManager,
|
||||
),
|
||||
) {
|
||||
LeadingIcon(iconRes = iconRes, accentColor = accentColor)
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = titleColor(isEnabled),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
|
||||
text = description.resolveReference(),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = descriptionColor(isEnabled),
|
||||
)
|
||||
Tail { tailContent() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeadingIcon(@DrawableRes iconRes: Int, accentColor: Color) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.HEAD)
|
||||
.padding(end = TangemTheme.dimens2.x3)
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = accentColor.copy(alpha = ACTION_BACKGROUND_ALPHA),
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = ImageVector.vectorResource(id = iconRes),
|
||||
contentDescription = null,
|
||||
tint = accentColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Tail(content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.TAIL)
|
||||
.padding(start = TangemTheme.dimens2.x2)
|
||||
.size(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun accentColor(isEnabled: Boolean): Color = if (isEnabled) {
|
||||
TangemTheme.colors2.graphic.status.accent
|
||||
} else {
|
||||
TangemTheme.colors2.graphic.neutral.quaternary
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun titleColor(isEnabled: Boolean): Color = if (isEnabled) {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
} else {
|
||||
TangemTheme.colors2.text.status.disabled
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun descriptionColor(isEnabled: Boolean): Color = if (isEnabled) {
|
||||
TangemTheme.colors2.text.neutral.secondary
|
||||
} else {
|
||||
TangemTheme.colors2.text.status.disabled
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun Modifier.clickableWithHaptic(
|
||||
onClick: (() -> Unit)?,
|
||||
onLongClick: (() -> Unit)?,
|
||||
hapticManager: HapticManager,
|
||||
): Modifier {
|
||||
if (onClick == null) return this
|
||||
return combinedClickable(
|
||||
onClick = hapticManager.withHaptic(TangemHapticEffect.View.SegmentTick, onClick),
|
||||
onLongClick = onLongClick?.let { hapticManager.withHaptic(TangemHapticEffect.View.LongPress, it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun HapticManager.withHaptic(effect: TangemHapticEffect, action: () -> Unit): () -> Unit = {
|
||||
perform(effect)
|
||||
action()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultTokenActionRowChevron(isEnabled: Boolean) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24),
|
||||
tint = if (isEnabled) {
|
||||
TangemTheme.colors2.graphic.neutral.tertiary
|
||||
} else {
|
||||
TangemTheme.colors2.graphic.neutral.quaternary
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import com.tangem.domain.models.currency.yieldSupplyKey
|
|||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingOption
|
||||
import com.tangem.domain.staking.model.optionOrNull
|
||||
import com.tangem.domain.staking.model.common.RewardInfo
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
|
|
@ -244,14 +245,21 @@ class TokenItemStateConverter(
|
|||
currencyStatus: CryptoCurrencyStatus,
|
||||
stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
|
||||
): StakingLocalInfo {
|
||||
val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available
|
||||
val availability = stakingApyMap[currencyStatus.currency]
|
||||
val option = availability?.optionOrNull
|
||||
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
|
||||
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
|
||||
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
||||
val p2pEthPoolBalance = stakingBalance as? StakingBalance.Data.P2PEthPool
|
||||
val isActive = stakeKitBalance != null || p2pEthPoolBalance != null
|
||||
|
||||
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
|
||||
// Full = no free capacity: show the badge only for tokens that already have a stake.
|
||||
if (availability is StakingAvailability.Full && !isActive) {
|
||||
return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
}
|
||||
|
||||
val rateInfo = when (val stakingOptions = option) {
|
||||
is StakingOption.P2PEthPool -> {
|
||||
RewardInfo(
|
||||
rate = stakingOptions.apy,
|
||||
|
|
@ -282,7 +290,7 @@ class TokenItemStateConverter(
|
|||
|
||||
return StakingLocalInfo(
|
||||
rate = rateInfo?.rate,
|
||||
isActive = stakeKitBalance != null || p2pEthPoolBalance != null,
|
||||
isActive = isActive,
|
||||
rewardType = rateInfo?.type,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.common.ui.userwallet.ext
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
||||
fun walletInterationIcon(userWallet: UserWallet): Int? {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.common.ui.amountScreen.converters
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.amountScreen.models.AmountParameters
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class AmountStateConverterTest {
|
||||
|
||||
private val currency = mockk<CryptoCurrency.Coin> {
|
||||
every { symbol } returns "ETH"
|
||||
every { decimals } returns 18
|
||||
every { name } returns "Ethereum"
|
||||
}
|
||||
private val status = CryptoCurrencyStatus(currency = currency, value = mockk(relaxed = true))
|
||||
private val iconStateConverter = mockk<CryptoCurrencyToIconStateConverter> {
|
||||
every { convert(any<CryptoCurrency>()) } returns CurrencyIconState.Loading
|
||||
}
|
||||
private val clickIntents = mockk<AmountScreenClickIntents>(relaxed = true)
|
||||
private val accountTitleUM = mockk<AccountTitleUM>()
|
||||
|
||||
private fun convert(isMaxButtonVisible: Boolean = true): AmountState = AmountStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrency = AppCurrency.Default,
|
||||
cryptoCurrencyStatus = status,
|
||||
maxEnterAmount = EnterAmountBoundary(
|
||||
amount = BigDecimal.ONE,
|
||||
fiatAmount = BigDecimal.TEN,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
),
|
||||
iconStateConverter = iconStateConverter,
|
||||
isBalanceHidden = false,
|
||||
accountTitleUM = accountTitleUM,
|
||||
isMaxButtonVisible = isMaxButtonVisible,
|
||||
).convert(AmountParameters(title = stringReference("Wallet"), value = ""))
|
||||
|
||||
@Test
|
||||
fun `GIVEN no isMaxButtonVisible param WHEN convert THEN Data isMaxButtonVisible is true`() {
|
||||
val result = convert()
|
||||
|
||||
assertThat((result as AmountState.Data).isMaxButtonVisible).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN isMaxButtonVisible false WHEN convert THEN Data isMaxButtonVisible is false`() {
|
||||
val result = convert(isMaxButtonVisible = false)
|
||||
|
||||
assertThat((result as AmountState.Data).isMaxButtonVisible).isFalse()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
package com.tangem.common.ui.components.currency.icon.converter
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class CryptoCurrencyToIconStateConverterTest {
|
||||
|
||||
private val sut = CryptoCurrencyToIconStateConverter(isAvailable = true)
|
||||
private val sutUnavailable = CryptoCurrencyToIconStateConverter(isAvailable = false)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic("com.tangem.common.ui.extensions.NetworkIconExtKt")
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
// region public API — convert(value: CryptoCurrencyStatus)
|
||||
|
||||
@Test
|
||||
fun `GIVEN coin status WHEN convert THEN return CoinIcon with currency and network fields`() {
|
||||
val coin = buildCoin(
|
||||
isTestnet = false,
|
||||
isCustom = false,
|
||||
iconUrl = "https://example.com/eth.png",
|
||||
)
|
||||
val status = buildStatus(currency = coin, isError = false)
|
||||
|
||||
val result = sut.convert(status)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
CurrencyIconState.CoinIcon(
|
||||
url = "https://example.com/eth.png",
|
||||
fallbackResId = NETWORK_ICON_RES_ID,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token status WHEN convert THEN return TokenIcon with currency and network fields`() {
|
||||
val token = buildToken(
|
||||
isTestnet = false,
|
||||
isCustom = false,
|
||||
iconUrl = "https://example.com/usdt.png",
|
||||
contractAddress = USDT_CONTRACT,
|
||||
)
|
||||
val status = buildStatus(currency = token, isError = false)
|
||||
|
||||
val result = sut.convert(status) as CurrencyIconState.TokenIcon
|
||||
|
||||
assertThat(result.url).isEqualTo("https://example.com/usdt.png")
|
||||
assertThat(result.topBadgeIconResId).isEqualTo(NETWORK_ICON_RES_ID)
|
||||
assertThat(result.isGrayscale).isFalse()
|
||||
assertThat(result.shouldShowCustomBadge).isFalse()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region public API — convert(currency: CryptoCurrency)
|
||||
|
||||
@Test
|
||||
fun `GIVEN coin currency WHEN convert without status THEN return CoinIcon with isUnreachable=false`() {
|
||||
val coin = buildCoin(isTestnet = false, isCustom = false, iconUrl = "url")
|
||||
|
||||
val result = sut.convert(currency = coin) as CurrencyIconState.CoinIcon
|
||||
|
||||
assertThat(result.isGrayscale).isFalse()
|
||||
assertThat(result.url).isEqualTo("url")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token currency WHEN convert without status THEN return TokenIcon with isErrorStatus=false`() {
|
||||
val token = buildToken(
|
||||
isTestnet = false,
|
||||
isCustom = false,
|
||||
iconUrl = "url",
|
||||
contractAddress = USDT_CONTRACT,
|
||||
)
|
||||
|
||||
val result = sut.convert(currency = token) as CurrencyIconState.TokenIcon
|
||||
|
||||
assertThat(result.isGrayscale).isFalse()
|
||||
assertThat(result.url).isEqualTo("url")
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region public API — convertCustom
|
||||
|
||||
@Test
|
||||
fun `GIVEN coin status with custom flag WHEN convertCustom with forceGrayscale and badge off THEN both flags propagate`() {
|
||||
val coin = buildCoin(isTestnet = false, isCustom = true, iconUrl = "url")
|
||||
val status = buildStatus(currency = coin, isError = false)
|
||||
|
||||
val result = sut.convertCustom(
|
||||
value = status,
|
||||
forceGrayscale = true,
|
||||
showCustomTokenBadge = false,
|
||||
) as CurrencyIconState.CoinIcon
|
||||
|
||||
assertThat(result.isGrayscale).isTrue()
|
||||
assertThat(result.shouldShowCustomBadge).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token status WHEN convertCustom with forceGrayscale THEN TokenIcon is grayscale`() {
|
||||
val token = buildToken(
|
||||
isTestnet = false,
|
||||
isCustom = false,
|
||||
iconUrl = "url",
|
||||
contractAddress = USDT_CONTRACT,
|
||||
)
|
||||
val status = buildStatus(currency = token, isError = false)
|
||||
|
||||
val result = sut.convertCustom(
|
||||
value = status,
|
||||
forceGrayscale = true,
|
||||
showCustomTokenBadge = true,
|
||||
) as CurrencyIconState.TokenIcon
|
||||
|
||||
assertThat(result.isGrayscale).isTrue()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getIconStateForCoin — isGrayscale matrix
|
||||
|
||||
@Test
|
||||
fun `GIVEN no override and live data WHEN convert coin THEN isGrayscale is false`() {
|
||||
val coin = buildCoin(isTestnet = false, isCustom = false, iconUrl = "url")
|
||||
val status = buildStatus(currency = coin, isError = false)
|
||||
|
||||
val result = sut.convert(status) as CurrencyIconState.CoinIcon
|
||||
|
||||
assertThat(result.isGrayscale).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN testnet network WHEN convert coin THEN isGrayscale is true`() {
|
||||
val coin = buildCoin(isTestnet = true, isCustom = false, iconUrl = "url")
|
||||
val status = buildStatus(currency = coin, isError = false)
|
||||
|
||||
val result = sut.convert(status) as CurrencyIconState.CoinIcon
|
||||
|
||||
assertThat(result.isGrayscale).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN error status WHEN convert coin THEN isGrayscale is true`() {
|
||||
val coin = buildCoin(isTestnet = false, isCustom = false, iconUrl = "url")
|
||||
val status = buildStatus(currency = coin, isError = true)
|
||||
|
||||
val result = sut.convert(status) as CurrencyIconState.CoinIcon
|
||||
|
||||
assertThat(result.isGrayscale).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN converter not available WHEN convert coin THEN isGrayscale is true`() {
|
||||
val coin = buildCoin(isTestnet = false, isCustom = false, iconUrl = "url")
|
||||
val status = buildStatus(currency = coin, isError = false)
|
||||
|
||||
val result = sutUnavailable.convert(status) as CurrencyIconState.CoinIcon
|
||||
|
||||
assertThat(result.isGrayscale).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN forceGrayscale flag WHEN convertCustom coin THEN isGrayscale is true`() {
|
||||
val coin = buildCoin(isTestnet = false, isCustom = false, iconUrl = "url")
|
||||
val status = buildStatus(currency = coin, isError = false)
|
||||
|
||||
val result = sut.convertCustom(
|
||||
value = status,
|
||||
forceGrayscale = true,
|
||||
showCustomTokenBadge = true,
|
||||
) as CurrencyIconState.CoinIcon
|
||||
|
||||
assertThat(result.isGrayscale).isTrue()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getIconStateForToken — branches
|
||||
|
||||
@Test
|
||||
fun `GIVEN custom token without iconUrl WHEN convert THEN return CustomTokenIcon`() {
|
||||
val token = buildToken(
|
||||
isTestnet = false,
|
||||
isCustom = true,
|
||||
iconUrl = null,
|
||||
contractAddress = USDT_CONTRACT,
|
||||
)
|
||||
val status = buildStatus(currency = token, isError = false)
|
||||
|
||||
val result = sut.convert(status) as CurrencyIconState.CustomTokenIcon
|
||||
|
||||
assertThat(result.topBadgeIconResId).isEqualTo(NETWORK_ICON_RES_ID)
|
||||
assertThat(result.isGrayscale).isFalse()
|
||||
assertThat(result.shouldShowCustomBadge).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN custom token with iconUrl WHEN convert THEN return TokenIcon with custom badge`() {
|
||||
val token = buildToken(
|
||||
isTestnet = false,
|
||||
isCustom = true,
|
||||
iconUrl = "https://example.com/usdt.png",
|
||||
contractAddress = USDT_CONTRACT,
|
||||
)
|
||||
val status = buildStatus(currency = token, isError = false)
|
||||
|
||||
val result = sut.convert(status) as CurrencyIconState.TokenIcon
|
||||
|
||||
assertThat(result.url).isEqualTo("https://example.com/usdt.png")
|
||||
assertThat(result.shouldShowCustomBadge).isTrue()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region helpers
|
||||
|
||||
private fun buildCoin(
|
||||
isTestnet: Boolean,
|
||||
isCustom: Boolean,
|
||||
iconUrl: String?,
|
||||
): CryptoCurrency.Coin {
|
||||
val network: Network = mockk { every { this@mockk.isTestnet } returns isTestnet }
|
||||
val coin: CryptoCurrency.Coin = mockk()
|
||||
every { coin.network } returns network
|
||||
every { coin.iconUrl } returns iconUrl
|
||||
every { coin.isCustom } returns isCustom
|
||||
every { coin.networkIconResId } returns NETWORK_ICON_RES_ID
|
||||
return coin
|
||||
}
|
||||
|
||||
private fun buildToken(
|
||||
isTestnet: Boolean,
|
||||
isCustom: Boolean,
|
||||
iconUrl: String?,
|
||||
contractAddress: String,
|
||||
): CryptoCurrency.Token {
|
||||
val network: Network = mockk { every { this@mockk.isTestnet } returns isTestnet }
|
||||
val token: CryptoCurrency.Token = mockk()
|
||||
every { token.network } returns network
|
||||
every { token.iconUrl } returns iconUrl
|
||||
every { token.isCustom } returns isCustom
|
||||
every { token.contractAddress } returns contractAddress
|
||||
every { token.networkIconResId } returns NETWORK_ICON_RES_ID
|
||||
return token
|
||||
}
|
||||
|
||||
private fun buildStatus(currency: CryptoCurrency, isError: Boolean): CryptoCurrencyStatus {
|
||||
val value: CryptoCurrencyStatus.Value = mockk { every { this@mockk.isError } returns isError }
|
||||
return CryptoCurrencyStatus(currency = currency, value = value)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private companion object {
|
||||
const val NETWORK_ICON_RES_ID = 1234
|
||||
const val USDT_CONTRACT = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
|
||||
}
|
||||
}
|
||||
|
|
@ -96,6 +96,7 @@ internal class BlockchainIconsTest {
|
|||
Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.img_optimism_22
|
||||
Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> R.drawable.img_pepecoin_22
|
||||
Blockchain.Plasma, Blockchain.PlasmaTestnet -> R.drawable.img_plasma_22
|
||||
Blockchain.Adi, Blockchain.AdiTestnet -> R.drawable.img_adi_22
|
||||
Blockchain.Playa3ull -> R.drawable.img_playa3ull_22
|
||||
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.drawable.img_polkadot_22
|
||||
Blockchain.Polygon, Blockchain.PolygonTestnet -> R.drawable.img_polygon_22
|
||||
|
|
@ -218,6 +219,7 @@ internal class BlockchainIconsTest {
|
|||
Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism_22
|
||||
Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> R.drawable.ic_pepecoin_22
|
||||
Blockchain.Plasma, Blockchain.PlasmaTestnet -> R.drawable.ic_plasma_22
|
||||
Blockchain.Adi, Blockchain.AdiTestnet -> R.drawable.ic_adi_22
|
||||
Blockchain.Playa3ull -> R.drawable.ic_playa3ull_22
|
||||
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.drawable.ic_polkadot_16
|
||||
Blockchain.Polygon, Blockchain.PolygonTestnet -> R.drawable.ic_polygon_22
|
||||
|
|
|
|||
|
|
@ -0,0 +1,244 @@
|
|||
package com.tangem.common.ui.swap
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class SwapRateDirectionResolverTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN stable usdt and stable usdc WHEN resolve THEN base is usdt`() {
|
||||
val usdt = stable(symbol = "USDT")
|
||||
val usdc = stable(symbol = "USDC")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = usdt, to = usdc)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = usdt, quote = usdc))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stable dai and stable usdt WHEN resolve THEN base is usdt`() {
|
||||
val dai = stable(symbol = "DAI")
|
||||
val usdt = stable(symbol = "USDT")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = dai, to = usdt)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = usdt, quote = dai))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stable usdd and stable usdc WHEN resolve THEN base is usdc`() {
|
||||
val usdd = stable(symbol = "USDD")
|
||||
val usdc = stable(symbol = "USDC")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = usdd, to = usdc)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = usdc, quote = usdd))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN coin and stable WHEN resolve THEN base is coin`() {
|
||||
val sol = coin(symbol = "SOL")
|
||||
val usdt = stable(symbol = "USDT")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = sol, to = usdt)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = sol, quote = usdt))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stable and coin WHEN resolve THEN base is coin`() {
|
||||
val usdt = stable(symbol = "USDT")
|
||||
val sol = coin(symbol = "SOL")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = usdt, to = sol)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = sol, quote = usdt))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN coin and btc WHEN resolve THEN base is coin and quote is btc`() {
|
||||
val sol = coin(symbol = "SOL")
|
||||
val btc = coin(symbol = "BTC")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = sol, to = btc)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = sol, quote = btc))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN btc and coin WHEN resolve THEN base is coin and quote is btc`() {
|
||||
val btc = coin(symbol = "BTC")
|
||||
val trx = coin(symbol = "TRX")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = btc, to = trx)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = trx, quote = btc))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN coin and eth WHEN resolve THEN base is coin and quote is eth`() {
|
||||
val sol = coin(symbol = "SOL")
|
||||
val eth = coin(symbol = "ETH")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = sol, to = eth)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = sol, quote = eth))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN eth and coin WHEN resolve THEN base is coin and quote is eth`() {
|
||||
val eth = coin(symbol = "ETH")
|
||||
val sol = coin(symbol = "SOL")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = eth, to = sol)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = sol, quote = eth))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN btc and eth WHEN resolve THEN base is eth and quote is btc`() {
|
||||
val btc = coin(symbol = "BTC")
|
||||
val eth = coin(symbol = "ETH")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = btc, to = eth)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = eth, quote = btc))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN eth and btc WHEN resolve THEN base is eth and quote is btc`() {
|
||||
val eth = coin(symbol = "ETH")
|
||||
val btc = coin(symbol = "BTC")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = eth, to = btc)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = eth, quote = btc))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two non-major coins WHEN resolve THEN base is to and quote is from`() {
|
||||
val sol = coin(symbol = "SOL")
|
||||
val trx = coin(symbol = "TRX")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = sol, to = trx)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = trx, quote = sol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token symbol matching priority list but not Token type WHEN resolve THEN treated as coin`() {
|
||||
// Edge: a CryptoCurrency.Coin whose symbol coincidentally equals a stable symbol must NOT
|
||||
// be treated as stable — the type check is type-aware now.
|
||||
val usdtLikeCoin = coin(symbol = "USDT")
|
||||
val usdt = stable(symbol = "USDT")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = usdtLikeCoin, to = usdt)
|
||||
|
||||
// usdt is stable, usdtLikeCoin is a Coin → Coin↔Stable rule, base = coin
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = usdtLikeCoin, quote = usdt))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-stable token and coin WHEN resolve THEN base is to and quote is from`() {
|
||||
// Non-stable Token (e.g., LINK) is neither Stable nor Coin → falls into default branch.
|
||||
val link = token(symbol = "LINK")
|
||||
val eth = coin(symbol = "ETH")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = link, to = eth)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = eth, quote = link))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two non-stable tokens WHEN resolve THEN base is to and quote is from`() {
|
||||
val link = token(symbol = "LINK")
|
||||
val aave = token(symbol = "AAVE")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = link, to = aave)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = aave, quote = link))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN lowercase usdt and lowercase usdc WHEN resolve THEN base is usdt`() {
|
||||
val usdt = stable(symbol = "usdt")
|
||||
val usdc = stable(symbol = "usdc")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = usdt, to = usdc)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = usdt, quote = usdc))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stable usdt and bridged usdc_e WHEN resolve THEN base is usdt`() {
|
||||
val usdt = stable(symbol = "USDT")
|
||||
val usdcE = stable(symbol = "USDC.E")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = usdt, to = usdcE)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = usdt, quote = usdcE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN bridged usdc_e and stable usdt WHEN resolve THEN base is usdt`() {
|
||||
val usdcE = stable(symbol = "USDC.E")
|
||||
val usdt = stable(symbol = "USDT")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = usdcE, to = usdt)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = usdt, quote = usdcE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN coin sol and bridged usdc_e WHEN resolve THEN base is coin`() {
|
||||
val sol = coin(symbol = "SOL")
|
||||
val usdcE = stable(symbol = "USDC.E")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = sol, to = usdcE)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = sol, quote = usdcE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN bridged usdc_e and coin sol WHEN resolve THEN base is coin`() {
|
||||
val usdcE = stable(symbol = "USDC.E")
|
||||
val sol = coin(symbol = "SOL")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = usdcE, to = sol)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = sol, quote = usdcE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN lowercase bridged usdt_e and stable usdc WHEN resolve THEN base is usdt`() {
|
||||
val usdtE = stable(symbol = "usdt.e")
|
||||
val usdc = stable(symbol = "USDC")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = usdtE, to = usdc)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = usdtE, quote = usdc))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN bridged dai_e and bridged usdc_e WHEN resolve THEN base is usdc`() {
|
||||
val daiE = stable(symbol = "DAI.E")
|
||||
val usdcE = stable(symbol = "USDC.E")
|
||||
|
||||
val result = SwapRateDirectionResolver.resolve(from = daiE, to = usdcE)
|
||||
|
||||
assertThat(result).isEqualTo(SwapRateDirection(base = usdcE, quote = daiE))
|
||||
}
|
||||
|
||||
private fun coin(symbol: String): CryptoCurrency = mockk<CryptoCurrency.Coin> {
|
||||
every { this@mockk.symbol } returns symbol
|
||||
}
|
||||
|
||||
private fun token(symbol: String): CryptoCurrency = mockk<CryptoCurrency.Token> {
|
||||
every { this@mockk.symbol } returns symbol
|
||||
}
|
||||
|
||||
private fun stable(symbol: String): CryptoCurrency = token(symbol)
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
package com.tangem.common.ui.swap
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.utils.StringsSigns
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
internal class SwapRateFormatterTest {
|
||||
|
||||
private var originalLocale: Locale = Locale.getDefault()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
originalLocale = Locale.getDefault()
|
||||
Locale.setDefault(Locale.US)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
Locale.setDefault(originalLocale)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN coin to stable swap WHEN formatRate THEN base is coin`() {
|
||||
val eth = coin(symbol = "ETH", decimals = 18)
|
||||
val usdt = stable(symbol = "USDT", decimals = 6)
|
||||
|
||||
val result = SwapRateFormatter.formatRate(
|
||||
from = eth,
|
||||
to = usdt,
|
||||
fromAmount = BigDecimal.ONE,
|
||||
toAmount = BigDecimal("3000"),
|
||||
)
|
||||
|
||||
result.assertOrder(base = "ETH", quote = "USDT")
|
||||
assertThat(result).contains("3,000")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stable to coin swap WHEN formatRate THEN base is coin`() {
|
||||
val usdt = stable(symbol = "USDT", decimals = 6)
|
||||
val eth = coin(symbol = "ETH", decimals = 18)
|
||||
|
||||
val result = SwapRateFormatter.formatRate(
|
||||
from = usdt,
|
||||
to = eth,
|
||||
fromAmount = BigDecimal("3000"),
|
||||
toAmount = BigDecimal.ONE,
|
||||
)
|
||||
|
||||
result.assertOrder(base = "ETH", quote = "USDT")
|
||||
assertThat(result).contains("3,000")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN btc to other coin swap WHEN formatRate THEN base is other coin`() {
|
||||
val btc = coin(symbol = "BTC", decimals = 8)
|
||||
val sol = coin(symbol = "SOL", decimals = 8)
|
||||
|
||||
val result = SwapRateFormatter.formatRate(
|
||||
from = btc,
|
||||
to = sol,
|
||||
fromAmount = BigDecimal.ONE,
|
||||
toAmount = BigDecimal("20"),
|
||||
)
|
||||
|
||||
result.assertOrder(base = "SOL", quote = "BTC")
|
||||
assertThat(result).contains("0.05")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN btc and eth swap WHEN formatRate THEN base is eth`() {
|
||||
val btc = coin(symbol = "BTC", decimals = 8)
|
||||
val eth = coin(symbol = "ETH", decimals = 18)
|
||||
|
||||
val result = SwapRateFormatter.formatRate(
|
||||
from = btc,
|
||||
to = eth,
|
||||
fromAmount = BigDecimal.ONE,
|
||||
toAmount = BigDecimal("18"),
|
||||
)
|
||||
|
||||
result.assertOrder(base = "ETH", quote = "BTC")
|
||||
assertThat(result).contains("0.05555")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two stables swap WHEN formatRate THEN base is higher ranked`() {
|
||||
val usdc = stable(symbol = "USDC", decimals = 6)
|
||||
val dai = stable(symbol = "DAI", decimals = 18)
|
||||
|
||||
val result = SwapRateFormatter.formatRate(
|
||||
from = dai,
|
||||
to = usdc,
|
||||
fromAmount = BigDecimal.ONE,
|
||||
toAmount = BigDecimal("0.999"),
|
||||
)
|
||||
|
||||
result.assertOrder(base = "USDC", quote = "DAI")
|
||||
assertThat(result).contains("1.001")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two non-major coins swap WHEN formatRate THEN base is to currency`() {
|
||||
val sol = coin(symbol = "SOL", decimals = 8)
|
||||
val trx = coin(symbol = "TRX", decimals = 6)
|
||||
|
||||
val result = SwapRateFormatter.formatRate(
|
||||
from = sol,
|
||||
to = trx,
|
||||
fromAmount = BigDecimal.ONE,
|
||||
toAmount = BigDecimal("100"),
|
||||
)
|
||||
|
||||
result.assertOrder(base = "TRX", quote = "SOL")
|
||||
assertThat(result).contains("0.01")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN zero from amount WHEN formatRate THEN rate is zero`() {
|
||||
val eth = coin(symbol = "ETH", decimals = 18)
|
||||
val usdt = stable(symbol = "USDT", decimals = 6)
|
||||
|
||||
val result = SwapRateFormatter.formatRate(
|
||||
from = eth,
|
||||
to = usdt,
|
||||
fromAmount = BigDecimal.ZERO,
|
||||
toAmount = BigDecimal.ZERO,
|
||||
)
|
||||
|
||||
result.assertOrder(base = "ETH", quote = "USDT")
|
||||
assertThat(result).contains("0.00")
|
||||
}
|
||||
|
||||
private fun String.assertOrder(base: String, quote: String) {
|
||||
val baseIndex = indexOf(base)
|
||||
val quoteIndex = lastIndexOf(quote)
|
||||
assertThat(baseIndex).isAtLeast(0)
|
||||
assertThat(quoteIndex).isGreaterThan(baseIndex)
|
||||
assertThat(this).contains(StringsSigns.APPROXIMATE)
|
||||
val approximateIndex = indexOf(StringsSigns.APPROXIMATE)
|
||||
assertThat(approximateIndex).isGreaterThan(baseIndex)
|
||||
assertThat(quoteIndex).isGreaterThan(approximateIndex)
|
||||
}
|
||||
|
||||
private fun coin(symbol: String, decimals: Int): CryptoCurrency = mockk<CryptoCurrency.Coin> {
|
||||
every { this@mockk.symbol } returns symbol
|
||||
every { this@mockk.decimals } returns decimals
|
||||
}
|
||||
|
||||
private fun stable(symbol: String, decimals: Int): CryptoCurrency = mockk<CryptoCurrency.Token> {
|
||||
every { this@mockk.symbol } returns symbol
|
||||
every { this@mockk.decimals } returns decimals
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue