From 872d52b0dda1ce19c9e547fc7dd05d08b9a9c09a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Nov 2023 13:13:57 +0300 Subject: [PATCH 1/3] Updated on 2026-08-14 --- .../ui/components/fields/SimpleTextField.kt | 60 ++++++ .../ui/components/inputrow/InputRowDefault.kt | 152 ++++++++++++++ .../ui/components/inputrow/InputRowEnter.kt | 167 +++++++++++++++ .../components/inputrow/InputRowEnterInfo.kt | 153 ++++++++++++++ .../ui/components/inputrow/InputRowImage.kt | 196 ++++++++++++++++++ .../components/inputrow/InputRowRecipient.kt | 168 +++++++++++++++ .../inputrow/InputRowRecipientDefault.kt | 106 ++++++++++ .../inputrow/inner/DividerContainer.kt | 31 +++ .../components/inputrow/inner/PasteButton.kt | 80 +++++++ 9 files changed, 1113 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt new file mode 100644 index 0000000000..7e0e1643c6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -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), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt new file mode 100644 index 0000000000..e41ac02d17 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -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 { + override val values: Sequence + 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 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt new file mode 100644 index 0000000000..f81372c3da --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt @@ -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 { + override val values: Sequence + 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 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt new file mode 100644 index 0000000000..25debb16dc --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt @@ -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 { + override val values: Sequence + 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 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt new file mode 100644 index 0000000000..1754c3b8bc --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt @@ -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 { + override val values: Sequence + 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 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt new file mode 100644 index 0000000000..312b0c0db1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -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 { + override val values: Sequence + get() = sequenceOf( + InputRowRecipientPreviewData( + value = "", + isError = false, + ), + InputRowRecipientPreviewData( + value = "0x391316d97a07027a0702c8A002c8A0C25d8470", + isError = false, + ), + InputRowRecipientPreviewData( + value = "0x391316d97a07027a0702c8A002c8A0C25d8470", + isError = true, + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt new file mode 100644 index 0000000000..8ae6b1b386 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt @@ -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 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt new file mode 100644 index 0000000000..a5510a378d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt new file mode 100644 index 0000000000..3e4642c061 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -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("") }, + ), + ) + } +} \ No newline at end of file From 3c5c53ce92e41ddff8945b27f05411967eb404ad Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 12:42:58 +0300 Subject: [PATCH 2/3] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 4 +- ...> GetCryptoCurrencyStatusesSyncUseCase.kt} | 29 +---- .../CurrenciesStatusesOperations.kt | 18 +++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 6 +- .../swap/converters/SwapPairInfoConverter.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 27 ++--- .../feature/swap/domain/SwapRepository.kt | 3 +- .../swap/domain/di/SwapDomainModule.kt | 8 +- .../domain/models/domain/ExchangeQuote.kt | 2 +- .../domain/models/domain/SwapPairLeast.kt | 2 +- features/swap/presentation/build.gradle.kts | 3 + .../swap/converters/TokensDataConverter.kt | 104 ++++++++++++++---- .../swap/models/SwapSelectTokenStateHolder.kt | 10 +- .../tangem/feature/swap/ui/StateBuilder.kt | 19 ++-- .../feature/swap/ui/SwapSelectTokenScreen.kt | 32 +++--- .../swap/viewmodels/SwapProcessDataState.kt | 2 + .../feature/swap/viewmodels/SwapViewModel.kt | 51 ++++++--- 17 files changed, 193 insertions(+), 128 deletions(-) rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{GetCryptoCurrencyStatusUseCase.kt => GetCryptoCurrencyStatusesSyncUseCase.kt} (51%) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 66027e8a6b..80f84ebf33 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -36,8 +36,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json @Suppress("LargeClass") class TradeCryptoMiddleware { @@ -259,7 +257,7 @@ class TradeCryptoMiddleware { private fun openSwap(currency: CryptoCurrency) { val bundle = bundleOf( - SwapFragment.CURRENCY_BUNDLE_KEY to currency + SwapFragment.CURRENCY_BUNDLE_KEY to currency, ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt similarity index 51% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt index d917a747c7..cdf58b3080 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt @@ -4,24 +4,21 @@ 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.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.operations.TokenListOperations 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 -import kotlinx.coroutines.flow.* -class GetCryptoCurrencyStatusUseCase( +class GetCryptoCurrencyStatusesSyncUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { - operator fun invoke(userWalletId: UserWalletId): Flow>> { + suspend operator fun invoke(userWalletId: UserWalletId): Either> { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, currenciesRepository = currenciesRepository, @@ -29,25 +26,7 @@ class GetCryptoCurrencyStatusUseCase( networksRepository = networksRepository, ) - return operations.getCurrenciesStatusesFlow() - .map { maybeCurrenciesStatuses -> - maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) - } - } - - @Suppress("UnusedPrivateMember") - private fun createTokenList( - userWalletId: UserWalletId, - tokens: List, - ): Flow> { - val operations = TokenListOperations( - userWalletId = userWalletId, - tokens = tokens, - currenciesRepository = currenciesRepository, - ) - - return operations.getTokenListFlow().map { maybeTokenList -> - maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) - } + return operations.getCurrenciesStatusesSync() + .mapLeft { error -> error.mapToTokenListError() } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 9bce6f0d35..3ce6112296 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,6 +67,24 @@ internal class CurrenciesStatusesOperations( } } + suspend fun getCurrenciesStatusesSync(): Either> { + return either { + catch( + block = { + val nonEmptyCurrencies = + currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull() + ?: return emptyList().right() + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() + val networkStatuses = + networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() + return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses) + }, + catch = { raise(Error.DataError(it)) }, + ) + } + } + fun getCardCurrenciesStatusesFlow(): Flow>> { return flow { val nonEmptyCurrencies = recover( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 9eb5bedf15..9aa4fc368b 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -265,7 +265,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork: String, fromAmount: BigDecimal, providerId: Int, - rateType: RateType + rateType: RateType, ): ExchangeQuote { val response = tangemExpressApi.getExchangeQuote( fromContractAddress, @@ -274,12 +274,12 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork, fromAmount, providerId, - rateTypeConverter.convertBack(rateType) + rateTypeConverter.convertBack(rateType), ).getOrThrow() return ExchangeQuote( toAmount = response.toAmount, - allowanceContract = response.allowanceContract + allowanceContract = response.allowanceContract, ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index e1e48fc630..c205bec15e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -33,5 +33,4 @@ class SwapPairInfoConverter : Converter { rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) }, ) } - } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 738424d73c..ec089f6df7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network @@ -22,7 +22,6 @@ import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.toFiatString -import kotlinx.coroutines.flow.first import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -39,7 +38,7 @@ internal class SwapInteractorImpl @Inject constructor( private val networksRepository: NetworksRepository, private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, ) : SwapInteractor { // TODO: Move to DI @@ -58,10 +57,9 @@ internal class SwapInteractorImpl @Inject constructor( ifRight = { it }, ) - requireNotNull(selectedWallet) + requireNotNull(selectedWallet) { "No selected wallet" } val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) - .first() .getOrElse { emptyList() } val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses.filter { @@ -112,12 +110,13 @@ internal class SwapInteractorImpl @Inject constructor( status?.let { CryptoCurrencySwapInfo(it, pair.providers) } } - val unavailableCryptoCurrencies = - (cryptoCurrenciesList - availableCryptoCurrencies.map { it.currencyStatus }.toSet()) + val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies + .map { it.currencyStatus } + .toSet() return CurrenciesGroup( available = availableCryptoCurrencies, - unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) } + unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) }, ) } @@ -138,14 +137,10 @@ internal class SwapInteractorImpl @Inject constructor( } } - private fun Currency.getContractAddress(): String { - return when (this) { - is Currency.NativeToken -> "0" - is Currency.NonNativeToken -> this.contractAddress - } - } - - suspend fun getPairs(initialCurrency: LeastTokenInfo, currenciesList: List): List { + private suspend fun getPairs( + initialCurrency: LeastTokenInfo, + currenciesList: List, + ): List { return repository.getPairs(initialCurrency, currenciesList) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 8942e75161..d64b0fa8f0 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -62,6 +62,7 @@ interface SwapRepository { amount: BigDecimal?, ): String + @Suppress("LongParameterList") suspend fun getExchangeQuote( fromContractAddress: String, fromNetwork: String, @@ -70,5 +71,5 @@ interface SwapRepository { fromAmount: BigDecimal, providerId: Int, rateType: RateType, - ) : ExchangeQuote + ): ExchangeQuote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index dc882859ae..7fa93f1c70 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -35,7 +35,7 @@ class SwapDomainModule { networksRepository: NetworksRepository, walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, + @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -81,8 +81,8 @@ class SwapDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, - ): GetCryptoCurrencyStatusUseCase { - return GetCryptoCurrencyStatusUseCase( + ): GetCryptoCurrencyStatusesSyncUseCase { + return GetCryptoCurrencyStatusesSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt index d087b97955..ee352e70c4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt @@ -4,5 +4,5 @@ import java.math.BigDecimal data class ExchangeQuote( val toAmount: BigDecimal, - val allowanceContract: String? + val allowanceContract: String?, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index a41f3d283d..ee772becce 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -17,7 +17,7 @@ data class SwapPairLeast( data class CryptoCurrencySwapInfo( val currencyStatus: CryptoCurrencyStatus, - val providers: List + val providers: List, ) /** diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 1068c49d28..8be06eaf61 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -17,6 +17,8 @@ dependencies { implementation(projects.common) /** Domain modules **/ + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) @@ -28,6 +30,7 @@ dependencies { implementation(deps.androidx.browser) /** Compose */ + implementation(deps.arrow.core) implementation(deps.compose.foundation) implementation(deps.compose.material) implementation(deps.compose.ui.tooling) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index bb52e2b01d..e3eff35d4c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -2,52 +2,108 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.getTintForTokenIcon +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.feature.swap.domain.models.domain.NetworkInfo -import com.tangem.feature.swap.domain.models.ui.FoundTokensStateExpress -import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress -import com.tangem.feature.swap.models.Network +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState +import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList class TokensDataConverter( private val onSearchEntered: (String) -> Unit, private val onTokenSelected: (String) -> Unit, private val isBalanceHiddenProvider: Provider, -) { + private val appCurrencyProvider: Provider, +) : Converter { - fun convertWithNetwork(value: FoundTokensStateExpress, network: NetworkInfo): SwapSelectTokenStateHolder { + override fun convert(value: CurrenciesGroup): SwapSelectTokenStateHolder { + val availableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource + val unavailableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource return SwapSelectTokenStateHolder( - availableTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), - unavailableTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), + availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } + .toMutableList() + .apply { + this.add(0, availableTitle) + } + .toImmutableList(), + unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } + .toMutableList() + .apply { + this.add(0, unavailableTitle) + } + .toImmutableList(), onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, - network = Network(network.name, network.blockchainId), ) } private fun tokenWithBalanceToTokenToSelect( - tokenWithBalance: TokenWithBalanceExpress, - ): TokenToSelectState.TokenToSelect { + cryptoCurrencySwapInfo: CryptoCurrencySwapInfo, + isAvailable: Boolean, + ): TokenToSelectState { + val cryptoCurrencyStatus = cryptoCurrencySwapInfo.currencyStatus return TokenToSelectState.TokenToSelect( - id = tokenWithBalance.token.id.value, - name = tokenWithBalance.token.name, - symbol = tokenWithBalance.token.symbol, - isNative = tokenWithBalance.token is CryptoCurrency.Coin, - // todo replace converting - tokenIcon = TokenIconState.CoinIcon( - url = "", - fallbackResId = 0, - isGrayscale = false, - showCustomBadge = false, - ), + id = cryptoCurrencyStatus.currency.id.value, + name = cryptoCurrencyStatus.currency.name, + symbol = cryptoCurrencyStatus.currency.symbol, + available = isAvailable, + tokenIcon = convertIcon(cryptoCurrencyStatus.currency, isAvailable), addedTokenBalanceData = TokenBalanceData( - amount = tokenWithBalance.tokenBalanceData?.amount, - amountEquivalent = tokenWithBalance.tokenBalanceData?.amountEquivalent, + amount = formatCryptoAmount(cryptoCurrencyStatus), + amountEquivalent = formatFiatAmount(cryptoCurrencyStatus, appCurrencyProvider.invoke()), isBalanceHidden = isBalanceHiddenProvider.invoke(), ), ) } + + private fun convertIcon(currency: CryptoCurrency, isAvailable: Boolean): TokenIconState { + return when (currency) { + is CryptoCurrency.Coin -> { + TokenIconState.CoinIcon( + url = currency.iconUrl, + fallbackResId = currency.networkIconResId, + isGrayscale = !isAvailable, + showCustomBadge = currency.isCustom, + ) + } + is CryptoCurrency.Token -> { + val isGrayscale = currency.network.isTestnet + val background = currency.tryGetBackgroundForTokenIcon(isGrayscale) + val tint = getTintForTokenIcon(background) + TokenIconState.TokenIcon( + url = currency.iconUrl, + isGrayscale = !isAvailable, + showCustomBadge = currency.isCustom, + networkBadgeIconResId = currency.networkIconResId, + fallbackTint = tint, + fallbackBackground = background, + ) + } + } + } + + private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String { + return BigDecimalFormatter.formatCryptoAmount( + cryptoCurrencyStatus.value.amount, + cryptoCurrencyStatus.currency.symbol, + cryptoCurrencyStatus.currency.decimals, + ) + } + + private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = cryptoCurrencyStatus.value.fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 382da8e588..ac6762d7b7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -1,30 +1,24 @@ package com.tangem.feature.swap.models import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList data class SwapSelectTokenStateHolder( val availableTokens: ImmutableList, val unavailableTokens: ImmutableList, - val network: Network, val onSearchEntered: (String) -> Unit, val onTokenSelected: (String) -> Unit, ) -data class Network( - val name: String, - val blockchainId: String, -) - sealed class TokenToSelectState { - data class Title(val title: String) : TokenToSelectState() + data class Title(val title: TextReference) : TokenToSelectState() data class TokenToSelect( val id: String, val name: String, val symbol: String, - val isNative: Boolean, val tokenIcon: TokenIconState, val available: Boolean = true, val addedTokenBalanceData: TokenBalanceData? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index bea53175af..510502e088 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -9,6 +9,7 @@ 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.wrappedList +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError @@ -24,12 +25,17 @@ import kotlinx.collections.immutable.toImmutableList * State builder creates a specific states for SwapScreen */ @Suppress("LargeClass") -internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: Provider) { +internal class StateBuilder( + private val actions: UiActions, + private val isBalanceHiddenProvider: Provider, + appCurrencyProvider: Provider, +) { private val tokensDataConverter = TokensDataConverter( onSearchEntered = actions.onSearchEntered, onTokenSelected = actions.onTokenSelected, isBalanceHiddenProvider = isBalanceHiddenProvider, + appCurrencyProvider = appCurrencyProvider, ) fun createInitialLoadingState(initialCurrency: CryptoCurrency, networkInfo: NetworkInfo): SwapStateHolder { @@ -247,15 +253,10 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: ) } - fun addTokensToState( - uiState: SwapStateHolder, - dataState: FoundTokensStateExpress, - networkInfo: NetworkInfo, - ): SwapStateHolder { + fun addTokensToState(uiState: SwapStateHolder, tokensDataState: CurrenciesGroup): SwapStateHolder { return uiState.copy( - selectTokenState = tokensDataConverter.convertWithNetwork( - value = dataState, - network = networkInfo, + selectTokenState = tokensDataConverter.convert( + value = tokensDataState, ), ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 1faa262bb3..6e89f78c3a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.Strings @@ -20,7 +19,8 @@ import com.tangem.core.ui.components.appbar.ExpandableSearchView import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.R @@ -43,8 +43,7 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) placeholderSearchText = stringResource(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered, onSearchDisplayClose = { state.onSearchEntered("") }, - subtitle = state.network.name, - icon = painterResource(id = getActiveIconRes(state.network.blockchainId)), + subtitle = "", // todo add title ) }, ) @@ -112,7 +111,7 @@ private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Mod .background(TangemTheme.colors.background.action), ) { Text( - text = item.title, + text = item.title.resolveReference().uppercase(), style = TangemTheme.typography.overline, modifier = Modifier .padding( @@ -134,7 +133,10 @@ private fun TokenItem( modifier = modifier .fillMaxWidth() .height(TangemTheme.dimens.size72) - .clickable(onClick = onTokenClick) + .clickable( + enabled = token.available, + onClick = onTokenClick, + ) .padding( vertical = TangemTheme.dimens.spacing14, horizontal = TangemTheme.dimens.spacing16, @@ -170,13 +172,7 @@ private fun TokenItem( Spacer(modifier = Modifier.weight(1f)) - if (!token.available) { - Text( - text = stringResource(id = R.string.swapping_token_not_available), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } else if (token.addedTokenBalanceData != null) { + if (token.addedTokenBalanceData != null) { Column( horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Center, @@ -191,7 +187,11 @@ private fun TokenItem( token.addedTokenBalanceData.amountEquivalent.orEmpty() }, style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, + color = if (token.available) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.tertiary + }, ) SpacerW2() Text( @@ -220,7 +220,6 @@ private val token = TokenToSelectState.TokenToSelect( id = "", name = "USDC", symbol = "USDC", - isNative = false, addedTokenBalanceData = TokenBalanceData( amount = "15 000 $", amountEquivalent = "15 000 " + @@ -230,7 +229,7 @@ private val token = TokenToSelectState.TokenToSelect( ) private val title = TokenToSelectState.Title( - title = "MY TOKENS", + title = stringReference("MY TOKENS"), ) @Preview @@ -243,7 +242,6 @@ private fun TokenScreenPreview() { unavailableTokens = listOf(title, token, token, token).toImmutableList(), onSearchEntered = {}, onTokenSelected = {}, - network = Network("Ethereum", "ETH"), ), onBack = {}, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 65abcf814c..c9018a2345 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency 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 +import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.TxFee data class SwapProcessDataState( @@ -20,4 +21,5 @@ data class SwapProcessDataState( val approveDataModel: RequestApproveStateData? = null, val swapDataModel: SwapStateData? = null, val selectedFee: TxFee? = null, + val tokensDataState: TokensDataStateExpress? = null, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 99919c05a3..0f2d531f35 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -4,9 +4,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import arrow.core.getOrElse import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler 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.model.CryptoCurrency import com.tangem.feature.swap.analytics.SwapEvents @@ -27,11 +30,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.runCatching import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json import timber.log.Timber import java.text.DecimalFormat import java.text.NumberFormat @@ -47,6 +48,7 @@ internal class SwapViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -55,9 +57,12 @@ internal class SwapViewModel @Inject constructor( private var isBalanceHidden = true + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private val stateBuilder = StateBuilder( actions = createUiActions(), isBalanceHiddenProvider = Provider { isBalanceHidden }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), ) private val inputNumberFormatter = @@ -87,7 +92,7 @@ internal class SwapViewModel @Inject constructor( derivationPath = initialCryptoCurrency.network.derivationPath.value, network = initialCryptoCurrency.network, ) - initTokens(initialCryptoCurrency) + initTokens() } override fun onCreate(owner: LifecycleOwner) { @@ -126,7 +131,7 @@ internal class SwapViewModel @Inject constructor( } @Suppress("UnusedPrivateMember") - private fun initTokens(currency: CryptoCurrency) { + private fun initTokens() { // new flow viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { @@ -134,10 +139,11 @@ internal class SwapViewModel @Inject constructor( }.onSuccess { state -> dataState = dataState.copy( fromCryptoCurrency = initialCryptoCurrency, - toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency + toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency, + tokensDataState = state, ) - // updateTokensState(dataState = state.foundTokensState) + updateTokensState(state) startLoadingQuotes( fromToken = initialCryptoCurrency, toToken = state.toGroup.available.first().currencyStatus.currency, @@ -171,11 +177,11 @@ internal class SwapViewModel @Inject constructor( // } } - private fun updateTokensState(dataState: FoundTokensStateExpress) { + private fun updateTokensState(dataState: TokensDataStateExpress) { + val tokensDataState = if (!isOrderReversed) dataState.toGroup else dataState.fromGroup uiState = stateBuilder.addTokensToState( uiState = uiState, - dataState = dataState, - networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), + tokensDataState = tokensDataState, ) } @@ -369,28 +375,31 @@ internal class SwapViewModel @Inject constructor( swapInteractor.searchTokens(dataState.networkId, searchQuery) } .onSuccess { - updateTokensState(it) + // updateTokensState(it) } .onFailure { } } } private fun onTokenSelect(id: String) { - val foundToken = swapInteractor.findTokenById(id) + val tokens = dataState.tokensDataState ?: return + val foundToken = tokens.toGroup.available.firstOrNull { + it.currencyStatus.currency.id.value == id + } analyticsEventHandler.send( - event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.symbol), + event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.currencyStatus?.currency?.symbol), ) if (foundToken != null) { val fromToken: CryptoCurrency val toToken: CryptoCurrency if (isOrderReversed) { - fromToken = foundToken + fromToken = foundToken.currencyStatus.currency toToken = initialCryptoCurrency } else { fromToken = initialCryptoCurrency - toToken = foundToken + toToken = foundToken.currencyStatus.currency } dataState = dataState.copy( fromCryptoCurrency = fromToken, @@ -532,6 +541,18 @@ internal class SwapViewModel @Inject constructor( ) } + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" From 537543636bceda85f3743b46907ad0ef7abdbb3f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 18:49:21 +0300 Subject: [PATCH 3/3] Updated on 2026-08-14 --- .../src/main/res/drawable/ic_no_token_44.xml | 10 ++ .../repository/DefaultQuotesRepository.kt | 8 ++ .../GetCryptoCurrencyStatusSyncUseCase.kt | 36 +++++ .../CurrenciesStatusesOperations.kt | 22 +++ .../tokens/repository/QuotesRepository.kt | 2 + .../swap/domain/BlockchainInteractor.kt | 3 - .../domain/DefaultBlockchainInteractor.kt | 9 -- .../feature/swap/domain/SwapInteractor.kt | 3 + .../feature/swap/domain/SwapInteractorImpl.kt | 5 + .../swap/domain/di/SwapDomainModule.kt | 8 -- features/swap/presentation/build.gradle.kts | 4 + .../swap/converters/TokensDataConverter.kt | 8 +- .../feature/swap/di/SwapPresentationModule.kt | 34 +++++ .../feature/swap/models/SwapStateHolder.kt | 38 ++++-- .../tangem/feature/swap/ui/StateBuilder.kt | 93 +++++++++++-- .../feature/swap/ui/SwapScreenContent.kt | 93 ++++++++----- .../tangem/feature/swap/ui/TransactionCard.kt | 65 ++++++++- .../swap/viewmodels/SwapProcessDataState.kt | 6 +- .../feature/swap/viewmodels/SwapViewModel.kt | 129 ++++++++++-------- 19 files changed, 431 insertions(+), 145 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_no_token_44.xml create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt diff --git a/core/ui/src/main/res/drawable/ic_no_token_44.xml b/core/ui/src/main/res/drawable/ic_no_token_44.xml new file mode 100644 index 0000000000..49e3b75b2a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_no_token_44.xml @@ -0,0 +1,10 @@ + + + diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index cdd04c62ce..7aa81fc78c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -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, appCurrencyId: String, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt new file mode 100644 index 0000000000..ffe6e79c3b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt @@ -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 { + val operations = CurrenciesStatusesOperations( + userWalletId = userWalletId, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + ) + + return operations.getCurrencyStatusSync(cryptoCurrencyId) + .mapLeft { error -> error.mapToTokenListError() } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 3ce6112296..a86698763b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -85,6 +85,28 @@ internal class CurrenciesStatusesOperations( } } + suspend fun getCurrencyStatusSync(cryptoCurrencyId: CryptoCurrency.ID): Either { + 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>> { return flow { val nonEmptyCurrencies = recover( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index a44b1e6c99..4c38eda580 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -29,4 +29,6 @@ interface QuotesRepository { * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. */ suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set + + suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt index 229911c654..8883e0f1a3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt @@ -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 * diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt index cb99b58076..836f193fe3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt @@ -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) - } - } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 2757583249..073c2464d8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -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.* @@ -110,4 +111,6 @@ interface SwapInteractor { networkId: String, fromToken: CryptoCurrency, ): Boolean + + fun getSelectedWallet(): UserWallet? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index ec089f6df7..1cfcebfb20 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -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 @@ -93,6 +94,10 @@ internal class SwapInteractorImpl @Inject constructor( ) } + override fun getSelectedWallet(): UserWallet? { + return getSelectedWalletSyncUseCase().getOrNull() + } + private fun getToCurrenciesGroup( currency: CryptoCurrency, leastPairs: List, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 7fa93f1c70..8845c68949 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCardTokensListUseCase -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -66,13 +65,6 @@ class SwapDomainModule { return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder) } - @SwapScope - @Provides - @Singleton - fun providesGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { - return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) - } - @SwapScope @Provides @Singleton diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 8be06eaf61..86879ce609 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -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) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index e3eff35d4c..6f42bbd1d2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -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, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt new file mode 100644 index 0000000000..441abf4a2e --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index ace9ee54d5..6427891e34 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -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, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 510502e088..2b1c60f67a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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, - appCurrencyProvider: Provider, + private val appCurrencyProvider: Provider, ) { 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() 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 = "—" } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 1ea66c1e90..153e3cd1b4 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -145,48 +145,24 @@ private fun MainInfo(state: SwapStateHolder) { } val (topCard, bottomCard, button) = createRefs() val priceImpactWarning = state.warnings.filterIsInstance().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", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 3cbd35a23f..d5996c36a5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -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 diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index c9018a2345..9c756453d6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -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, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 0f2d531f35..68b7f39e22 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -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 @@ -49,11 +51,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 @@ -88,11 +92,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) { @@ -137,44 +150,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) - startLoadingQuotes( - fromToken = initialCryptoCurrency, - toToken = state.toGroup.available.first().currencyStatus.currency, - amount = lastAmount.value, - ) + 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) { @@ -203,7 +210,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) } } @@ -285,8 +292,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), ) @@ -335,10 +342,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) { @@ -384,28 +391,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) } } @@ -419,13 +432,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) } } @@ -433,20 +446,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()) } } @@ -522,7 +535,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 @@ -531,7 +544,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) }