Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-22 00:37:34 +02:00
commit ee12920c42
28 changed files with 1545 additions and 148 deletions

View file

@ -0,0 +1,60 @@
package com.tangem.core.ui.components.fields
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.input.VisualTransformation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* Simple text field with placeholder
*/
@Composable
fun SimpleTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
placeholder: TextReference? = null,
singleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
color: Color = TangemTheme.colors.text.primary1,
readOnly: Boolean = false,
) {
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = TangemTheme.typography.body2.copy(color = color),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
decorationBox = { textValue ->
Box {
if (value.isBlank() && placeholder != null) {
Text(
text = placeholder.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.disabled,
modifier = Modifier,
)
}
textValue()
}
},
modifier = modifier
.focusRequester(focusRequester),
)
}

View file

@ -0,0 +1,152 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4)
*
* @param title title reference
* @param text primary text reference
* @param modifier modifier
* @param titleColor title color
* @param textColor text color
* @param iconRes action icon
* @param iconTint action icon tint
* @param onIconClick click on action icon
* @param showDivider show divider
* @see [InputRowEnter] for editable version
*/
@Composable
fun InputRowDefault(
title: TextReference,
text: TextReference,
modifier: Modifier = Modifier,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
iconRes: Int? = null,
iconTint: Color = TangemTheme.colors.icon.informative,
onIconClick: (() -> Unit)? = null,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Column(
modifier = Modifier
.weight(1f),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
Text(
text = text.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
iconRes?.let {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) { onIconClick?.invoke() },
)
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowDefaultPreview_Light(
@PreviewParameter(InputRowDefaultPreviewDataProvider::class) data: InputRowDefaultPreviewData,
) {
TangemTheme {
InputRowDefault(
title = TextReference.Str(data.title),
text = TextReference.Str(data.text),
iconRes = data.iconRes,
showDivider = data.showDivider,
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
@Preview
@Composable
private fun InputRowDefaultPreview_Dark(
@PreviewParameter(InputRowDefaultPreviewDataProvider::class) data: InputRowDefaultPreviewData,
) {
TangemTheme(isDark = true) {
InputRowDefault(
title = TextReference.Str(data.title),
text = TextReference.Str(data.text),
iconRes = data.iconRes,
showDivider = data.showDivider,
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
private data class InputRowDefaultPreviewData(
val title: String,
val text: String,
val iconRes: Int?,
val showDivider: Boolean,
)
private class InputRowDefaultPreviewDataProvider : PreviewParameterProvider<InputRowDefaultPreviewData> {
override val values: Sequence<InputRowDefaultPreviewData>
get() = sequenceOf(
InputRowDefaultPreviewData(
title = "title",
text = "text",
iconRes = null,
showDivider = true,
),
InputRowDefaultPreviewData(
title = "title",
text = "text",
iconRes = R.drawable.ic_chevron_right_24,
showDivider = false,
),
)
}
//endregion

View file

@ -0,0 +1,167 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* [InputRowEnter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4)
*
* @param title title reference
* @param text primary text reference
* @param onValueChange text change callback
* @param modifier modifier
* @param titleColor title color
* @param textColor text color
* @param isSingleLine text
* @param visualTransformation applied transformation to text
* @param keyboardOptions keyboard options for field
* @param iconRes action icon
* @param iconTint action icon tint
* @param onIconClick click on action icon
* @param showDivider show divider
* @see [InputRowDefault] for read only version
*/
@Composable
fun InputRowEnter(
title: TextReference,
text: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
isSingleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
iconRes: Int? = null,
iconTint: Color = TangemTheme.colors.icon.informative,
onIconClick: (() -> Unit)? = null,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
SimpleTextField(
value = text,
onValueChange = onValueChange,
color = textColor,
singleLine = isSingleLine,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
)
}
iconRes?.let {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) { onIconClick?.invoke() },
)
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowEnterPreview_Light(
@PreviewParameter(InputRowEnterPreviewDataProvider::class) data: InputRowEnterPreviewData,
) {
TangemTheme {
InputRowEnter(
title = TextReference.Str(data.title),
text = data.text,
iconRes = data.iconRes,
showDivider = data.showDivider,
onValueChange = {},
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
@Preview
@Composable
private fun InputRowEnterPreview_Dark(
@PreviewParameter(InputRowEnterPreviewDataProvider::class) data: InputRowEnterPreviewData,
) {
TangemTheme(isDark = true) {
InputRowEnter(
title = TextReference.Str(data.title),
text = data.text,
iconRes = data.iconRes,
showDivider = data.showDivider,
onValueChange = {},
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
private data class InputRowEnterPreviewData(
val title: String,
val text: String,
val iconRes: Int?,
val showDivider: Boolean,
)
private class InputRowEnterPreviewDataProvider :
PreviewParameterProvider<InputRowEnterPreviewData> {
override val values: Sequence<InputRowEnterPreviewData>
get() = sequenceOf(
InputRowEnterPreviewData(
title = "title",
text = "text",
iconRes = null,
showDivider = true,
),
InputRowEnterPreviewData(
title = "title",
text = "text",
iconRes = R.drawable.ic_chevron_right_24,
showDivider = false,
),
)
}
//endregion

View file

@ -0,0 +1,153 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
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.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* Input Row Enter with Info variation.
* [Input Row Enter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4)
* [Input Row Enter Info](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7854-33577&mode=design&t=6o23sqF8fDQdn4C5-4)
*
* @param title title reference
* @param text primary text reference
* @param onValueChange text change callback
* @param modifier modifier
* @param titleColor title color
* @param textColor text color
* @param isSingleLine text
* @param visualTransformation applied transformation to text
* @param keyboardOptions keyboard options for field
* @param showDivider show divider
*/
@Composable
fun InputRowEnterInfo(
title: TextReference,
text: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
info: TextReference? = null,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
infoColor: Color = TangemTheme.colors.text.tertiary,
isSingleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
Row {
SimpleTextField(
value = text,
onValueChange = onValueChange,
singleLine = isSingleLine,
color = textColor,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8)
.weight(1f),
)
info?.let {
Text(
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = infoColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.align(Alignment.Bottom),
)
}
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowEnterInfoPreview_Light(
@PreviewParameter(InputRowEnterInfoPreviewDataProvider::class) data: InputRowEnterInfoPreviewData,
) {
TangemTheme {
InputRowEnterInfo(
title = data.title,
text = data.text,
info = data.info,
showDivider = data.showDivider,
onValueChange = {},
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
@Preview
@Composable
private fun InputRowEnterInfoPreview_Dark(
@PreviewParameter(InputRowEnterInfoPreviewDataProvider::class) data: InputRowEnterInfoPreviewData,
) {
TangemTheme(isDark = true) {
InputRowEnterInfo(
title = data.title,
text = data.text,
info = data.info,
showDivider = data.showDivider,
onValueChange = {},
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
private data class InputRowEnterInfoPreviewData(
val title: TextReference,
val text: String,
val showDivider: Boolean,
val info: TextReference?,
)
private class InputRowEnterInfoPreviewDataProvider :
PreviewParameterProvider<InputRowEnterInfoPreviewData> {
override val values: Sequence<InputRowEnterInfoPreviewData>
get() = sequenceOf(
InputRowEnterInfoPreviewData(
title = TextReference.Str("title"),
text = "text",
showDivider = true,
info = TextReference.Str("info"),
),
InputRowEnterInfoPreviewData(
title = TextReference.Str("title"),
text = "text",
showDivider = false,
info = null,
),
)
}
//endregion

View file

@ -0,0 +1,196 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* [Input Row Image](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-813&mode=design&t=IQ5lBJEkFGU4WSvi-4)
*
* @param title title reference
* @param subtitle subtitle reference
* @param caption caption reference
* @param tokenIconState token icon state [TokenIconState]
* @param modifier modifier
* @param titleColor title color
* @param subtitleColor subtitle color
* @param captionColor caption color
* @param iconRes action icon
* @param iconTint action icon tint
* @param onIconClick click on action icon
* @param showNetworkIcon show token network icon
* @param showDivider show divider
*/
@Composable
fun InputRowImage(
title: TextReference,
subtitle: TextReference,
caption: TextReference,
tokenIconState: TokenIconState,
modifier: Modifier = Modifier,
titleColor: Color = TangemTheme.colors.text.secondary,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
iconRes: Int? = null,
iconTint: Color = TangemTheme.colors.icon.informative,
onIconClick: (() -> Unit)? = null,
showNetworkIcon: Boolean = false,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
Row(
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing6,
),
) {
TokenIcon(
state = tokenIconState,
shouldDisplayNetwork = showNetworkIcon,
modifier = Modifier
.size(TangemTheme.dimens.size36),
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = TangemTheme.dimens.spacing12),
) {
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = subtitleColor,
)
Text(
text = caption.resolveReference(),
style = TangemTheme.typography.caption2,
color = captionColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
}
iconRes?.let {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier
.align(CenterVertically)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) { onIconClick?.invoke() },
)
}
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowInputEnterInfoPreview_Light(
@PreviewParameter(InputRowImagePreviewDataProvider::class) data: InputRowImagePreviewData,
) {
TangemTheme {
InputRowImage(
title = data.title,
modifier = Modifier.background(TangemTheme.colors.background.action),
subtitle = data.subtitle,
caption = data.caption,
tokenIconState = data.iconState,
iconRes = data.actionIconRes,
onIconClick = {},
showNetworkIcon = false,
showDivider = data.showDivider,
)
}
}
@Preview
@Composable
private fun InputRowImagePreview_Dark(
@PreviewParameter(InputRowImagePreviewDataProvider::class) data: InputRowImagePreviewData,
) {
TangemTheme(isDark = true) {
InputRowImage(
title = data.title,
modifier = Modifier.background(TangemTheme.colors.background.action),
subtitle = data.subtitle,
caption = data.caption,
tokenIconState = data.iconState,
iconRes = data.actionIconRes,
onIconClick = {},
showNetworkIcon = false,
showDivider = data.showDivider,
)
}
}
private data class InputRowImagePreviewData(
val title: TextReference,
val subtitle: TextReference,
val caption: TextReference,
val iconState: TokenIconState,
val showDivider: Boolean,
val actionIconRes: Int?,
val showNetworkIcon: Boolean = false,
)
private class InputRowImagePreviewDataProvider :
PreviewParameterProvider<InputRowImagePreviewData> {
override val values: Sequence<InputRowImagePreviewData>
get() = sequenceOf(
InputRowImagePreviewData(
title = TextReference.Str("title"),
subtitle = TextReference.Str("subtitle"),
caption = TextReference.Str("caption"),
iconState = TokenIconState.Locked,
actionIconRes = null,
showDivider = false,
showNetworkIcon = false,
),
InputRowImagePreviewData(
title = TextReference.Str("title"),
subtitle = TextReference.Str("subtitle"),
caption = TextReference.Str("caption"),
iconState = TokenIconState.Locked,
actionIconRes = R.drawable.ic_chevron_right_24,
showDivider = true,
showNetworkIcon = true,
),
)
}
//endregion

View file

@ -0,0 +1,168 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.inputrow.inner.PasteButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode=design&t=IQ5lBJEkFGU4WSvi-4)
*
* @param title title reference
* @param value recipient address
* @param placeholder placeholder
* @param onValueChange callback for value change
* @param onPasteClick callback for paste
* @param modifier composable modifier
* @param singleLine is single line text
* @param error error text
* @param isError is error flag
* @param showDivider show divider
*
* @see InputRowRecipientDefault for readonly version
*/
@Composable
fun InputRowRecipient(
title: TextReference,
value: String,
placeholder: TextReference,
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
modifier: Modifier = Modifier,
singleLine: Boolean = false,
error: TextReference? = null,
isError: Boolean = false,
showDivider: Boolean = false,
) {
val (titleText, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
title to TangemTheme.colors.text.secondary
}
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = titleText.resolveReference(),
style = TangemTheme.typography.caption2,
color = color,
)
Row(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
) {
IdentIcon(
address = value,
modifier = Modifier
.align(CenterVertically)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.size(TangemTheme.dimens.size36)
.background(TangemTheme.colors.background.tertiary),
)
SimpleTextField(
value = value,
placeholder = placeholder,
onValueChange = onValueChange,
singleLine = singleLine,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12)
.weight(1f)
.align(CenterVertically),
)
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.padding(start = TangemTheme.dimens.spacing8),
)
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowRecipientPreview_Light(
@PreviewParameter(InputRowRecipientPreviewDataProvider::class) value: InputRowRecipientPreviewData,
) {
TangemTheme {
InputRowRecipient(
value = value.value,
title = TextReference.Res(R.string.send_recipient),
placeholder = TextReference.Res(R.string.send_optional_field),
error = TextReference.Str("Error"),
isError = value.isError,
showDivider = true,
onValueChange = {},
onPasteClick = {},
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}
@Preview
@Composable
private fun InputRowRecipientPreview_Dark(
@PreviewParameter(InputRowRecipientPreviewDataProvider::class) value: InputRowRecipientPreviewData,
) {
TangemTheme(isDark = true) {
InputRowRecipient(
value = value.value,
title = TextReference.Res(R.string.send_recipient),
placeholder = TextReference.Res(R.string.send_optional_field),
error = TextReference.Str("Error"),
isError = value.isError,
showDivider = true,
onValueChange = {},
onPasteClick = {},
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}
private data class InputRowRecipientPreviewData(
val value: String,
val isError: Boolean,
)
private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider<InputRowRecipientPreviewData> {
override val values: Sequence<InputRowRecipientPreviewData>
get() = sequenceOf(
InputRowRecipientPreviewData(
value = "",
isError = false,
),
InputRowRecipientPreviewData(
value = "0x391316d97a07027a0702c8A002c8A0C25d8470",
isError = false,
),
InputRowRecipientPreviewData(
value = "0x391316d97a07027a0702c8A002c8A0C25d8470",
isError = true,
),
)
}
//endregion

View file

@ -0,0 +1,106 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
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.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* Read only version of [InputRowRecipient].
* [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode
* =design&t=IQ5lBJEkFGU4WSvi-4)
*
* @param title title reference
* @param value recipient address
* @param modifier composable modifier
* @param titleColor title color
* @param showDivider show divider
* @see InputRowRecipient for editable version
*/
@Composable
fun InputRowRecipientDefault(
title: TextReference,
value: String,
modifier: Modifier = Modifier,
titleColor: Color = TangemTheme.colors.text.secondary,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
Row(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
) {
IdentIcon(
address = value,
modifier = Modifier
.align(Alignment.CenterVertically)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.size(TangemTheme.dimens.size36)
.background(TangemTheme.colors.background.tertiary),
)
Text(
text = value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12)
.weight(1f)
.align(Alignment.CenterVertically),
)
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowRecipientPreview_Light() {
TangemTheme {
InputRowRecipientDefault(
value = "0x391316d97a07027a0702c8A002c8A0C25d8470",
title = TextReference.Res(R.string.send_recipient),
showDivider = true,
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}
@Preview
@Composable
private fun InputRowRecipientPreview_Dark() {
TangemTheme(isDark = true) {
InputRowRecipientDefault(
value = "0x391316d97a07027a0702c8A002c8A0C25d8470",
title = TextReference.Res(R.string.send_recipient),
showDivider = false,
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}
//endregion

View file

@ -0,0 +1,31 @@
package com.tangem.core.ui.components.inputrow.inner
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@Composable
fun DividerContainer(
modifier: Modifier = Modifier,
showDivider: Boolean = false,
paddingValues: PaddingValues = PaddingValues(start = TangemTheme.dimens.spacing12),
content: @Composable () -> Unit,
) {
Box(modifier = modifier) {
content()
if (showDivider) {
Divider(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(paddingValues),
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
)
}
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.core.ui.components.inputrow.inner
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
/**
* Paste button with cross icon. Retrieves text from clipboard.
* [Paste button](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7853-33535&mode=design&t=6o23sqF8fDQdn4C5-4)
*
* @param isPasteButtonVisible is paste button visible
* @param onClick action callback
* @param modifier composable modifier
*/
@Composable
fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
if (isPasteButtonVisible) {
Box(modifier = modifier) {
Text(
text = "Paste",
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary2,
modifier = Modifier
.background(
color = TangemTheme.colors.button.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
horizontal = TangemTheme.dimens.spacing10,
vertical = TangemTheme.dimens.spacing2,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClick(
clipboardManager
.getText()
?.toString()
.orEmpty(),
)
},
),
)
}
} else {
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = stringResource(R.string.common_close),
modifier = modifier
.size(TangemTheme.dimens.size20)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
onClick = { onClick("") },
),
)
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="44dp"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:pathData="M6.31,26.499C6.508,25.666 6.773,24.813 7.102,23.949C8.493,26.484 10.368,29 12.684,31.316C15.001,33.632 17.516,35.507 20.051,36.898C19.187,37.227 18.334,37.492 17.501,37.69C13.387,38.669 10.043,37.991 8.026,35.974C6.009,33.957 5.331,30.613 6.31,26.499ZM6.862,37.139C3.507,33.784 3.448,27.963 6.13,22C3.448,16.037 3.507,10.216 6.862,6.862C10.216,3.507 16.037,3.448 22,6.13C27.963,3.448 33.784,3.507 37.139,6.862C40.493,10.216 40.552,16.037 37.87,22C40.553,27.963 40.493,33.784 37.139,37.139C33.784,40.493 27.963,40.552 22,37.87C16.037,40.552 10.216,40.493 6.862,37.139ZM13.849,30.152C11.273,27.576 9.292,24.765 7.948,22C9.292,19.235 11.273,16.424 13.849,13.849C16.424,11.273 19.235,9.292 22,7.948C24.765,9.292 27.576,11.273 30.152,13.849C32.728,16.424 34.708,19.235 36.052,22C34.708,24.765 32.727,27.576 30.152,30.152C27.576,32.727 24.765,34.708 22,36.052C19.235,34.708 16.425,32.727 13.849,30.152ZM12.684,12.684C15,10.368 17.515,8.493 20.051,7.102C19.187,6.773 18.334,6.508 17.502,6.31C13.388,5.331 10.044,6.009 8.026,8.026C6.009,10.043 5.331,13.387 6.31,17.501C6.509,18.334 6.773,19.187 7.102,20.051C8.493,17.515 10.368,15 12.684,12.684ZM31.316,12.684C29,10.368 26.485,8.493 23.949,7.102C24.813,6.773 25.666,6.508 26.499,6.31C30.613,5.331 33.957,6.009 35.974,8.026C37.991,10.043 38.669,13.387 37.69,17.501C37.492,18.334 37.227,19.187 36.898,20.051C35.507,17.515 33.632,15 31.316,12.684ZM31.316,31.316C33.632,29 35.507,26.484 36.898,23.949C37.227,24.813 37.492,25.666 37.69,26.499C38.67,30.613 37.992,33.957 35.974,35.974C33.957,37.991 30.613,38.669 26.499,37.69C25.666,37.492 24.813,37.227 23.949,36.898C26.485,35.507 29,33.632 31.316,31.316ZM22.539,19.035L21.919,14.838L21.299,19.035C21.112,20.303 20.116,21.299 18.848,21.486L14.65,22.106L18.848,22.726C20.116,22.914 21.112,23.91 21.299,25.178L21.919,29.375L22.539,25.178C22.726,23.91 23.722,22.914 24.991,22.726L29.188,22.106L24.991,21.486C23.722,21.299 22.726,20.303 22.539,19.035Z"
android:fillColor="#EBEBEB"
android:fillType="evenOdd"/>
</vector>

View file

@ -67,6 +67,14 @@ internal class DefaultQuotesRepository(
}
}
override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote {
return withContext(dispatchers.io) {
val quote = quotesStore.getSync(setOf(currencyId)).firstOrNull()
requireNotNull(quote) { "Unable to get quote for $currencyId" }
quotesConverter.convert(quote)
}
}
private suspend fun fetchExpiredQuotes(
currenciesIds: Set<CryptoCurrency.ID>,
appCurrencyId: String,

View file

@ -0,0 +1,36 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
class GetCryptoCurrencyStatusSyncUseCase(
internal val currenciesRepository: CurrenciesRepository,
internal val quotesRepository: QuotesRepository,
internal val networksRepository: NetworksRepository,
internal val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Either<TokenListError, CryptoCurrencyStatus> {
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
)
return operations.getCurrencyStatusSync(cryptoCurrencyId)
.mapLeft { error -> error.mapToTokenListError() }
}
}

View file

@ -85,6 +85,28 @@ internal class CurrenciesStatusesOperations(
}
}
suspend fun getCurrencyStatusSync(cryptoCurrencyId: CryptoCurrency.ID): Either<Error, CryptoCurrencyStatus> {
return either {
catch(
block = {
val currency =
currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId)
val quotes = quotesRepository.getQuoteSync(cryptoCurrencyId).right()
val networkStatuses =
networksRepository.getNetworkStatusesSync(
userWalletId,
setOf(currency.network),
false,
).firstOrNull {
it.network == currency.network
}.right()
return createCurrencyStatus(currency, quotes, networkStatuses)
},
catch = { raise(Error.DataError(it)) },
)
}
}
fun getCardCurrenciesStatusesFlow(): Flow<Either<Error, List<CryptoCurrencyStatus>>> {
return flow {
val nonEmptyCurrencies = recover(

View file

@ -29,4 +29,6 @@ interface QuotesRepository {
* @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies.
*/
suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote>
suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote
}

View file

@ -1,12 +1,9 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
interface BlockchainInteractor {
fun getTokenDecimals(token: CryptoCurrency): Int
/**
* In app blockchain id, actual in blockchain sdk, not the same as networkId
*

View file

@ -1,6 +1,5 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
import com.tangem.lib.crypto.TransactionManager
import javax.inject.Inject
@ -22,12 +21,4 @@ internal class DefaultBlockchainInteractor @Inject constructor(
override fun getExplorerTransactionLink(networkId: String, txAddress: String): String {
return transactionManager.getExplorerTransactionLink(networkId, txAddress)
}
override fun getTokenDecimals(token: CryptoCurrency): Int {
return if (token is CryptoCurrency.Token) {
token.decimals
} else {
transactionManager.getNativeTokenDecimals(token.network.backendId)
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.ui.*
@ -111,4 +112,6 @@ interface SwapInteractor {
networkId: String,
fromToken: CryptoCurrency,
): Boolean
fun getSelectedWallet(): UserWallet?
}

View file

@ -8,6 +8,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.swap.domain.cache.SwapDataCache
import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter
@ -97,6 +98,10 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
override fun getSelectedWallet(): UserWallet? {
return getSelectedWalletSyncUseCase().getOrNull()
}
private fun getToCurrenciesGroup(
currency: CryptoCurrency,
leastPairs: List<SwapPairLeast>,

View file

@ -66,13 +66,6 @@ class SwapDomainModule {
return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder)
}
@SwapScope
@Provides
@Singleton
fun providesGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
}
@SwapScope
@Provides
@Singleton

View file

@ -21,6 +21,10 @@ dependencies {
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
/** AndroidX */
implementation(deps.androidx.activity.compose)

View file

@ -32,13 +32,17 @@ class TokensDataConverter(
availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) }
.toMutableList()
.apply {
this.add(0, availableTitle)
if (this.isNotEmpty()) {
this.add(0, availableTitle)
}
}
.toImmutableList(),
unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) }
.toMutableList()
.apply {
this.add(0, unavailableTitle)
if (this.isNotEmpty()) {
this.add(0, unavailableTitle)
}
}
.toImmutableList(),
onSearchEntered = onSearchEntered,

View file

@ -0,0 +1,34 @@
package com.tangem.feature.swap.di
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.feature.swap.domain.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
class SwapPresentationModule {
@ViewModelScoped
@Provides
fun providesGetCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository,
dispatcherProvider: CoroutineDispatcherProvider,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
): GetCryptoCurrencyStatusSyncUseCase {
return GetCryptoCurrencyStatusSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
dispatchers = dispatcherProvider,
)
}
}

View file

@ -7,8 +7,8 @@ import com.tangem.core.ui.components.states.SelectableItemsState
import com.tangem.feature.swap.domain.models.ui.TxFee
data class SwapStateHolder(
val sendCardData: SwapCardData,
val receiveCardData: SwapCardData,
val sendCardData: SwapCardState,
val receiveCardData: SwapCardState,
val networkCurrency: String,
val networkId: String,
val blockchainId: String, // not the same as networkId, its local id in app
@ -33,18 +33,28 @@ data class SwapStateHolder(
val onCancelPermissionBottomSheet: () -> Unit = {},
)
data class SwapCardData(
val type: TransactionCardType,
val amountEquivalent: String?,
val coinId: String?,
val amountTextFieldValue: TextFieldValue?,
val tokenIconUrl: String?,
val tokenCurrency: String,
val balance: String,
val isBalanceHidden: Boolean,
val isNotNativeToken: Boolean,
val canSelectAnotherToken: Boolean = false,
)
sealed class SwapCardState {
data class SwapCardData(
val type: TransactionCardType,
val amountEquivalent: String?,
val coinId: String?,
val amountTextFieldValue: TextFieldValue?,
val tokenIconUrl: String?,
val tokenCurrency: String,
val balance: String,
val isBalanceHidden: Boolean,
val isNotNativeToken: Boolean,
val canSelectAnotherToken: Boolean = false,
) : SwapCardState()
data class Empty(
val type: TransactionCardType,
val amountEquivalent: String?,
val amountTextFieldValue: TextFieldValue?,
val canSelectAnotherToken: Boolean = false,
) : SwapCardState()
}
data class SwapButton(
val enabled: Boolean,

View file

@ -8,9 +8,12 @@ import com.tangem.core.ui.components.states.Item
import com.tangem.core.ui.components.states.SelectableItemsState
import com.tangem.core.ui.extensions.TextReference
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.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
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.domain.NetworkInfo
@ -28,7 +31,7 @@ import kotlinx.collections.immutable.toImmutableList
internal class StateBuilder(
private val actions: UiActions,
private val isBalanceHiddenProvider: Provider<Boolean>,
appCurrencyProvider: Provider<AppCurrency>,
private val appCurrencyProvider: Provider<AppCurrency>,
) {
private val tokensDataConverter = TokensDataConverter(
@ -42,7 +45,7 @@ internal class StateBuilder(
return SwapStateHolder(
networkId = initialCurrency.network.backendId,
blockchainId = networkInfo.blockchainId,
sendCardData = SwapCardData(
sendCardData = SwapCardState.SwapCardData(
type = TransactionCardType.SendCard(actions.onAmountChanged, actions.onAmountSelected),
amountEquivalent = null,
amountTextFieldValue = null,
@ -54,7 +57,7 @@ internal class StateBuilder(
balance = "",
isBalanceHidden = true,
),
receiveCardData = SwapCardData(
receiveCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReceiveCard(),
amountEquivalent = null,
tokenIconUrl = "",
@ -79,6 +82,53 @@ internal class StateBuilder(
)
}
fun createNoAvailableTokensToSwapState(
uiStateHolder: SwapStateHolder,
fromToken: CryptoCurrencyStatus,
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
return uiStateHolder.copy(
sendCardData = SwapCardState.SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amountTextFieldValue = TextFieldValue(
text = "0",
),
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
coinId = uiStateHolder.sendCardData.coinId,
isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken,
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
balance = fromToken.getFormattedAmount(),
isBalanceHidden = isBalanceHiddenProvider(),
),
receiveCardData = SwapCardState.Empty(
type = TransactionCardType.ReceiveCard(),
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
amountTextFieldValue = TextFieldValue(
text = "0",
),
canSelectAnotherToken = true,
),
warnings = listOf(
SwapWarning.NoAvailableTokensToSwap(
notificationConfig = NotificationConfig(
title = stringReference("No tokens"),
subtitle = stringReference("Swap tokens not available"),
iconResId = R.drawable.ic_alert_24,
),
),
),
fee = FeeState.Empty,
swapButton = SwapButton(
enabled = false,
loading = false,
onClick = { },
),
updateInProgress = false,
)
}
fun createQuotesLoadingState(
uiStateHolder: SwapStateHolder,
fromToken: CryptoCurrency,
@ -87,8 +137,10 @@ internal class StateBuilder(
): SwapStateHolder {
val canSelectSendToken = mainTokenId != fromToken.id.value // TODO look at id matching
val canSelectReceiveToken = mainTokenId != toToken.id.value // TODO look at id matching
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
return uiStateHolder.copy(
sendCardData = SwapCardData(
sendCardData = SwapCardState.SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
amountEquivalent = null,
@ -100,7 +152,7 @@ internal class StateBuilder(
balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "",
isBalanceHidden = isBalanceHiddenProvider(),
),
receiveCardData = SwapCardData(
receiveCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReceiveCard(),
amountTextFieldValue = null,
amountEquivalent = null,
@ -128,12 +180,15 @@ internal class StateBuilder(
* @param onFeeSetup callback for reset fee after auto update
* @return updated whole screen state
*/
@Suppress("LongMethod")
fun createQuotesLoadedState(
uiStateHolder: SwapStateHolder,
quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency,
onFeeSetup: (TxFee) -> Unit,
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
val warnings = mutableListOf<SwapWarning>()
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
quoteModel.preparedSwapConfigState.isFeeEnough &&
@ -159,7 +214,7 @@ internal class StateBuilder(
}
val feeState = createFeeState(quoteModel, uiStateHolder, onFeeSetup)
return uiStateHolder.copy(
sendCardData = SwapCardData(
sendCardData = SwapCardState.SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance,
@ -171,7 +226,7 @@ internal class StateBuilder(
balance = quoteModel.fromTokenInfo.tokenWalletBalance,
isBalanceHidden = isBalanceHiddenProvider(),
),
receiveCardData = SwapCardData(
receiveCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReceiveCard(),
amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()),
amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance,
@ -208,8 +263,10 @@ internal class StateBuilder(
uiStateHolder: SwapStateHolder,
emptyAmountState: SwapState.EmptyAmountState,
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
return uiStateHolder.copy(
sendCardData = SwapCardData(
sendCardData = SwapCardState.SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
amountEquivalent = emptyAmountState.zeroAmountEquivalent,
@ -221,7 +278,7 @@ internal class StateBuilder(
balance = emptyAmountState.fromTokenWalletBalance,
isBalanceHidden = isBalanceHiddenProvider(),
),
receiveCardData = SwapCardData(
receiveCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReceiveCard(),
amountTextFieldValue = TextFieldValue("0"),
amountEquivalent = emptyAmountState.zeroAmountEquivalent,
@ -268,6 +325,7 @@ internal class StateBuilder(
}
fun updateSwapAmount(uiState: SwapStateHolder, amount: String): SwapStateHolder {
if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState
return uiState.copy(
sendCardData = uiState.sendCardData.copy(
amountTextFieldValue = TextFieldValue(
@ -279,6 +337,8 @@ internal class StateBuilder(
}
fun updateBalanceHiddenState(uiState: SwapStateHolder, isBalanceHidden: Boolean): SwapStateHolder {
if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState
if (uiState.receiveCardData !is SwapCardState.SwapCardData) return uiState
val patchedSendCardData = uiState.sendCardData.copy(
isBalanceHidden = isBalanceHidden,
)
@ -666,11 +726,26 @@ internal class StateBuilder(
return "$firstAddressPart...$secondAddressPart"
}
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN
return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals)
}
@Suppress("UnusedPrivateMember")
private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String {
val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN
val appCurrency = appCurrencyProvider()
return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol)
}
private companion object {
const val ADDRESS_MIN_LENGTH = 11
const val ADDRESS_FIRST_PART_LENGTH = 7
const val ADDRESS_SECOND_PART_LENGTH = 4
private const val PRICE_IMPACT_THRESHOLD = 0.1
private const val HUNDRED_PERCENTS = 100
private const val UNKNOWN_AMOUNT_SIGN = ""
}
}

View file

@ -145,48 +145,24 @@ private fun MainInfo(state: SwapStateHolder) {
}
val (topCard, bottomCard, button) = createRefs()
val priceImpactWarning = state.warnings.filterIsInstance<SwapWarning.HighPriceImpact>().firstOrNull()
TransactionCard(
type = state.sendCardData.type,
balance = if (state.sendCardData.isBalanceHidden) {
STARS
} else {
state.sendCardData.balance
},
textFieldValue = state.sendCardData.amountTextFieldValue,
amountEquivalent = state.sendCardData.amountEquivalent,
tokenIconUrl = state.sendCardData.tokenIconUrl ?: "",
tokenCurrency = state.sendCardData.tokenCurrency,
priceImpact = priceImpactWarning,
networkIconRes = if (state.sendCardData.isNotNativeToken) networkIconRes else null,
iconPlaceholder = state.sendCardData.coinId?.let {
getActiveIconResByCoinId(it)
},
onChangeTokenClick = if (state.sendCardData.canSelectAnotherToken) state.onSelectTokenClick else null,
TransactionCardData(
priceImpactWarning = priceImpactWarning,
networkIconRes = networkIconRes,
swapCardState = state.sendCardData,
modifier = Modifier.constrainAs(topCard) {
top.linkTo(parent.top)
},
onSelectTokenClick = state.onSelectTokenClick,
)
val marginCard = TangemTheme.dimens.spacing16
TransactionCard(
type = state.receiveCardData.type,
balance = if (state.receiveCardData.isBalanceHidden) STARS else state.receiveCardData.balance,
textFieldValue = state.receiveCardData.amountTextFieldValue,
amountEquivalent = state.receiveCardData.amountEquivalent,
tokenIconUrl = state.receiveCardData.tokenIconUrl ?: "",
tokenCurrency = state.receiveCardData.tokenCurrency,
priceImpact = priceImpactWarning,
networkIconRes = if (state.receiveCardData.isNotNativeToken) networkIconRes else null,
iconPlaceholder = state.receiveCardData.coinId?.let {
getActiveIconResByCoinId(it)
},
onChangeTokenClick = if (state.receiveCardData.canSelectAnotherToken) {
state.onSelectTokenClick
} else {
null
},
TransactionCardData(
priceImpactWarning = priceImpactWarning,
networkIconRes = networkIconRes,
swapCardState = state.receiveCardData,
modifier = Modifier.constrainAs(bottomCard) {
top.linkTo(topCard.bottom, margin = marginCard)
},
onSelectTokenClick = state.onSelectTokenClick,
)
val marginButton = TangemTheme.dimens.spacing32
SwapButton(
@ -200,6 +176,48 @@ private fun MainInfo(state: SwapStateHolder) {
}
}
@Composable
private fun TransactionCardData(
priceImpactWarning: SwapWarning.HighPriceImpact?,
networkIconRes: Int?,
swapCardState: SwapCardState,
onSelectTokenClick: (() -> Unit)?,
modifier: Modifier = Modifier,
) {
when (swapCardState) {
is SwapCardState.Empty -> {
TransactionCardEmpty(
type = swapCardState.type,
amountEquivalent = swapCardState.amountEquivalent,
textFieldValue = swapCardState.amountTextFieldValue,
onChangeTokenClick = if (swapCardState.canSelectAnotherToken) onSelectTokenClick else null,
modifier = modifier,
)
}
is SwapCardState.SwapCardData -> {
TransactionCard(
type = swapCardState.type,
balance = if (swapCardState.isBalanceHidden) {
STARS
} else {
swapCardState.balance
},
textFieldValue = swapCardState.amountTextFieldValue,
amountEquivalent = swapCardState.amountEquivalent,
tokenIconUrl = swapCardState.tokenIconUrl ?: "",
tokenCurrency = swapCardState.tokenCurrency,
priceImpact = priceImpactWarning,
networkIconRes = if (swapCardState.isNotNativeToken) networkIconRes else null,
iconPlaceholder = swapCardState.coinId?.let {
getActiveIconResByCoinId(it)
},
onChangeTokenClick = if (swapCardState.canSelectAnotherToken) onSelectTokenClick else null,
modifier = modifier,
)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) {
@ -268,7 +286,8 @@ private fun FeeItem(feeState: FeeState, currency: String) {
}
}
is FeeState.Empty -> {
SmallInfoCard(startText = titleString, endText = "")
// show nothing
// SmallInfoCard(startText = titleString, endText = "")
}
}
}
@ -355,7 +374,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U
// region preview
private val sendCard = SwapCardData(
private val sendCard = SwapCardState.SwapCardData(
type = TransactionCardType.SendCard({}) {},
amountTextFieldValue = TextFieldValue(),
amountEquivalent = "1 000 000",
@ -368,7 +387,7 @@ private val sendCard = SwapCardData(
isBalanceHidden = false,
)
private val receiveCard = SwapCardData(
private val receiveCard = SwapCardState.SwapCardData(
type = TransactionCardType.ReceiveCard(),
amountTextFieldValue = TextFieldValue(),
amountEquivalent = "1 000 000",

View file

@ -68,7 +68,7 @@ fun TransactionCard(
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start,
) {
Header(balance = balance, type = type)
Header(balance = stringResource(R.string.common_balance, balance), type = type)
Content(
type = type,
@ -106,6 +106,67 @@ fun TransactionCard(
}
}
@Composable
fun TransactionCardEmpty(
type: TransactionCardType,
amountEquivalent: String?,
textFieldValue: TextFieldValue?,
modifier: Modifier = Modifier,
onChangeTokenClick: (() -> Unit)? = null,
) {
Card(
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
backgroundColor = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
modifier = modifier,
) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start,
) {
Header(
balance = stringResource(id = R.string.swapping_token_not_available),
type = type,
)
Content(
type = type,
amountEquivalent = amountEquivalent,
textFieldValue = textFieldValue,
priceImpact = null,
)
}
Box(modifier = Modifier.align(Alignment.BottomEnd)) {
Token(
tokenIconUrl = "",
tokenCurrency = "",
iconPlaceholder = R.drawable.ic_no_token_44,
)
}
if (onChangeTokenClick != null) {
Box(modifier = Modifier.align(Alignment.CenterEnd)) {
ChangeTokenSelector()
}
Box(
Modifier
.align(Alignment.CenterEnd)
.height(TangemTheme.dimens.size116)
.width(TangemTheme.dimens.size102)
.clickable(
indication = rememberRipple(bounded = false),
interactionSource = remember { MutableInteractionSource() },
) { onChangeTokenClick() },
)
}
}
}
}
@Composable
private fun Header(type: TransactionCardType, balance: String, modifier: Modifier = Modifier) {
Row(
@ -134,7 +195,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
SpacerW16()
if (balance.isNotBlank()) {
Text(
text = stringResource(R.string.common_balance, balance),
text = balance,
color = TangemTheme.colors.text.tertiary,
style = MaterialTheme.typography.body2,
modifier = Modifier

View file

@ -1,6 +1,6 @@
package com.tangem.feature.swap.viewmodels
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapStateData
@ -14,8 +14,8 @@ data class SwapProcessDataState(
val fromCurrency: Currency? = null,
@Deprecated("used in old swap mechanism")
val toCurrency: Currency? = null,
val fromCryptoCurrency: CryptoCurrency? = null,
val toCryptoCurrency: CryptoCurrency? = null,
val fromCryptoCurrency: CryptoCurrencyStatus? = null,
val toCryptoCurrency: CryptoCurrencyStatus? = null,
// Amount from input
val amount: String? = null,
val approveDataModel: RequestApproveStateData? = null,

View file

@ -11,7 +11,9 @@ import com.tangem.core.ui.utils.InputNumberFormatter
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
@ -51,11 +53,13 @@ internal class SwapViewModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver {
private val initialCryptoCurrency: CryptoCurrency = savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY]
?: error("no expected parameter CryptoCurrency found`")
private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus
private var isBalanceHidden = true
@ -90,11 +94,20 @@ internal class SwapViewModel @Inject constructor(
get() = swapRouter.currentScreen
init {
swapInteractor.initDerivationPathAndNetwork(
derivationPath = initialCryptoCurrency.network.derivationPath.value,
network = initialCryptoCurrency.network,
)
initTokens()
viewModelScope.launch(dispatchers.io) {
swapInteractor.getSelectedWallet()?.let {
initialCryptoCurrencyStatus =
requireNotNull(getCryptoCurrencyStatusUseCase(it.walletId, initialCryptoCurrency.id).getOrNull()) {
"Failed to get initial crypto currency status"
}
swapInteractor.initDerivationPathAndNetwork(
derivationPath = initialCryptoCurrency.network.derivationPath.value,
network = initialCryptoCurrency.network,
)
initTokens()
}
}
}
override fun onCreate(owner: LifecycleOwner) {
@ -139,47 +152,38 @@ internal class SwapViewModel @Inject constructor(
runCatching(dispatchers.io) {
swapInteractor.getTokensDataState(initialCryptoCurrency)
}.onSuccess { state ->
dataState = dataState.copy(
fromCryptoCurrency = initialCryptoCurrency,
toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency,
tokensDataState = state,
)
// updateTokensState(state)
val toToken = state.toGroup.available.first()
startLoadingQuotes(
fromToken = initialCryptoCurrency,
toToken = state.toGroup.available.first().currencyStatus.currency,
amount = lastAmount.value,
toProvidersList = toToken.providers,
)
updateTokensState(state)
applyInitialTokenChoice(state, selectInitialCurrencyToSwap(state))
}.onFailure {
Timber.tag(loggingTag).e(it)
}
}
}
// old flow
// viewModelScope.launch(dispatchers.main) {
// runCatching(dispatchers.io) {
// swapInteractor.initTokensToSwap(currency)
// }
// .onSuccess { state ->
// // dataState = dataState.copy(
// // fromCurrency = state.preselectTokens.fromToken,
// // toCurrency = state.preselectTokens.toToken,
// // )
// // updateTokensState(dataState = state.foundTokensState)
// // startLoadingQuotes(
// // fromToken = state.preselectTokens.fromToken,
// // toToken = state.preselectTokens.toToken,
// // amount = lastAmount.value,
// // )
// }
// .onFailure {
// Timber.tag(loggingTag).e(it)
// }
// }
private fun applyInitialTokenChoice(state: TokensDataStateExpress, selectedCurrency: CryptoCurrencyStatus?) {
val fromCurrencyStatus = initialCryptoCurrencyStatus
dataState = dataState.copy(
fromCryptoCurrency = fromCurrencyStatus,
toCryptoCurrency = selectedCurrency,
tokensDataState = state,
)
if (selectedCurrency == null) {
uiState = stateBuilder.createNoAvailableTokensToSwapState(
uiStateHolder = uiState,
fromToken = fromCurrencyStatus,
)
} else {
startLoadingQuotes(
fromToken = fromCurrencyStatus.currency,
toToken = selectedCurrency.currency,
amount = lastAmount.value,
)
}
}
private fun selectInitialCurrencyToSwap(state: TokensDataStateExpress): CryptoCurrencyStatus? {
// todo add algorithm to select initial currency
return state.toGroup.available.firstOrNull()?.currencyStatus
}
private fun updateTokensState(dataState: TokensDataStateExpress) {
@ -214,7 +218,7 @@ internal class SwapViewModel @Inject constructor(
val toCurrency = dataState.toCryptoCurrency
val amount = dataState.amount
if (fromCurrency != null && toCurrency != null && amount != null) {
startLoadingQuotes(fromCurrency, toCurrency, amount)
startLoadingQuotes(fromCurrency.currency, toCurrency.currency, amount)
}
}
@ -298,8 +302,8 @@ internal class SwapViewModel @Inject constructor(
swapInteractor.onSwap(
networkId = dataState.networkId,
swapStateData = requireNotNull(dataState.swapDataModel),
currencyToSend = requireNotNull(dataState.fromCryptoCurrency),
currencyToGet = requireNotNull(dataState.toCryptoCurrency),
currencyToSend = requireNotNull(dataState.fromCryptoCurrency?.currency),
currencyToGet = requireNotNull(dataState.toCryptoCurrency?.currency),
amountToSwap = requireNotNull(dataState.amount),
fee = requireNotNull(dataState.selectedFee),
)
@ -348,10 +352,10 @@ internal class SwapViewModel @Inject constructor(
approveData = requireNotNull(dataState.approveDataModel) {
"dataState.approveDataModel might not be null"
},
forTokenContractAddress = (dataState.fromCryptoCurrency as? CryptoCurrency.Token)
forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token)
?.contractAddress
?: "",
fromToken = requireNotNull(dataState.fromCryptoCurrency) {
fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) {
"dataState.fromCurrency might not be null"
},
approveType = requireNotNull(uiState.permissionState as? SwapPermissionState.ReadyForRequest) {
@ -397,28 +401,34 @@ internal class SwapViewModel @Inject constructor(
private fun onTokenSelect(id: String) {
val tokens = dataState.tokensDataState ?: return
val foundToken = tokens.toGroup.available.firstOrNull {
it.currencyStatus.currency.id.value == id
val foundToken = if (isOrderReversed) {
tokens.fromGroup.available.firstOrNull {
it.currencyStatus.currency.id.value == id
}
} else {
tokens.toGroup.available.firstOrNull {
it.currencyStatus.currency.id.value == id
}
}
analyticsEventHandler.send(
event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.currencyStatus?.currency?.symbol),
)
if (foundToken != null) {
val fromToken: CryptoCurrency
val toToken: CryptoCurrency
val fromToken: CryptoCurrencyStatus
val toToken: CryptoCurrencyStatus
if (isOrderReversed) {
fromToken = foundToken.currencyStatus.currency
toToken = initialCryptoCurrency
fromToken = foundToken.currencyStatus
toToken = initialCryptoCurrencyStatus
} else {
fromToken = initialCryptoCurrency
toToken = foundToken.currencyStatus.currency
fromToken = initialCryptoCurrencyStatus
toToken = foundToken.currencyStatus
}
dataState = dataState.copy(
fromCryptoCurrency = fromToken,
toCryptoCurrency = toToken,
)
startLoadingQuotes(fromToken, toToken, lastAmount.value)
startLoadingQuotes(fromToken.currency, toToken.currency, lastAmount.value)
swapRouter.openScreen(SwapNavScreen.Main)
}
}
@ -432,13 +442,13 @@ internal class SwapViewModel @Inject constructor(
toCryptoCurrency = newToToken,
)
isOrderReversed = !isOrderReversed
val decimals = blockchainInteractor.getTokenDecimals(newFromToken)
val decimals = newFromToken.currency.decimals
lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value)
uiState = stateBuilder.updateSwapAmount(
uiState,
inputNumberFormatter.formatWithThousands(lastAmount.value, decimals),
)
startLoadingQuotes(newFromToken, newToToken, lastAmount.value)
startLoadingQuotes(newFromToken.currency, newToToken.currency, lastAmount.value)
}
}
@ -446,20 +456,20 @@ internal class SwapViewModel @Inject constructor(
val fromToken = dataState.fromCryptoCurrency
val toToken = dataState.toCryptoCurrency
if (fromToken != null && toToken != null) {
val decimals = blockchainInteractor.getTokenDecimals(fromToken)
val decimals = fromToken.currency.decimals
val cutValue = cutAmountWithDecimals(decimals, value)
lastAmount.value = cutValue
uiState =
stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals))
amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) {
startLoadingQuotes(fromToken, toToken, lastAmount.value)
startLoadingQuotes(fromToken.currency, toToken.currency, lastAmount.value)
}
}
}
private fun onMaxAmountClicked() {
dataState.fromCryptoCurrency?.let {
val balance = swapInteractor.getTokenBalance(initialCryptoCurrency.network.id.value, it)
val balance = swapInteractor.getTokenBalance(initialCryptoCurrency.network.id.value, it.currency)
onAmountChanged(balance.formatToUIRepresentation())
}
}
@ -535,7 +545,7 @@ internal class SwapViewModel @Inject constructor(
dataState = dataState.copy(selectedFee = feeItem.data)
val spendAmount = dataState.amount?.let { amount ->
val fromToken = dataState.fromCryptoCurrency ?: return@let null
swapInteractor.getSwapAmountForToken(amount, fromToken)
swapInteractor.getSwapAmountForToken(amount, fromToken.currency)
} ?: dataState.approveDataModel?.fromTokenAmount
spendAmount ?: return@UiActions
val fromToken = dataState.fromCryptoCurrency ?: return@UiActions
@ -544,7 +554,7 @@ internal class SwapViewModel @Inject constructor(
fee = feeItem.data.feeValue,
spendAmount = spendAmount,
networkId = dataState.networkId,
fromToken = fromToken,
fromToken = fromToken.currency,
)
uiState = stateBuilder.updateFeeSelectedItem(uiState, feeItem, isFeeEnough)
}