From 7ea613144d7af1cee0d4990670e4774daf2c874f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Jun 2025 15:50:44 +0500 Subject: [PATCH 01/99] Updated on 2026-08-14 --- .../testpush/ui/TestPushDeepLinkBottomSheet.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt index 4a99b46f96..98c73c7e22 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt @@ -3,6 +3,8 @@ package com.tangem.feature.tester.presentation.testpush.ui import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -29,8 +31,12 @@ internal fun TestPushDeeplinkBottomSheet(config: TangemBottomSheetConfig?) { @Composable private fun TestPushMenuDeepLinkBottomSheetContent(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content -> - LazyColumn { + TangemBottomSheet(config = config, addBottomInsets = false) { content -> + LazyColumn( + modifier = Modifier + .navigationBarsPadding() + .imePadding(), + ) { items( items = content.itemList.toList(), key = { it }, @@ -56,8 +62,12 @@ private fun TestPushMenuDeepLinkBottomSheetContent(config: TangemBottomSheetConf @Composable private fun TestPushMarketsTokenBottomSheetContent(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content -> - LazyColumn { + TangemBottomSheet(config = config, addBottomInsets = false) { content -> + LazyColumn( + modifier = Modifier + .navigationBarsPadding() + .imePadding(), + ) { item(key = "Markets Search") { OutlineTextField( label = "Title", From 5ba7bf294fa13c55d68203840b7e8d4c567ecc07 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Jun 2025 16:21:25 +0500 Subject: [PATCH 02/99] Updated on 2026-08-14 --- .../ui/TestPushDeepLinkBottomSheet.kt | 6 ++--- ...stPushMarketTokenBottomSheetTransformer.kt | 23 +++++++++++++------ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt index 98c73c7e22..7ca799438d 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt @@ -2,10 +2,7 @@ package com.tangem.feature.tester.presentation.testpush.ui import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text @@ -65,6 +62,7 @@ private fun TestPushMarketsTokenBottomSheetContent(config: TangemBottomSheetConf TangemBottomSheet(config = config, addBottomInsets = false) { content -> LazyColumn( modifier = Modifier + .fillMaxSize() .navigationBarsPadding() .imePadding(), ) { diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenBottomSheetTransformer.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenBottomSheetTransformer.kt index abd657eb9e..3c0c470fe5 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenBottomSheetTransformer.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenBottomSheetTransformer.kt @@ -15,16 +15,25 @@ internal class TestPushMarketTokenBottomSheetTransformer( private val onItemClick: (TokenMarket) -> Unit, ) : Transformer { override fun transform(prevState: TestPushUM): TestPushUM { + val content = (prevState.bottomSheetConfig?.content as? TestPushMarketsTokenConfigUM)?.copy( + itemList = item, + onSearchValueEdit = onSearchEdit, + onItemClick = onItemClick, + ) ?: TestPushMarketsTokenConfigUM( + itemList = item, + searchValue = TextFieldValue(""), + onSearchValueEdit = onSearchEdit, + onItemClick = onItemClick, + ) return prevState.copy( - bottomSheetConfig = TangemBottomSheetConfig( + bottomSheetConfig = prevState.bottomSheetConfig?.let { + prevState.bottomSheetConfig.copy( + content = content, + ) + } ?: TangemBottomSheetConfig( isShown = true, onDismissRequest = onDismissBottomSheet, - content = TestPushMarketsTokenConfigUM( - itemList = item, - searchValue = TextFieldValue(""), - onSearchValueEdit = onSearchEdit, - onItemClick = onItemClick, - ), + content = content, ), ) } From 9f1ae7fafb454715e8ef175167b1d7776907bbc2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Jun 2025 14:04:03 +0500 Subject: [PATCH 03/99] Updated on 2026-08-14 --- .../core/ui/components/audits/AuditLabel.kt | 44 ++- .../core/ui/components/audits/AuditLabelUM.kt | 3 + .../tangem/core/ui/components/badge/Badge.kt | 74 ++++ .../ui/components/badge/entity/BadgeUM.kt | 15 + .../buttons/common/TangemButtonSize.kt | 16 + .../provider/ProviderChooseCrypto.kt | 323 ++++++++++++++++++ .../provider/entity/ProviderChooseUM.kt | 39 +++ .../tangem/core/ui/extensions/ModifierExt.kt | 58 +++- core/ui/src/main/res/drawable/ic_gas_24.xml | 9 + core/ui/src/main/res/drawable/ic_speed_24.xml | 10 + .../main/res/drawable/ic_star_outline_24.xml | 9 + .../ui/SelectPaymentMethodBottomSheet.kt | 33 +- .../providers/ui/SelectProviderBottomSheet.kt | 24 +- .../features/onramp/utils/ModifierExt.kt | 24 -- 14 files changed, 602 insertions(+), 79 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/badge/Badge.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/badge/entity/BadgeUM.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/provider/entity/ProviderChooseUM.kt create mode 100644 core/ui/src/main/res/drawable/ic_gas_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_speed_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_star_outline_24.xml delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ModifierExt.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabel.kt index eed177c971..e5293e07ab 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabel.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabel.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.audits import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -11,6 +12,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -30,41 +32,59 @@ import com.tangem.core.ui.res.TangemThemePreview fun AuditLabel(state: AuditLabelUM, modifier: Modifier = Modifier) { AuditLabelInternal( textReference = state.text, - textColor = getColorByType(state.type), - backgroundColor = getColorByType(state.type).copy(alpha = 0.1f), + type = state.type, modifier = modifier, ) } @Composable -private fun AuditLabelInternal( - textReference: TextReference, - textColor: Color, - backgroundColor: Color, - modifier: Modifier = Modifier, -) { +private fun AuditLabelInternal(textReference: TextReference, type: AuditLabelUM.Type, modifier: Modifier = Modifier) { Box( modifier = modifier.background( - color = backgroundColor, + color = getBackgroundColorByType(type), shape = TangemTheme.shapes.roundedCornersSmall2, ), ) { Text( text = textReference.resolveReference(), style = TangemTheme.typography.caption1, - color = textColor, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4), + color = getTextColorByType(type), + modifier = Modifier.padding(getPaddingByType(type)), ) } } @ReadOnlyComposable @Composable -private fun getColorByType(type: AuditLabelUM.Type): Color { +private fun getTextColorByType(type: AuditLabelUM.Type): Color { return when (type) { AuditLabelUM.Type.Prohibition -> TangemTheme.colors.text.warning AuditLabelUM.Type.Warning -> TangemTheme.colors.text.attention AuditLabelUM.Type.Permit -> TangemTheme.colors.text.accent + AuditLabelUM.Type.Info -> TangemTheme.colors.text.primary2 + } +} + +@ReadOnlyComposable +@Composable +private fun getBackgroundColorByType(type: AuditLabelUM.Type): Color { + return when (type) { + AuditLabelUM.Type.Prohibition -> TangemTheme.colors.text.warning.copy(alpha = 0.1f) + AuditLabelUM.Type.Warning -> TangemTheme.colors.text.attention.copy(alpha = 0.1f) + AuditLabelUM.Type.Permit -> TangemTheme.colors.text.accent.copy(alpha = 0.1f) + AuditLabelUM.Type.Info -> TangemTheme.colors.text.accent + } +} + +@ReadOnlyComposable +@Composable +private fun getPaddingByType(type: AuditLabelUM.Type): PaddingValues { + return when (type) { + AuditLabelUM.Type.Prohibition, + AuditLabelUM.Type.Warning, + AuditLabelUM.Type.Permit, + -> PaddingValues(horizontal = 4.dp) + AuditLabelUM.Type.Info -> PaddingValues(horizontal = 6.dp, vertical = 1.dp) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabelUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabelUM.kt index 25059b9594..4b1ed19772 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabelUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabelUM.kt @@ -22,5 +22,8 @@ data class AuditLabelUM(val text: TextReference, val type: Type) { /** Blue. Like, "trusted" */ Permit, + + /** Blue with white text. Like, "best rate" */ + Info, } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/badge/Badge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/badge/Badge.kt new file mode 100644 index 0000000000..282f4a0618 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/badge/Badge.kt @@ -0,0 +1,74 @@ +package com.tangem.core.ui.components.badge + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Badge component + * + * @param iconRes icon drawable resource + * @param text text reference + * @param modifier composable modifier + * + * @see Figma + */ +@Composable +fun Badge(@DrawableRes iconRes: Int, text: TextReference, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier + .border(1.dp, TangemTheme.colors.stroke.primary, RoundedCornerShape(4.dp)) + .padding(horizontal = 4.dp), + ) { + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(iconRes), + ), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.size(12.dp), + ) + Text( + text = text.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Badge_Preview() { + TangemThemePreview { + Badge( + iconRes = R.drawable.ic_star_mini_24, + text = stringReference("4.9"), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/badge/entity/BadgeUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/badge/entity/BadgeUM.kt new file mode 100644 index 0000000000..ee6aa0bcdb --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/badge/entity/BadgeUM.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.components.badge.entity + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +/** + * Badge component UI model + * + * @param iconRes icon drawable resource + * @param text text reference + */ +data class BadgeUM( + val text: TextReference, + @DrawableRes val iconRes: Int, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt index 961bca8869..46530d78b1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt @@ -16,6 +16,7 @@ enum class TangemButtonSize { RoundedAction, WideAction, TwoLines, + Small, } @Composable @@ -28,6 +29,7 @@ internal fun TangemButtonSize.toHeightDp(): Dp = when (this) { TangemButtonSize.RoundedAction, -> TangemTheme.dimens.size36 TangemButtonSize.WideAction -> TangemTheme.dimens.size40 + TangemButtonSize.Small -> 24.dp } @Composable @@ -40,6 +42,7 @@ internal fun TangemButtonSize.toShape(): Shape = when (this) { TangemButtonSize.Action -> TangemTheme.shapes.roundedCornersMedium TangemButtonSize.TwoLines -> TangemTheme.shapes.roundedCornersXMedium TangemButtonSize.RoundedAction -> TangemTheme.shapes.roundedCornersLarge + TangemButtonSize.Small -> TangemTheme.shapes.roundedCornersXMedium } @Composable @@ -48,6 +51,7 @@ internal fun TangemButtonSize.toIconPadding(): Dp = when (this) { TangemButtonSize.Default, TangemButtonSize.WideAction, TangemButtonSize.TwoLines, + TangemButtonSize.Small, -> TangemTheme.dimens.spacing4 TangemButtonSize.Text -> TangemTheme.dimens.spacing8 TangemButtonSize.Selector -> 0.dp @@ -100,11 +104,18 @@ internal fun TangemButtonSize.toContentPadding(icon: TangemButtonIconPosition): start = horizontalPadding.first, end = horizontalPadding.second, ) + TangemButtonSize.Small -> PaddingValues( + top = TangemTheme.dimens.spacing4, + bottom = TangemTheme.dimens.spacing4, + start = horizontalPadding.first, + end = horizontalPadding.second, + ) } } @Composable @ReadOnlyComposable +@Suppress("CyclomaticComplexMethod") internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconPosition): Pair { return when (this) { TangemButtonSize.Default, @@ -124,5 +135,10 @@ internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconP is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing24 is TangemButtonIconPosition.End -> TangemTheme.dimens.spacing24 to TangemTheme.dimens.spacing16 } + TangemButtonSize.Small -> when (icon) { + is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing12 to TangemTheme.dimens.spacing12 + is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing8 to TangemTheme.dimens.spacing12 + is TangemButtonIconPosition.End -> TangemTheme.dimens.spacing12 to TangemTheme.dimens.spacing8 + } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt new file mode 100644 index 0000000000..1083b56912 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt @@ -0,0 +1,323 @@ +package com.tangem.core.ui.components.provider + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.R +import com.tangem.core.ui.components.audits.AuditLabel +import com.tangem.core.ui.components.audits.AuditLabelUM +import com.tangem.core.ui.components.badge.Badge +import com.tangem.core.ui.components.badge.entity.BadgeUM +import com.tangem.core.ui.components.provider.entity.ProviderChooseUM +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf + +/** + * Provider Row - Choose Crypto component + * + * @param providerChooseUM component ui model + * @param onClick click listener + * @param modifier composable modifier + * + * @see Figma + */ +@Composable +@Suppress("DestructuringDeclarationWithTooManyEntries") +fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + ConstraintLayout( + modifier = modifier + .background(TangemTheme.colors.background.action) + .clip(RoundedCornerShape(14.dp)) + .selectedBorder(isSelected = providerChooseUM.isSelected) + .clickable(enabled = true, onClick = onClick) + .fillMaxWidth() + .padding(all = 12.dp), + ) { + val (iconRef, titleRef, badgeRef, infoRef, labelRef) = createRefs() + val titleEndRef = createStartBarrier(infoRef, labelRef) + + IconContent( + iconUrl = providerChooseUM.iconUrl, + modifier = Modifier + .constrainAs(iconRef) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + }, + ) + TitleContent( + providerChooseUM = providerChooseUM, + modifier = Modifier.constrainAs(titleRef) { + start.linkTo(iconRef.end, 12.dp) + end.linkTo(titleEndRef, 4.dp) + top.linkTo(parent.top) + bottom.linkTo(badgeRef.top) + width = Dimension.fillToConstraints + }, + ) + BadgeContent( + providerChooseUM = providerChooseUM, + modifier = Modifier.constrainAs(badgeRef) { + start.linkTo(iconRef.end, 12.dp) + end.linkTo(labelRef.start, 4.dp) + top.linkTo(titleRef.bottom, 4.dp) + bottom.linkTo(parent.bottom) + width = Dimension.fillToConstraints + visibility = if (providerChooseUM.badgeList.isEmpty()) { + Visibility.Gone + } else { + Visibility.Visible + } + }, + ) + InfoContent( + text = providerChooseUM.infoText, + modifier = Modifier.constrainAs(infoRef) { + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(labelRef.top, 4.dp) + }, + ) + LabelContent( + labelUM = providerChooseUM.labelUM, + modifier = Modifier.constrainAs(labelRef) { + end.linkTo(parent.end) + top.linkTo(infoRef.bottom) + bottom.linkTo(parent.bottom) + visibility = if (providerChooseUM.labelUM == null) { + Visibility.Gone + } else { + Visibility.Visible + } + }, + ) + } +} + +@Composable +private fun IconContent(iconUrl: String, modifier: Modifier = Modifier) { + SubcomposeAsyncImage( + modifier = modifier + .size(40.dp) + .clip(RoundedCornerShape(8.dp)) + .background(TangemColorPalette.Light1), + model = ImageRequest.Builder(context = LocalContext.current) + .data(iconUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + contentDescription = null, + ) +} + +@Composable +private fun TitleContent(providerChooseUM: ProviderChooseUM, modifier: Modifier = Modifier) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + ) { + Text( + text = providerChooseUM.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + ) + Text( + text = providerChooseUM.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + ) + } +} + +@Composable +private fun InfoContent(text: TextReference, modifier: Modifier = Modifier) { + Text( + text = text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier, + ) +} + +@Composable +private fun BadgeContent(providerChooseUM: ProviderChooseUM, modifier: Modifier = Modifier) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + ) { + providerChooseUM.badgeList.fastForEach { badge -> + Badge( + iconRes = badge.iconRes, + text = badge.text, + ) + } + } +} + +@Composable +private fun LabelContent(labelUM: ProviderChooseUM.LabelUM?, modifier: Modifier = Modifier) { + Box( + modifier = modifier, + ) { + when (labelUM) { + is ProviderChooseUM.LabelUM.Info -> AuditLabel(labelUM.auditLabelUM) + is ProviderChooseUM.LabelUM.Text -> Text( + text = labelUM.text.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.warning, + ) + null -> Unit + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun ProviderChooseCrypto_Preview( + @PreviewParameter(ProviderChooseCryptoPreviewProvider::class) params: ProviderChooseUM, +) { + TangemThemePreview { + ProviderChooseCrypto( + providerChooseUM = params, + onClick = {}, + ) + } +} + +private class ProviderChooseCryptoPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + ProviderChooseUM( + title = stringReference("ChangeHero"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 800,00 POL"), + iconUrl = "", + badgeList = persistentListOf( + BadgeUM( + text = stringReference("4.9"), + iconRes = R.drawable.ic_star_outline_24, + ), + BadgeUM( + text = stringReference("5 mins"), + iconRes = R.drawable.ic_speed_24, + ), + BadgeUM( + text = stringReference("$3.45"), + iconRes = R.drawable.ic_gas_24, + ), + ), + isSelected = true, + labelUM = ProviderChooseUM.LabelUM.Info( + auditLabelUM = AuditLabelUM( + text = resourceReference(R.string.express_provider_best_rate), + type = AuditLabelUM.Type.Info, + ), + ), + ), + ProviderChooseUM( + title = stringReference("Changelly"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 799,12 POL"), + iconUrl = "", + badgeList = persistentListOf( + BadgeUM( + text = stringReference("4.9"), + iconRes = R.drawable.ic_star_outline_24, + ), + BadgeUM( + text = stringReference("5 mins"), + iconRes = R.drawable.ic_speed_24, + ), + BadgeUM( + text = stringReference("$3.45"), + iconRes = R.drawable.ic_gas_24, + ), + ), + isSelected = false, + labelUM = ProviderChooseUM.LabelUM.Text( + text = stringReference("–0.2%"), + ), + ), + ProviderChooseUM( + title = stringReference("Changelly"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 799,12 POL"), + iconUrl = "", + badgeList = persistentListOf(), + isSelected = false, + labelUM = ProviderChooseUM.LabelUM.Text( + text = stringReference("–0.2%"), + ), + ), + ProviderChooseUM( + title = stringReference("Changelly"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 POL"), + iconUrl = "", + badgeList = persistentListOf(), + isSelected = false, + labelUM = ProviderChooseUM.LabelUM.Text( + text = stringReference("–0.2%"), + ), + ), + ProviderChooseUM( + title = stringReference("Changelly"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 POL"), + iconUrl = "", + badgeList = persistentListOf(), + isSelected = false, + labelUM = null, + ), + ProviderChooseUM( + title = stringReference("Changelly"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 POL"), + iconUrl = "", + badgeList = persistentListOf( + BadgeUM( + text = stringReference("4.9"), + iconRes = R.drawable.ic_star_outline_24, + ), + BadgeUM( + text = stringReference("5 mins"), + iconRes = R.drawable.ic_speed_24, + ), + BadgeUM( + text = stringReference("$3.45"), + iconRes = R.drawable.ic_gas_24, + ), + ), + isSelected = false, + labelUM = null, + ), + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/entity/ProviderChooseUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/entity/ProviderChooseUM.kt new file mode 100644 index 0000000000..b3a9392c87 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/entity/ProviderChooseUM.kt @@ -0,0 +1,39 @@ +package com.tangem.core.ui.components.provider.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.audits.AuditLabelUM +import com.tangem.core.ui.components.badge.entity.BadgeUM +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +/** + * Provider Row - Choose Crypto component UI Model + * + * @property title title text + * @property subtitle subtitle text + * @property infoText info text + * @property iconUrl icon url + * @property isSelected indicated whether component is selected (border outline) + * @property badgeList list of badge components + * @property labelUM label component (whether AuditLabel or text) + */ +data class ProviderChooseUM( + val title: TextReference, + val subtitle: TextReference, + val infoText: TextReference, + val iconUrl: String, + val isSelected: Boolean, + val badgeList: ImmutableList, + val labelUM: LabelUM?, +) { + @Immutable + sealed class LabelUM { + data class Info( + val auditLabelUM: AuditLabelUM, + ) : LabelUM() + + data class Text( + val text: TextReference, + ) : LabelUM() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt index a014910b24..fc74ccf197 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt @@ -1,12 +1,21 @@ package com.tangem.core.ui.extensions import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.MultipleClickPreventer /** @@ -38,4 +47,51 @@ fun Modifier.conditional(condition: Boolean, modifier: Modifier.() -> Modifier): } else { this } -} \ No newline at end of file +} + +/** + * Conditionally applies a modifier based on a boolean condition. + */ +@Composable +fun Modifier.conditionalCompose( + condition: Boolean, + modifier: @Composable Modifier.() -> Modifier = { Modifier }, + otherModifier: @Composable Modifier.() -> Modifier = { this }, +): Modifier { + return if (condition) { + then(modifier(Modifier)) + } else { + otherModifier() + } +} + +/** + * Border with accent outline when selected + */ +@Composable +fun Modifier.selectedBorder( + isSelected: Boolean, + width: Dp = 2.5.dp, + color: Color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), + radius: Dp = 16.dp, +) = conditionalCompose( + condition = isSelected, + modifier = { + border( + width = width, + color = color, + shape = RoundedCornerShape(radius), + ) + .padding(2.5.dp) + .border( + width = 1.dp, + color = TangemTheme.colors.text.accent, + shape = RoundedCornerShape(radius - 2.dp), + ) + .clip(RoundedCornerShape(radius - 2.dp)) + }, + otherModifier = { + padding(2.dp) + .clip(RoundedCornerShape(radius - 2.dp)) + }, +) \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_gas_24.xml b/core/ui/src/main/res/drawable/ic_gas_24.xml new file mode 100644 index 0000000000..278945f9fa --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_gas_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_speed_24.xml b/core/ui/src/main/res/drawable/ic_speed_24.xml new file mode 100644 index 0000000000..8e4390a831 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_speed_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_star_outline_24.xml b/core/ui/src/main/res/drawable/ic_star_outline_24.xml new file mode 100644 index 0000000000..116e8fdc03 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_star_outline_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt index 48ce4dfda8..fb80254c3c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.draw.clip import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.res.TangemTheme import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodsBottomSheetConfig -import com.tangem.features.onramp.utils.selectedBorder import kotlinx.collections.immutable.ImmutableList @Composable @@ -59,27 +59,16 @@ private fun SelectPaymentMethodBottomSheetContent( val isSelected = remember(item, selectedMethodId) { item.id == selectedMethodId } - val itemModifier = if (isSelected) { - Modifier - .fillMaxWidth() - .selectedBorder() - .clickable(onClick = item.onSelect) - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing10, - bottom = TangemTheme.dimens.spacing10, - ) - } else { - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .clickable(onClick = item.onSelect) - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing10, - bottom = TangemTheme.dimens.spacing10, - ) - } + val itemModifier = Modifier + .fillMaxWidth() + .selectedBorder(isSelected = isSelected) + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .clickable(onClick = item.onSelect) + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing10, + bottom = TangemTheme.dimens.spacing10, + ) PaymentMethodItem( modifier = itemModifier, paymentMethod = item, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt index 6f74ef7c95..59cb8cb0b0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt @@ -26,13 +26,10 @@ import androidx.compose.ui.util.fastForEach import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.onramp.model.OnrampPaymentMethod @@ -41,7 +38,6 @@ import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon import com.tangem.features.onramp.providers.entity.ProviderListItemUM import com.tangem.features.onramp.providers.entity.SelectPaymentAndProviderUM import com.tangem.features.onramp.providers.model.previewData.SelectProviderPreviewData -import com.tangem.features.onramp.utils.selectedBorder @Composable internal fun SelectProviderBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { @@ -151,13 +147,7 @@ private fun ProviderItem(state: ProviderListItemUM, modifier: Modifier = Modifie private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, modifier: Modifier = Modifier) { Row( modifier = modifier - .then( - if (state.isSelected) { - Modifier.selectedBorder() - } else { - Modifier.clip(RoundedCornerShape(16.dp)) - }, - ) + .selectedBorder(isSelected = state.isSelected) .clickable(onClick = state.onClick) .padding(all = TangemTheme.dimens.spacing12), verticalAlignment = Alignment.CenterVertically, @@ -227,13 +217,7 @@ private fun UnavailableProviderItem( ) { Row( modifier = modifier - .then( - if (isSelected) { - Modifier.selectedBorder() - } else { - Modifier.clip(RoundedCornerShape(16.dp)) - }, - ) + .selectedBorder(isSelected = isSelected) .padding(all = TangemTheme.dimens.spacing12), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ModifierExt.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ModifierExt.kt deleted file mode 100644 index 8e38b54575..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ModifierExt.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.features.onramp.utils - -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme - -@Composable -internal fun Modifier.selectedBorder() = border( - width = 2.5.dp, - color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), - shape = RoundedCornerShape(18.dp), -) - .padding(2.5.dp) - .border( - width = 1.dp, - color = TangemTheme.colors.text.accent, - shape = RoundedCornerShape(16.dp), - ) - .clip(RoundedCornerShape(16.dp)) \ No newline at end of file From c8c122b84840bc478249a9b2a26a20b12685e767 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 May 2025 17:36:17 +0400 Subject: [PATCH 04/99] Updated on 2026-08-14 --- .../test/data/quote/QuoteResponseExt.kt | 12 +- ...teConverter.kt => QuoteStatusConverter.kt} | 21 ++-- .../converters/HotCryptoCurrencyConverter.kt | 18 +-- .../data/quotes/DefaultQuotesRepositoryV2.kt | 7 +- .../quotes/multi/DefaultMultiQuoteUpdater.kt | 4 +- .../single/DefaultSingleQuoteProducer.kt | 6 +- .../data/quotes/store/DefaultQuotesStoreV2.kt | 22 ++-- .../tangem/data/quotes/store/QuotesStoreV2.kt | 8 +- ... => DefaultMultiQuoteStatusFetcherTest.kt} | 2 +- ... => DefaultMultiQuoteStatusUpdaterTest.kt} | 2 +- ...=> DefaultSingleQuoteStatusFetcherTest.kt} | 2 +- ...> DefaultSingleQuoteStatusProducerTest.kt} | 40 ++++--- .../quotes/store/QuotesStoreGetMethodTest.kt | 10 +- .../store/QuotesStoreInitializationTest.kt | 8 +- .../store/QuotesStoreUpdateMethodsTest.kt | 26 +++-- .../markets/GetCurrencyQuotesUseCase.kt | 6 +- .../tangem/domain/nft/GetNFTPriceUseCase.kt | 11 +- .../domain/onramp/model/HotCryptoCurrency.kt | 6 +- .../domain/quotes/QuotesRepositoryV2.kt | 4 +- .../quotes/single/SingleQuoteProducer.kt | 4 +- .../quotes/single/SingleQuoteSupplier.kt | 4 +- .../com/tangem/domain/tokens/model/Quote.kt | 39 ------- .../tangem/domain/tokens/model/QuoteStatus.kt | 69 ++++++++++++ .../com/tangem/domain/tokens/model/Token.kt | 4 +- .../BaseCurrencyStatusOperations.kt | 12 +- .../CachedCurrenciesStatusesOperations.kt | 22 ++-- .../operations/CurrencyStatusOperations.kt | 62 +++++------ .../utils/CurrencyStatusProxyCreator.kt | 20 ++-- .../tangem/domain/tokens/mock/MockQuotes.kt | 104 +++++++++++------- .../domain/tokens/mock/MockTokensStates.kt | 58 +++++----- .../converter/HotTokenItemStateConverter.kt | 17 +-- .../feature/swap/domain/SwapInteractorImpl.kt | 26 +++-- ...enDetailsSwapTransactionsStateConverter.kt | 20 ++-- .../factory/express/ExchangeStatusFactory.kt | 10 +- 34 files changed, 383 insertions(+), 303 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/{QuoteConverter.kt => QuoteStatusConverter.kt} (64%) rename data/quotes/src/test/java/com/tangem/data/quotes/multi/{DefaultMultiQuoteFetcherTest.kt => DefaultMultiQuoteStatusFetcherTest.kt} (99%) rename data/quotes/src/test/java/com/tangem/data/quotes/multi/{DefaultMultiQuoteUpdaterTest.kt => DefaultMultiQuoteStatusUpdaterTest.kt} (99%) rename data/quotes/src/test/java/com/tangem/data/quotes/single/{DefaultSingleQuoteFetcherTest.kt => DefaultSingleQuoteStatusFetcherTest.kt} (99%) rename data/quotes/src/test/java/com/tangem/data/quotes/single/{DefaultSingleQuoteProducerTest.kt => DefaultSingleQuoteStatusProducerTest.kt} (79%) delete mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/QuoteStatus.kt diff --git a/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt index ac570f6d81..5e294cc4c0 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt @@ -1,14 +1,14 @@ package com.tangem.common.test.data.quote import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.datasource.local.quote.converter.QuoteConverter +import com.tangem.datasource.local.quote.converter.QuoteStatusConverter import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus -fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): Quote { - return QuoteConverter(source = source).convert(value = mapOf(rawCurrencyId to this).entries.first()) +fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): QuoteStatus { + return QuoteStatusConverter(source = source).convert(value = mapOf(rawCurrencyId to this).entries.first()) } -fun Pair.toDomain(source: StatusSource = StatusSource.ACTUAL): Quote { - return QuoteConverter(source = source).convert(value = mapOf(this).entries.first()) +fun Pair.toDomain(source: StatusSource = StatusSource.ACTUAL): QuoteStatus { + return QuoteStatusConverter(source = source).convert(value = mapOf(this).entries.first()) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteStatusConverter.kt similarity index 64% rename from core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteStatusConverter.kt index 555eb268f9..d3ee90a729 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteStatusConverter.kt @@ -3,21 +3,20 @@ package com.tangem.datasource.local.quote.converter import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.orZero /** - * Converter from [QuotesResponse.Quote] to [Quote.Value] + * Converter from [QuotesResponse.Quote] to [QuoteStatus] * * @property source status source * [REDACTED_AUTHOR] */ -class QuoteConverter( +class QuoteStatusConverter( private val source: StatusSource, -) : - Converter, Quote.Value> { +) : Converter, QuoteStatus> { /** * Secondary constructor @@ -28,14 +27,16 @@ class QuoteConverter( source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, ) - override fun convert(value: Map.Entry): Quote.Value { + override fun convert(value: Map.Entry): QuoteStatus { val (currencyId, quote) = value - return Quote.Value( + return QuoteStatus( rawCurrencyId = CryptoCurrency.RawID(currencyId), - fiatRate = quote.price.orZero(), - priceChange = quote.priceChange24h.orZero().movePointLeft(2), - source = source, + value = QuoteStatus.Data( + source = source, + fiatRate = quote.price.orZero(), + priceChange = quote.priceChange24h.orZero().movePointLeft(2), + ), ) } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt index a0cadbe035..c1ece25f69 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onramp.model.HotCryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.converter.Converter import java.math.BigDecimal @@ -40,7 +40,7 @@ internal class HotCryptoCurrencyConverter( return HotCryptoCurrency( cryptoCurrency = currency.setupIconUrl(id = rawId.value), - quote = createQuote( + quoteStatus = createQuote( fiatRate = value.currentPrice, priceChange = value.priceChangePercentage, rawCurrencyId = rawId, @@ -89,16 +89,18 @@ internal class HotCryptoCurrencyConverter( fiatRate: BigDecimal?, priceChange: BigDecimal?, rawCurrencyId: CryptoCurrency.RawID, - ): Quote { + ): QuoteStatus { return if (fiatRate != null && priceChange != null) { - Quote.Value( + QuoteStatus( rawCurrencyId = rawCurrencyId, - fiatRate = fiatRate, - priceChange = priceChange.movePointLeft(2), - source = StatusSource.ACTUAL, // It doesn't matter + value = QuoteStatus.Data( + fiatRate = fiatRate, + priceChange = priceChange.movePointLeft(2), + source = StatusSource.ACTUAL, // It doesn't matter + ), ) } else { - Quote.Empty(rawCurrencyId) + QuoteStatus(rawCurrencyId = rawCurrencyId) } } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt index 616b9cebc6..ed18015ad2 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt @@ -3,7 +3,7 @@ package com.tangem.data.quotes import com.tangem.data.quotes.store.QuotesStoreV2 import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.QuotesRepositoryV2 -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import javax.inject.Inject /** @@ -17,9 +17,10 @@ internal class DefaultQuotesRepositoryV2 @Inject constructor( private val quotesStore: QuotesStoreV2, ) : QuotesRepositoryV2 { - override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? { + override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? { return quotesStore.getAllSyncOrNull()?.mapTo(hashSetOf()) { - it.takeIf { it.rawCurrencyId in currenciesIds } ?: Quote.Empty(it.rawCurrencyId) + it.takeIf { it.rawCurrencyId in currenciesIds } + ?: QuoteStatus(rawCurrencyId = it.rawCurrencyId) } } } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt index 06e492857e..8f0ae6297f 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt @@ -7,7 +7,7 @@ import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.multi.MultiQuoteUpdater -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -61,7 +61,7 @@ internal class DefaultMultiQuoteUpdater @Inject constructor( .filterNotNull() .mapLatest { appCurrency -> val currenciesIds = quotesStore.getAllSyncOrNull().orEmpty() - .mapTo(destination = hashSetOf(), transform = Quote::rawCurrencyId) + .mapTo(destination = hashSetOf(), transform = QuoteStatus::rawCurrencyId) multiQuoteFetcher( params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrency.id), diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt index d8450243b6..796d0dceac 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt @@ -2,7 +2,7 @@ package com.tangem.data.quotes.single import com.tangem.data.quotes.store.QuotesStoreV2 import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -24,9 +24,9 @@ internal class DefaultSingleQuoteProducer @AssistedInject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : SingleQuoteProducer { - override val fallback: Quote = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + override val fallback: QuoteStatus = QuoteStatus(rawCurrencyId = params.rawCurrencyId) - override fun produce(): Flow { + override fun produce(): Flow { return quotesStore.get() .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } } .distinctUntilChanged() diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt index ccad728a92..2158d4d6a8 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt @@ -3,10 +3,10 @@ package com.tangem.data.quotes.store import androidx.datastore.core.DataStore import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.quote.converter.QuoteConverter +import com.tangem.datasource.local.quote.converter.QuoteStatusConverter import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.CoroutineScope @@ -26,7 +26,7 @@ internal typealias CurrencyIdWithQuote = Map * @param dispatchers dispatchers */ internal class DefaultQuotesStoreV2( - private val runtimeStore: RuntimeSharedStore>, + private val runtimeStore: RuntimeSharedStore>, private val persistenceDataStore: DataStore, dispatchers: CoroutineDispatcherProvider, ) : QuotesStoreV2 { @@ -40,14 +40,14 @@ internal class DefaultQuotesStoreV2( if (cachedStatuses.isNullOrEmpty()) return@launch runtimeStore.store( - value = QuoteConverter(isCached = true).convertSet(input = cachedStatuses.entries), + value = QuoteStatusConverter(isCached = true).convertSet(input = cachedStatuses.entries), ) } } - override fun get(): Flow> = runtimeStore.get() + override fun get(): Flow> = runtimeStore.get() - override suspend fun getAllSyncOrNull(): Set? = runtimeStore.getSyncOrNull() + override suspend fun getAllSyncOrNull(): Set? = runtimeStore.getSyncOrNull() override suspend fun refresh(currenciesIds: Set) { updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.CACHE) @@ -56,7 +56,7 @@ internal class DefaultQuotesStoreV2( override suspend fun storeActual(values: Map) { coroutineScope { launch { - val quotes = QuoteConverter(isCached = false).convertSet(input = values.entries) + val quotes = QuoteStatusConverter(isCached = false).convertSet(input = values.entries) storeInRuntimeStore(values = quotes) } launch { storeInPersistenceStore(values = values) } @@ -66,14 +66,14 @@ internal class DefaultQuotesStoreV2( override suspend fun storeError(currenciesIds: Set) { updateStatusSourceInRuntime( currenciesIds = currenciesIds, - ifNotFound = Quote::Empty, + ifNotFound = ::QuoteStatus, source = StatusSource.ONLY_CACHE, ) } private suspend fun updateStatusSourceInRuntime( currenciesIds: Set, - ifNotFound: (CryptoCurrency.RawID) -> Quote? = { null }, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, source: StatusSource, ) { runtimeStore.update(default = emptySet()) { stored -> @@ -82,14 +82,14 @@ internal class DefaultQuotesStoreV2( ?: ifNotFound(id) ?: return@mapNotNullTo null - quote.copySealed(source = source) + quote.copy(value = quote.value.copySealed(source = source)) } stored.addOrReplace(items = updatedQuotes) { old, new -> old.rawCurrencyId == new.rawCurrencyId } } } - private suspend fun storeInRuntimeStore(values: Set) { + private suspend fun storeInRuntimeStore(values: Set) { runtimeStore.update(default = emptySet()) { saved -> saved.addOrReplace(items = values) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId } } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt index 1fb13a9d5e..b29bb240af 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt @@ -2,17 +2,17 @@ package com.tangem.data.quotes.store import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import kotlinx.coroutines.flow.Flow -/** Store of [Quote]'es set */ +/** Store of [QuoteStatus]'es set */ internal interface QuotesStoreV2 { /** Get flow of quotes */ - fun get(): Flow> + fun get(): Flow> /** Get all quotes synchronously or null */ - suspend fun getAllSyncOrNull(): Set? + suspend fun getAllSyncOrNull(): Set? /** Refresh status of [currenciesIds] */ suspend fun refresh(currenciesIds: Set) diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt similarity index 99% rename from data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt rename to data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt index 63fec8ab17..d49e65f597 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt @@ -23,7 +23,7 @@ import java.math.BigDecimal /** [REDACTED_AUTHOR] */ -internal class DefaultMultiQuoteFetcherTest { +internal class DefaultMultiQuoteStatusFetcherTest { private val tangemTechApi = mockk(relaxed = true) private val appCurrencyResponseStore = mockk(relaxed = true) diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt similarity index 99% rename from data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt rename to data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt index f14962f52b..d51f1c0460 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt @@ -16,7 +16,7 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class DefaultMultiQuoteUpdaterTest { +internal class DefaultMultiQuoteStatusUpdaterTest { private val appCurrencyResponseStore: AppCurrencyResponseStore = mockk() private val quotesStore: QuotesStoreV2 = mockk() diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt similarity index 99% rename from data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt rename to data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt index a16f80b3d7..83108ee088 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import java.math.BigDecimal -internal class DefaultSingleQuoteFetcherTest { +internal class DefaultSingleQuoteStatusFetcherTest { private val tangemTechApi = mockk(relaxed = true) private val appCurrencyResponseStore = mockk(relaxed = true) diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt similarity index 79% rename from data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt rename to data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt index ce3be77d28..774a1b9d9f 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt @@ -6,7 +6,7 @@ import com.tangem.data.quotes.store.QuotesStoreV2 import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk @@ -19,7 +19,7 @@ import java.math.BigDecimal /** [REDACTED_AUTHOR] */ -internal class DefaultSingleQuoteProducerTest { +internal class DefaultSingleQuoteStatusProducerTest { private val params = SingleQuoteProducer.Params( rawCurrencyId = CryptoCurrency.RawID(value = "BTC"), @@ -35,11 +35,11 @@ internal class DefaultSingleQuoteProducerTest { @Test fun `test that flow is mapped for network from params`() = runTest { - val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId) val storeQuote = flowOf( setOf( status, - Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), ), ) @@ -57,7 +57,7 @@ internal class DefaultSingleQuoteProducerTest { @Test fun `test that flow is updated if quote is updated`() = runTest { - val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) every { quotesStore.get() } returns storeQuote @@ -66,7 +66,7 @@ internal class DefaultSingleQuoteProducerTest { verify { quotesStore.get() } // first emit - val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId) storeQuote.emit(value = setOf(status)) val values1 = getEmittedValues(flow = actual) @@ -75,11 +75,13 @@ internal class DefaultSingleQuoteProducerTest { Truth.assertThat(values1).isEqualTo(listOf(status)) // second emit - val updatedStatus = Quote.Value( + val updatedStatus = QuoteStatus( rawCurrencyId = params.rawCurrencyId, - fiatRate = BigDecimal.ONE, - priceChange = BigDecimal.ZERO, - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ), ) storeQuote.emit(value = setOf(updatedStatus)) @@ -91,7 +93,7 @@ internal class DefaultSingleQuoteProducerTest { @Test fun `test that flow is filtered the same status`() = runTest { - val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) every { quotesStore.get() } returns storeQuote @@ -100,7 +102,7 @@ internal class DefaultSingleQuoteProducerTest { verify { quotesStore.get() } // first emit - val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId) storeQuote.emit(value = setOf(status)) val values1 = getEmittedValues(flow = actual) @@ -120,11 +122,13 @@ internal class DefaultSingleQuoteProducerTest { @Test fun `test if flow throws exception`() = runTest { val exception = IllegalStateException() - val status = Quote.Value( + val status = QuoteStatus( rawCurrencyId = params.rawCurrencyId, - fiatRate = BigDecimal.ONE, - priceChange = BigDecimal.ZERO, - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ), ) val innerFlow = MutableStateFlow(value = false) @@ -146,7 +150,7 @@ internal class DefaultSingleQuoteProducerTest { val values1 = getEmittedValues(flow = actual) Truth.assertThat(values1.size).isEqualTo(1) - val fallbackStatus = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + val fallbackStatus = QuoteStatus(rawCurrencyId = params.rawCurrencyId) Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus)) innerFlow.emit(value = true) @@ -160,7 +164,7 @@ internal class DefaultSingleQuoteProducerTest { fun `test if flow doesn't contain network from params`() = runTest { val storeFlow = flowOf( setOf( - Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), ), ) diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt index d5a517f5e2..b957e0624e 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt @@ -6,7 +6,7 @@ import com.tangem.common.test.data.quote.toDomain import com.tangem.common.test.datastore.MockStateDataStore import com.tangem.common.test.utils.getEmittedValues import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import kotlinx.coroutines.test.runTest import org.junit.Test @@ -17,7 +17,7 @@ import java.math.BigDecimal */ internal class QuotesStoreGetMethodTest { - private val runtimeStore = RuntimeSharedStore>() + private val runtimeStore = RuntimeSharedStore>() private val persistenceStore = MockStateDataStore(default = emptyMap()) private val store = DefaultQuotesStoreV2( @@ -32,7 +32,7 @@ internal class QuotesStoreGetMethodTest { val values = getEmittedValues(flow = actual) - Truth.assertThat(values).isEqualTo(emptyList>()) + Truth.assertThat(values).isEqualTo(emptyList>()) } @Test @@ -43,7 +43,7 @@ internal class QuotesStoreGetMethodTest { val values = getEmittedValues(flow = actual) - Truth.assertThat(values).isEqualTo(listOf(emptySet())) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) } @Test @@ -74,7 +74,7 @@ internal class QuotesStoreGetMethodTest { val actual = store.getAllSyncOrNull() - Truth.assertThat(actual).isEqualTo(emptySet()) + Truth.assertThat(actual).isEqualTo(emptySet()) } @Test diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt index 2452a66004..e60dd2bd09 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt @@ -7,7 +7,7 @@ import com.tangem.common.test.data.quote.toDomain import com.tangem.common.test.datastore.MockStateDataStore import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk @@ -23,7 +23,7 @@ internal class QuotesStoreInitializationTest { @Test fun `test initialization if cache store is empty`() = runTest { - val runtimeStore = RuntimeSharedStore>() + val runtimeStore = RuntimeSharedStore>() val persistenceStore: DataStore = mockk() every { persistenceStore.data } returns emptyFlow() @@ -39,7 +39,7 @@ internal class QuotesStoreInitializationTest { @Test fun `test initialization if cache store contains empty map`() = runTest { - val runtimeStore = RuntimeSharedStore>() + val runtimeStore = RuntimeSharedStore>() val persistenceStore = MockStateDataStore(default = emptyMap()) DefaultQuotesStoreV2( @@ -53,7 +53,7 @@ internal class QuotesStoreInitializationTest { @Test fun `test initialization if cache store is not empty`() = runTest { - val runtimeStore = RuntimeSharedStore>() + val runtimeStore = RuntimeSharedStore>() val persistenceStore = MockStateDataStore(default = emptyMap()) val btcQuote = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt index e935753dcf..d3224d5c48 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt @@ -7,7 +7,7 @@ import com.tangem.common.test.datastore.MockStateDataStore import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.test.runTest @@ -19,7 +19,7 @@ import java.math.BigDecimal */ internal class QuotesStoreUpdateMethodsTest { - private val runtimeStore = RuntimeSharedStore>() + private val runtimeStore = RuntimeSharedStore>() private val persistenceStore = MockStateDataStore(default = emptyMap()) private val store = DefaultQuotesStoreV2( @@ -37,8 +37,8 @@ internal class QuotesStoreUpdateMethodsTest { store.refresh(currenciesIds = currenciesIds) - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptySet()) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptySet()) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) } @Test @@ -50,10 +50,12 @@ internal class QuotesStoreUpdateMethodsTest { store.refresh(currenciesIds = setOf(quote.rawCurrencyId)) - val runtimeExpected = setOf(quote.copySealed(source = StatusSource.CACHE)) + val runtimeExpected = setOf( + quote.copy(value = quote.value.copySealed(source = StatusSource.CACHE)), + ) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) } @Test @@ -94,10 +96,12 @@ internal class QuotesStoreUpdateMethodsTest { store.storeError(currenciesIds = currenciesIds) - val runtimeExpected = currenciesIds.map(Quote::Empty).toSet() + val runtimeExpected = currenciesIds + .map { QuoteStatus(rawCurrencyId = it, value = QuoteStatus.Empty) } + .toSet() Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) } @Test @@ -107,7 +111,7 @@ internal class QuotesStoreUpdateMethodsTest { runtimeStore.store( value = setOf( status.toDomain(rawCurrencyId = "BTC", source = StatusSource.CACHE), - Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH"), value = QuoteStatus.Empty), ), ) @@ -120,10 +124,10 @@ internal class QuotesStoreUpdateMethodsTest { val runtimeExpected = setOf( status.toDomain(rawCurrencyId = "BTC", source = StatusSource.ONLY_CACHE), - Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH"), value = QuoteStatus.Empty), ) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) } } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt index d146df5090..f73cd4e58d 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -6,7 +6,7 @@ import arrow.core.toOption import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.single.SingleQuoteProducer import com.tangem.domain.quotes.single.SingleQuoteSupplier -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOf @@ -20,13 +20,13 @@ class GetCurrencyQuotesUseCase( currencyID: CryptoCurrency.ID, interval: PriceChangeInterval, refresh: Boolean, - ): Flow> { + ): Flow> { val rawId = currencyID.rawCurrencyId ?: return flowOf(None) return singleQuoteSupplier( params = SingleQuoteProducer.Params(rawCurrencyId = rawId), ) - .map { (it as? Quote.Value).toOption() } + .map { (it.value as? QuoteStatus.Data).toOption() } .catch { emit(None) } } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt index 4dc44a7590..fa6e95358c 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt @@ -6,7 +6,7 @@ import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.quotes.single.SingleQuoteProducer import com.tangem.domain.quotes.single.SingleQuoteSupplier -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.fold import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -31,12 +31,15 @@ class GetNFTPriceUseCase( assetId = nftAsset.id, ) - val quoteValue = quote as? Quote.Value - if (nftPrice !is NFTSalePrice.Value) { nftPrice } else { - nftPrice.copy(fiatValue = quoteValue?.fiatRate?.multiply(nftPrice.value)) + nftPrice.copy( + fiatValue = quote.fold( + onData = { fiatRate.multiply(nftPrice.value) }, + onEmpty = { null }, + ), + ) } } } diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt index 389cc866e3..aeb549ea8f 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt @@ -1,17 +1,17 @@ package com.tangem.domain.onramp.model import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus /** * Hot crypto currency * * @property cryptoCurrency crypto currency - * @property quote quote + * @property quoteStatus quote status * [REDACTED_AUTHOR] */ data class HotCryptoCurrency( val cryptoCurrency: CryptoCurrency, - val quote: Quote, + val quoteStatus: QuoteStatus, ) \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt index f9d517cd42..dd446b9f0a 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt @@ -1,7 +1,7 @@ package com.tangem.domain.quotes import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus /** * Quotes repository @@ -11,5 +11,5 @@ import com.tangem.domain.tokens.model.Quote interface QuotesRepositoryV2 { /** Get quotes by [currenciesIds] synchronously or null */ - suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? + suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? } \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt index c7fd024e28..4a8f30bccb 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt @@ -2,14 +2,14 @@ package com.tangem.domain.quotes.single import com.tangem.domain.core.flow.FlowProducer import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus /** * Producer of quote [CryptoCurrency.RawID] * [REDACTED_AUTHOR] */ -interface SingleQuoteProducer : FlowProducer { +interface SingleQuoteProducer : FlowProducer { data class Params(val rawCurrencyId: CryptoCurrency.RawID) diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt index e65dbb492d..dbbda17215 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt @@ -1,7 +1,7 @@ package com.tangem.domain.quotes.single import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus /** * Supplier of quote [SingleQuoteProducer.Params] @@ -14,4 +14,4 @@ import com.tangem.domain.tokens.model.Quote abstract class SingleQuoteSupplier( override val factory: SingleQuoteProducer.Factory, override val keyCreator: (SingleQuoteProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file +) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt deleted file mode 100644 index a92dfb0ab3..0000000000 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.domain.tokens.model - -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.currency.CryptoCurrency -import java.math.BigDecimal - -sealed interface Quote { - - val rawCurrencyId: CryptoCurrency.RawID - - fun copySealed(source: StatusSource): Quote { - return when (this) { - is Empty -> this - is Value -> copy(source = source) - } - } - - /** - * Represents unknown financial information for a specific cryptocurrency. - * - * @property rawCurrencyId The raw cryptocurrency ID. - */ - data class Empty(override val rawCurrencyId: CryptoCurrency.RawID) : Quote - - /** - * Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change. - * - * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. - * @property fiatRate The current fiat exchange rate for the cryptocurrency. - * @property priceChange The price change for the cryptocurrency. - * @property source source of data - */ - data class Value( - override val rawCurrencyId: CryptoCurrency.RawID, - val fiatRate: BigDecimal, - val priceChange: BigDecimal, - val source: StatusSource, - ) : Quote -} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/QuoteStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/QuoteStatus.kt new file mode 100644 index 0000000000..81fb8a8dd5 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/QuoteStatus.kt @@ -0,0 +1,69 @@ +package com.tangem.domain.tokens.model + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import java.math.BigDecimal + +/** + * Represents the status of a specific currency quote + * + * @property rawCurrencyId the unique identifier of the cryptocurrency for which the financial information is provided + * @property value the specific status value + */ +data class QuoteStatus(val rawCurrencyId: CryptoCurrency.RawID, val value: Value) { + + /** Constructor for creating the [Empty] status of a specific currency quote */ + constructor(rawCurrencyId: CryptoCurrency.RawID) : this(rawCurrencyId = rawCurrencyId, value = Empty) + + /** Represents the various possible statuses of a quote */ + sealed interface Value { + + /** Status source */ + val source: StatusSource + + fun copySealed(source: StatusSource): Value { + return when (this) { + is Empty -> this + is Data -> copy(source = source) + } + } + } + + /** Represents unknown financial information for a specific cryptocurrency */ + data object Empty : Value { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** + * Represents financial information for a specific cryptocurrency, including its fiat exchange rate and + * price change. + * + * @property source status source + * @property fiatRate the current fiat exchange rate for the cryptocurrency + * @property priceChange the price change for the cryptocurrency + */ + data class Data( + override val source: StatusSource, + val fiatRate: BigDecimal, + val priceChange: BigDecimal, + ) : Value +} + +/** Applies the given [function] if [QuoteStatus.Value] is [QuoteStatus.Data] or does nothing */ +inline fun QuoteStatus.mapData(function: QuoteStatus.Data.() -> Unit): QuoteStatus { + if (value is QuoteStatus.Data) value.function() + + return this +} + +/** + * Applies the given [onData] or [onEmpty] functions depending on [QuoteStatus.Data] or [QuoteStatus.Empty] + * + * @param T type of result + */ +inline fun QuoteStatus.fold(onData: QuoteStatus.Data.() -> T, onEmpty: QuoteStatus.Empty.() -> T): T { + return when (value) { + is QuoteStatus.Data -> value.onData() + is QuoteStatus.Empty -> value.onEmpty() + } +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt index 9c088e4fce..244197386c 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt @@ -6,7 +6,7 @@ package com.tangem.domain.tokens.model * @property id The unique identifier of the token. * @property networks List of networks associated with the token. * @property isAvailable Indicates whether this token is supported in Tangem app. - * @property quote Equivalent prices for the token. + * @property quoteStatus Equivalent prices for the token. * @property name The name of the token. * @property symbol The brief name of the token, e.g., "BTC". * @property iconUrl URL of the token's icon. @@ -15,7 +15,7 @@ data class Token( val id: String, val networks: List, val isAvailable: Boolean, - val quote: Quote?, + val quoteStatus: QuoteStatus?, val name: String, val symbol: String, val iconUrl: String, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 6e8d810651..44d9a9574c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -23,7 +23,7 @@ import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator @@ -52,7 +52,7 @@ abstract class BaseCurrencyStatusOperations( protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator(stakingRepository) - protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> + protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> protected abstract fun getNetworksStatuses( userWalletId: UserWalletId, @@ -120,7 +120,7 @@ abstract class BaseCurrencyStatusOperations( combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> currencyStatusProxyCreator.createCurrencyStatus( currency = currency, - maybeQuote = maybeQuote, + maybeQuoteStatus = maybeQuote, maybeNetworkStatus = maybeNetworkStatus, maybeYieldBalance = maybeYieldBalance, ) @@ -129,7 +129,7 @@ abstract class BaseCurrencyStatusOperations( combine(quoteFlow, statusFlow) { maybeQuote, maybeNetworkStatus -> currencyStatusProxyCreator.createCurrencyStatus( currency = currency, - maybeQuote = maybeQuote, + maybeQuoteStatus = maybeQuote, maybeNetworkStatus = maybeNetworkStatus, maybeYieldBalance = null, ) @@ -210,7 +210,7 @@ abstract class BaseCurrencyStatusOperations( return currencyStatusProxyCreator.createCurrencyStatus( currency = currency, - maybeQuote = quote, + maybeQuoteStatus = quote, maybeNetworkStatus = networkStatuses, maybeYieldBalance = yieldBalances, ) @@ -303,7 +303,7 @@ abstract class BaseCurrencyStatusOperations( return currencyStatusProxyCreator.createCurrencyStatus( currency = currency, - maybeQuote = quotes, + maybeQuoteStatus = quotes, maybeNetworkStatus = networkStatus, maybeYieldBalance = yieldBalances, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 0ba91d1225..e547b6549b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -31,7 +31,7 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.extractAddress @@ -121,7 +121,7 @@ class CachedCurrenciesStatusesOperations( val (networks, currenciesIds) = getIds(currencies) fun createCurrenciesStatuses( - maybeQuotes: Either>?, + maybeQuotes: Either>?, maybeNetworkStatuses: Either>?, maybeYieldBalances: Either?, isUpdating: Boolean, @@ -212,7 +212,7 @@ class CachedCurrenciesStatusesOperations( private fun createCurrenciesStatuses( currencies: NonEmptyList, - maybeQuotes: Either>?, + maybeQuotes: Either>?, maybeNetworkStatuses: Either>?, maybeYieldBalances: Either?, isUpdating: Boolean, @@ -238,7 +238,7 @@ class CachedCurrenciesStatusesOperations( val currencyStatus = currencyStatusProxyCreator.createCurrencyStatus( currency = currency, - quote = quote, + quoteStatus = quote, networkStatus = networkStatus, yieldBalance = yieldBalance, ignoreQuote = quotesRetrievingFailed, @@ -272,7 +272,7 @@ class CachedCurrenciesStatusesOperations( .distinctUntilChanged() } - private fun getQuotes(tokensIds: NonEmptySet): Flow>> { + private fun getQuotes(tokensIds: NonEmptySet): Flow>> { return getQuotesUpdates( rawCurrencyIds = tokensIds.mapNotNullTo( destination = hashSetOf(), @@ -281,11 +281,11 @@ class CachedCurrenciesStatusesOperations( ) } - override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { + override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { return singleQuoteSupplier( params = SingleQuoteProducer.Params(rawCurrencyId = id), ) - .map>> { setOf(it).right() } + .map>> { setOf(it).right() } .distinctUntilChanged() } @@ -353,9 +353,11 @@ class CachedCurrenciesStatusesOperations( } // temporary code because token list is built using networks list - private fun getQuotesUpdates(rawCurrencyIds: Set): EitherFlow> { + private fun getQuotesUpdates( + rawCurrencyIds: Set, + ): EitherFlow> { return channelFlow { - val state = MutableStateFlow(emptySet()) + val state = MutableStateFlow(emptySet()) rawCurrencyIds.onEach { launch { @@ -375,7 +377,7 @@ class CachedCurrenciesStatusesOperations( .onEach(::send) .launchIn(scope = this) } - .map, Either>> { it.right() } + .map, Either>> { it.right() } .distinctUntilChanged() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index f890486cc2..4e065d68c5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -5,28 +5,22 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import java.math.BigDecimal internal class CurrencyStatusOperations( private val currency: CryptoCurrency, - private val quote: Quote?, + private val quoteStatus: QuoteStatus?, private val networkStatus: NetworkStatus?, private val yieldBalance: YieldBalance?, private val ignoreQuote: Boolean, ) { - private val Quote?.fiatRate: BigDecimal? - get() = when (this) { - is Quote.Value -> this.fiatRate - is Quote.Empty, null -> null - } + private val QuoteStatus?.fiatRate: BigDecimal? + get() = (this?.value as? QuoteStatus.Data)?.fiatRate - private val Quote?.priceChange: BigDecimal? - get() = when (this) { - is Quote.Value -> this.priceChange - is Quote.Empty, null -> null - } + private val QuoteStatus?.priceChange: BigDecimal? + get() = (this?.value as? QuoteStatus.Data)?.priceChange fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus()) @@ -41,12 +35,12 @@ internal class CurrencyStatusOperations( } private fun createMissedDerivationStatus(): CryptoCurrencyStatus.MissedDerivation = - CryptoCurrencyStatus.MissedDerivation(priceChange = quote?.priceChange, fiatRate = quote?.fiatRate) + CryptoCurrencyStatus.MissedDerivation(priceChange = quoteStatus?.priceChange, fiatRate = quoteStatus?.fiatRate) private fun createUnreachableStatus(status: NetworkStatus.Unreachable): CryptoCurrencyStatus.Unreachable { return CryptoCurrencyStatus.Unreachable( - priceChange = quote?.priceChange, - fiatRate = quote?.fiatRate, + priceChange = quoteStatus?.priceChange, + fiatRate = quoteStatus?.fiatRate, networkAddress = status.address, ) } @@ -54,13 +48,13 @@ internal class CurrencyStatusOperations( private fun createNoAccountStatus(status: NetworkStatus.NoAccount): CryptoCurrencyStatus.NoAccount { return CryptoCurrencyStatus.NoAccount( amountToCreateAccount = status.amountToCreateAccount, - fiatAmount = if (quote == null) null else BigDecimal.ZERO, - priceChange = quote?.priceChange, - fiatRate = quote?.fiatRate, + fiatAmount = if (quoteStatus == null) null else BigDecimal.ZERO, + priceChange = quoteStatus?.priceChange, + fiatRate = quoteStatus?.fiatRate, networkAddress = status.address, sources = CryptoCurrencyStatus.Sources( networkSource = status.source, - quoteSource = (quote as? Quote.Value)?.source ?: StatusSource.ACTUAL, + quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL, ), ) } @@ -75,7 +69,10 @@ internal class CurrencyStatusOperations( return CryptoCurrencyStatus.Loading } is NetworkStatus.Amount.NotFound -> { - return CryptoCurrencyStatus.NoAmount(priceChange = quote?.priceChange, fiatRate = quote?.fiatRate) + return CryptoCurrencyStatus.NoAmount( + priceChange = quoteStatus?.priceChange, + fiatRate = quoteStatus?.fiatRate, + ) } is NetworkStatus.Amount.Loaded -> amount.value } @@ -96,24 +93,27 @@ internal class CurrencyStatusOperations( } else { null } + + val quoteValue = quoteStatus?.value + // order is important for correct total balance calculation return when { currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, - fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), - fiatRate = quote?.fiatRate, - priceChange = quote?.priceChange, + fiatAmount = calculateFiatAmountOrNull(amount, quoteStatus?.fiatRate), + fiatRate = quoteStatus?.fiatRate, + priceChange = quoteStatus?.priceChange, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = networkStatusValue.address, yieldBalance = currentYieldBalance, sources = CryptoCurrencyStatus.Sources( networkSource = networkStatusValue.source, - quoteSource = (quote as? Quote.Value)?.source ?: StatusSource.ACTUAL, + quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL, yieldBalanceSource = currentYieldBalance?.source ?: StatusSource.ACTUAL, ), ) - quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote( + quoteValue is QuoteStatus.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote( amount = amount, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, @@ -121,22 +121,22 @@ internal class CurrencyStatusOperations( yieldBalance = currentYieldBalance, sources = CryptoCurrencyStatus.Sources( networkSource = networkStatusValue.source, - quoteSource = (quote as? Quote.Value)?.source ?: StatusSource.ACTUAL, + quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL, yieldBalanceSource = currentYieldBalance?.source ?: StatusSource.ACTUAL, ), ) - quote is Quote.Value -> CryptoCurrencyStatus.Loaded( + quoteValue is QuoteStatus.Data -> CryptoCurrencyStatus.Loaded( amount = amount, - fiatAmount = calculateFiatAmount(amount, quote.fiatRate), - fiatRate = quote.fiatRate, - priceChange = quote.priceChange, + fiatAmount = calculateFiatAmount(amount, quoteValue.fiatRate), + fiatRate = quoteValue.fiatRate, + priceChange = quoteValue.priceChange, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = networkStatusValue.address, yieldBalance = currentYieldBalance, sources = CryptoCurrencyStatus.Sources( networkSource = networkStatusValue.source, - quoteSource = quote.source, + quoteSource = quoteValue.source, yieldBalanceSource = currentYieldBalance?.source ?: StatusSource.ACTUAL, ), ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt index 337ffb44db..6ed4a05e88 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -10,7 +10,7 @@ import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.operations.CurrencyStatusOperations @@ -27,14 +27,14 @@ class CurrencyStatusProxyCreator( fun createCurrencyStatus( currency: CryptoCurrency, - maybeQuote: Either, + maybeQuoteStatus: Either, maybeNetworkStatus: Either, maybeYieldBalance: Either?, ): Either = either { var quoteRetrievingFailed = false val networkStatus = maybeNetworkStatus.bind() - val quote = arrow.core.raise.recover({ maybeQuote.bind() }) { + val quote = arrow.core.raise.recover({ maybeQuoteStatus.bind() }) { quoteRetrievingFailed = true null } @@ -42,7 +42,7 @@ class CurrencyStatusProxyCreator( createCurrencyStatus( currency = currency, - quote = quote, + quoteStatus = quote, networkStatus = networkStatus, ignoreQuote = quoteRetrievingFailed, yieldBalance = yieldBalance, @@ -51,14 +51,14 @@ class CurrencyStatusProxyCreator( fun createCurrenciesStatuses( currencies: NonEmptyList, - maybeQuotes: Either>?, + maybeQuotes: Either>?, maybeNetworkStatuses: Either>?, maybeYieldBalances: Either?, ): Either> = either { var quotesRetrievingFailed = false val networksStatuses = maybeNetworkStatuses?.bind()?.toNonEmptySetOrNull() - val quotes: Set? = maybeQuotes?.fold( + val quoteStatuses: Set? = maybeQuotes?.fold( ifLeft = { quotesRetrievingFailed = true null @@ -74,7 +74,7 @@ class CurrencyStatusProxyCreator( val yieldBalances = maybeYieldBalances?.getOrNull() currencies.map { currency -> - val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } + val quote = quoteStatuses?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } val address = extractAddress(networkStatus) @@ -89,7 +89,7 @@ class CurrencyStatusProxyCreator( } createCurrencyStatus( currency = currency, - quote = quote, + quoteStatus = quote, networkStatus = networkStatus, ignoreQuote = quotesRetrievingFailed, yieldBalance = yieldBalance, @@ -99,14 +99,14 @@ class CurrencyStatusProxyCreator( fun createCurrencyStatus( currency: CryptoCurrency, - quote: Quote?, + quoteStatus: QuoteStatus?, networkStatus: NetworkStatus?, ignoreQuote: Boolean, yieldBalance: YieldBalance?, ): CryptoCurrencyStatus { val currencyStatusOperations = CurrencyStatusOperations( currency = currency, - quote = quote, + quoteStatus = quoteStatus, networkStatus = networkStatus, ignoreQuote = ignoreQuote, yieldBalance = yieldBalance, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index 0d786ca54c..ea2555d8c5 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -3,83 +3,103 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptySetOf import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") internal object MockQuotes { - val quote1 = Quote.Value( + val quote1 = QuoteStatus( rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!, - fiatRate = BigDecimal("1.23"), - priceChange = BigDecimal("0.01"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("1.23"), + priceChange = BigDecimal("0.01"), + source = StatusSource.ACTUAL, + ), ) - val quote2 = Quote.Value( + val quote2 = QuoteStatus( rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!, - fiatRate = BigDecimal("2.34"), - priceChange = BigDecimal("-0.02"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("2.34"), + priceChange = BigDecimal("-0.02"), + source = StatusSource.ACTUAL, + ), ) - val quote3 = Quote.Value( + val quote3 = QuoteStatus( rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!, - fiatRate = BigDecimal("3.45"), - priceChange = BigDecimal("0.03"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("3.45"), + priceChange = BigDecimal("0.03"), + source = StatusSource.ACTUAL, + ), ) - val quote4 = Quote.Value( + val quote4 = QuoteStatus( rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!, - fiatRate = BigDecimal("4.56"), - priceChange = BigDecimal("-0.04"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("4.56"), + priceChange = BigDecimal("-0.04"), + source = StatusSource.ACTUAL, + ), ) - val quote5 = Quote.Value( + val quote5 = QuoteStatus( rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!, - fiatRate = BigDecimal("5.67"), - priceChange = BigDecimal("0.05"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("5.67"), + priceChange = BigDecimal("0.05"), + source = StatusSource.ACTUAL, + ), ) - val quote6 = Quote.Value( + val quote6 = QuoteStatus( rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!, - fiatRate = BigDecimal("6.78"), - priceChange = BigDecimal("-0.06"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("6.78"), + priceChange = BigDecimal("-0.06"), + source = StatusSource.ACTUAL, + ), ) - val quote7 = Quote.Value( + val quote7 = QuoteStatus( rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!, - fiatRate = BigDecimal("7.89"), - priceChange = BigDecimal("0.07"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("7.89"), + priceChange = BigDecimal("0.07"), + source = StatusSource.ACTUAL, + ), ) - val quote8 = Quote.Value( + val quote8 = QuoteStatus( rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!, - fiatRate = BigDecimal("8.90"), - priceChange = BigDecimal("-0.08"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("8.90"), + priceChange = BigDecimal("-0.08"), + source = StatusSource.ACTUAL, + ), ) - val quote9 = Quote.Value( + val quote9 = QuoteStatus( rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!, - fiatRate = BigDecimal("9.01"), - priceChange = BigDecimal("0.09"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("9.01"), + priceChange = BigDecimal("0.09"), + source = StatusSource.ACTUAL, + ), ) - val quote10 = Quote.Value( + val quote10 = QuoteStatus( rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!, - fiatRate = BigDecimal("10.12"), - priceChange = BigDecimal("-0.10"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("10.12"), + priceChange = BigDecimal("-0.10"), + source = StatusSource.ACTUAL, + ), ) - val quote11 = Quote.Empty(CryptoCurrency.RawID("null")) + val quote11 = QuoteStatus(rawCurrencyId = CryptoCurrency.RawID("null"), value = QuoteStatus.Empty) val quotes = nonEmptySetOf( quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index c3f573af01..3a0e07cb0e 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -4,7 +4,8 @@ import arrow.core.nonEmptyListOf import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.tokens.model.fold import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") @@ -13,8 +14,8 @@ internal object MockTokensStates { val tokenState1 = CryptoCurrencyStatus( currency = MockTokens.token1, value = CryptoCurrencyStatus.Unreachable( - priceChange = MockQuotes.quote1.priceChange, - fiatRate = MockQuotes.quote1.fiatRate, + priceChange = MockQuotes.quote1.getPriceChange(), + fiatRate = MockQuotes.quote1.getFiatRate(), networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), @@ -24,8 +25,8 @@ internal object MockTokensStates { val tokenState2 = CryptoCurrencyStatus( currency = MockTokens.token2, value = CryptoCurrencyStatus.Unreachable( - priceChange = MockQuotes.quote2.priceChange, - fiatRate = MockQuotes.quote2.fiatRate, + priceChange = MockQuotes.quote2.getPriceChange(), + fiatRate = MockQuotes.quote2.getFiatRate(), networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), @@ -35,8 +36,8 @@ internal object MockTokensStates { val tokenState3 = CryptoCurrencyStatus( currency = MockTokens.token3, value = CryptoCurrencyStatus.Unreachable( - priceChange = MockQuotes.quote3.priceChange, - fiatRate = MockQuotes.quote3.fiatRate, + priceChange = MockQuotes.quote3.getPriceChange(), + fiatRate = MockQuotes.quote3.getFiatRate(), networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), @@ -46,24 +47,24 @@ internal object MockTokensStates { val tokenState4 = CryptoCurrencyStatus( currency = MockTokens.token4, value = CryptoCurrencyStatus.MissedDerivation( - priceChange = MockQuotes.quote4.priceChange, - fiatRate = MockQuotes.quote4.fiatRate, + priceChange = MockQuotes.quote4.getPriceChange(), + fiatRate = MockQuotes.quote4.getFiatRate(), ), ) val tokenState5 = CryptoCurrencyStatus( currency = MockTokens.token5, value = CryptoCurrencyStatus.MissedDerivation( - priceChange = MockQuotes.quote5.priceChange, - fiatRate = MockQuotes.quote5.fiatRate, + priceChange = MockQuotes.quote5.getPriceChange(), + fiatRate = MockQuotes.quote5.getFiatRate(), ), ) val tokenState6 = CryptoCurrencyStatus( currency = MockTokens.token6, value = CryptoCurrencyStatus.MissedDerivation( - priceChange = MockQuotes.quote6.priceChange, - fiatRate = MockQuotes.quote6.fiatRate, + priceChange = MockQuotes.quote6.getPriceChange(), + fiatRate = MockQuotes.quote6.getFiatRate(), ), ) @@ -71,8 +72,8 @@ internal object MockTokensStates { currency = MockTokens.token7, value = CryptoCurrencyStatus.NoAccount( fiatAmount = BigDecimal.ZERO, - priceChange = MockQuotes.quote7.priceChange, - fiatRate = MockQuotes.quote7.fiatRate, + priceChange = MockQuotes.quote7.getPriceChange(), + fiatRate = MockQuotes.quote7.getFiatRate(), amountToCreateAccount = MockNetworks.amountToCreateAccount, networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), @@ -85,8 +86,8 @@ internal object MockTokensStates { currency = MockTokens.token8, value = CryptoCurrencyStatus.NoAccount( fiatAmount = BigDecimal.ZERO, - priceChange = MockQuotes.quote8.priceChange, - fiatRate = MockQuotes.quote8.fiatRate, + priceChange = MockQuotes.quote8.getPriceChange(), + fiatRate = MockQuotes.quote8.getFiatRate(), amountToCreateAccount = MockNetworks.amountToCreateAccount, networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), @@ -99,8 +100,8 @@ internal object MockTokensStates { currency = MockTokens.token9, value = CryptoCurrencyStatus.NoAccount( fiatAmount = BigDecimal.ZERO, - priceChange = MockQuotes.quote9.priceChange, - fiatRate = MockQuotes.quote9.fiatRate, + priceChange = MockQuotes.quote9.getPriceChange(), + fiatRate = MockQuotes.quote9.getFiatRate(), amountToCreateAccount = MockNetworks.amountToCreateAccount, networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), @@ -113,8 +114,8 @@ internal object MockTokensStates { currency = MockTokens.token10, value = CryptoCurrencyStatus.NoAccount( fiatAmount = BigDecimal.ZERO, - priceChange = MockQuotes.quote10.priceChange, - fiatRate = MockQuotes.quote10.fiatRate, + priceChange = MockQuotes.quote10.getPriceChange(), + fiatRate = MockQuotes.quote10.getFiatRate(), amountToCreateAccount = MockNetworks.amountToCreateAccount, networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), @@ -145,8 +146,8 @@ internal object MockTokensStates { )?.value ?: BigDecimal.ZERO val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId } - val value = when (quote) { - is Quote.Empty -> CryptoCurrencyStatus.NoQuote( + val value = when (val value = quote.value) { + is QuoteStatus.Empty -> CryptoCurrencyStatus.NoQuote( amount = status.value.amount!!, pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, @@ -156,11 +157,11 @@ internal object MockTokensStates { yieldBalance = null, sources = CryptoCurrencyStatus.Sources(), ) - is Quote.Value -> CryptoCurrencyStatus.Loaded( + is QuoteStatus.Data -> CryptoCurrencyStatus.Loaded( amount = amount, - fiatAmount = amount * quote.fiatRate, - fiatRate = quote.fiatRate, - priceChange = quote.priceChange, + fiatAmount = amount * value.fiatRate, + fiatRate = value.fiatRate, + priceChange = value.priceChange, pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, @@ -187,4 +188,7 @@ internal object MockTokensStates { ), ) } + + private fun QuoteStatus.getPriceChange(): BigDecimal? = fold(onData = { priceChange }, onEmpty = { null }) + private fun QuoteStatus.getFiatRate(): BigDecimal? = fold(onData = { fiatRate }, onEmpty = { null }) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt index a6e5e646ce..887c06034a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt @@ -10,7 +10,8 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.onramp.model.HotCryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.tokens.model.fold import com.tangem.utils.converter.Converter import java.math.BigDecimal @@ -30,7 +31,7 @@ internal class HotTokenItemStateConverter( id = value.cryptoCurrency.id.value, iconState = CryptoCurrencyToIconStateConverter().convert(value.cryptoCurrency), titleState = TokenItemState.TitleState.Content(text = stringReference(value.cryptoCurrency.name)), - subtitleState = value.quote.getCryptoPriceState(appCurrency), + subtitleState = value.quoteStatus.getCryptoPriceState(appCurrency), fiatAmountState = null, subtitle2State = null, onItemClick = onItemClick.let { onItemClick -> { onItemClick(it, value) } }, @@ -38,17 +39,17 @@ internal class HotTokenItemStateConverter( ) } - private fun Quote.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState { - return when (this) { - is Quote.Empty -> TokenItemState.SubtitleState.Unknown - is Quote.Value -> { + private fun QuoteStatus.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState { + return fold( + onData = { TokenItemState.SubtitleState.CryptoPriceContent( price = fiatRate.getFormattedCryptoPrice(appCurrency), priceChangePercent = priceChange.format { percent() }, type = priceChange.getPriceChangeType(), ) - } - } + }, + onEmpty = { TokenItemState.SubtitleState.Unknown }, + ) } private fun BigDecimal.getFormattedCryptoPrice(appCurrency: AppCurrency): String { 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 410639462f..672cd819f8 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 @@ -25,7 +25,7 @@ import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository @@ -1034,7 +1034,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val rates = getQuotes(fromToken.currency.id) val fromTokenSwapInfo = TokenSwapInfo( tokenAmount = amount, - amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + amountFiat = rates[fromToken.currency.id]?.multiply(amount.value) ?: BigDecimal.ZERO, cryptoCurrencyStatus = fromToken, ) @@ -1130,7 +1130,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( else -> getNativeToken(networkId = fromToken.network.backendId).id } val rates = getQuotes(feeCurrencyId) - return rates[feeCurrencyId]?.fiatRate?.let { rate -> + return rates[feeCurrencyId]?.let { rate -> fees.map { fee -> rate.multiply(fee).format { fiat( @@ -1233,7 +1233,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val rates = getQuotes(fromToken.currency.id) val fromTokenSwapInfo = TokenSwapInfo( tokenAmount = amount, - amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + amountFiat = rates[fromToken.currency.id]?.multiply(amount.value) ?: BigDecimal.ZERO, cryptoCurrencyStatus = fromToken, ) @@ -1307,20 +1307,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( fromTokenInfo = TokenSwapInfo( tokenAmount = fromTokenAmount, cryptoCurrencyStatus = fromTokenStatus, - amountFiat = rates[fromToken.id]?.fiatRate?.multiply(fromTokenAmount.value) + amountFiat = rates[fromToken.id]?.multiply(fromTokenAmount.value) ?: BigDecimal.ZERO, ), toTokenInfo = TokenSwapInfo( tokenAmount = toTokenAmount, cryptoCurrencyStatus = toTokenStatus, - amountFiat = rates[toToken.id]?.fiatRate?.multiply(toTokenAmount.value) + amountFiat = rates[toToken.id]?.multiply(toTokenAmount.value) ?: BigDecimal.ZERO, ), priceImpact = calculatePriceImpact( fromTokenAmount = fromTokenAmount.value, - fromRate = rates[fromToken.id]?.fiatRate?.toDouble() ?: 0.0, + fromRate = rates[fromToken.id]?.toDouble() ?: 0.0, toTokenAmount = toTokenAmount.value, - toRate = rates[toToken.id]?.fiatRate?.toDouble() ?: 0.0, + toRate = rates[toToken.id]?.toDouble() ?: 0.0, ), swapDataModel = swapData, txFee = txFeeState, @@ -1795,18 +1795,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( return PriceImpact.Value(value) } - private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { + private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { val set = ids.mapNotNull { it.rawCurrencyId } .toSet() .getQuotesOrEmpty() - .filterIsInstance() return ids - .mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } } + .mapNotNull { id -> + set.find { it.rawCurrencyId == id.rawCurrencyId && it.value is QuoteStatus.Data } + ?.let { id to (it.value as QuoteStatus.Data).fiatRate } + } .toMap() } - private suspend fun Set.getQuotesOrEmpty(): Set { + private suspend fun Set.getQuotesOrEmpty(): Set { return runCatching { quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = this) } .getOrNull() .orEmpty() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index f02dc11cf4..4c5cb72a5b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -17,8 +17,9 @@ import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent +import com.tangem.domain.tokens.model.mapData import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatus.Companion.isFailed import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel @@ -57,7 +58,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( fun convert( savedTransactions: List, - quotes: Set, + quoteStatuses: Set, ): PersistentList { val result = mutableListOf() @@ -73,12 +74,17 @@ internal class TokenDetailsSwapTransactionsStateConverter( val fromAmount = transaction.fromCryptoAmount var toFiatAmount: BigDecimal? = null var fromFiatAmount: BigDecimal? = null - quotes.forEach { quote -> - if (quote is Quote.Value && quote.rawCurrencyId == toCryptoCurrencyRawId) { - toFiatAmount = quote.fiatRate.multiply(toAmount) + quoteStatuses.forEach { quote -> + quote.mapData { + if (quote.rawCurrencyId == toCryptoCurrencyRawId) { + toFiatAmount = fiatRate.multiply(toAmount) + } } - if (quote is Quote.Value && quote.rawCurrencyId == fromCryptoCurrencyRawId) { - fromFiatAmount = quote.fiatRate.multiply(fromAmount) + + quote.mapData { + if (quote.rawCurrencyId == fromCryptoCurrencyRawId) { + fromFiatAmount = fiatRate.multiply(fromAmount) + } } } val statusModel = transaction.status diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index 0a75b5e7d7..13c9843f25 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -8,7 +8,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.QuotesRepositoryV2 import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.SwapTransactionRepository @@ -66,7 +66,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( getExchangeStatusState( savedTransactions = savedTransactions, - quotes = quotes, + quoteStatuses = quotes, ) } } @@ -155,7 +155,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private fun getExchangeStatusState( savedTransactions: List?, - quotes: Set, + quoteStatuses: Set, ): PersistentList { if (savedTransactions == null) { return persistentListOf() @@ -163,7 +163,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( return swapTransactionsStateConverter.convert( savedTransactions = savedTransactions, - quotes = quotes, + quoteStatuses = quoteStatuses, ) } @@ -187,7 +187,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( } } - private suspend fun Set.getQuotesOrEmpty(): Set { + private suspend fun Set.getQuotesOrEmpty(): Set { val rawIds = mapNotNull { it.rawCurrencyId }.toSet() return runCatching { quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = rawIds) } From 90c61c73121a7164bbe589ac1c7278a4ffd90b81 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Jun 2025 09:40:37 +0300 Subject: [PATCH 05/99] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7d4da9d5f9..31f29bcef0 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1079" +tangemBlockchainSdk = "releases-5.25.0-1082" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-480" +tangemCardSdk = "releases-5.25.0-481" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From af243495bd79999b360c6c6da212a16ebdb78a21 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Jun 2025 18:50:05 +0800 Subject: [PATCH 06/99] Updated on 2026-08-14 --- .../pair/AssociateNetworksDelegate.kt | 4 +-- .../pair/DefaultWcPairUseCase.kt | 4 +-- .../walletconnect/pair/WcPairSdkDelegate.kt | 29 +++++++++++++++---- .../walletconnect/DefaultWcPairUseCaseTest.kt | 6 ++-- .../domain/walletconnect/model/WcPairError.kt | 22 ++++++++------ .../connections/model/WcPairModel.kt | 7 +++-- 6 files changed, 47 insertions(+), 25 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt index 7208eea4c4..808d399dab 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt @@ -28,7 +28,7 @@ internal class AssociateNetworksDelegate( } } - @Throws(WcPairError.UnsupportedNetworks::class) + @Throws(WcPairError.UnsupportedBlockchains::class) suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() @@ -74,7 +74,7 @@ internal class AssociateNetworksDelegate( notAdded.add(wcNetwork) } } - if (unknownRequired.isNotEmpty()) throw WcPairError.UnsupportedNetworks(unknownRequired) + if (unknownRequired.isNotEmpty()) throw WcPairError.UnsupportedBlockchains(unknownRequired) return ProposalNetwork( wallet = wallet, missingRequired = missingRequired, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index fc52444c3b..77b7c18e93 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -56,7 +56,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( // check unsupported dApps, just local constant for now, finish if unsupported if (sdkSessionProposal.name in unsupportedDApps) { Timber.tag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}") - val error = WcPairState.Error(WcPairError.UnsupportedDApp) + val error = WcPairState.Error(WcPairError.UnsupportedDomain) emit(error) return@flow } @@ -104,7 +104,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( ) newSession }.onLeft { - analytics.send(WcAnalyticEvents.DAppConnectionFailed(it.message)) + analytics.send(WcAnalyticEvents.DAppConnectionFailed(it.code)) Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}") } emit(WcPairState.Approving.Result(sessionForApprove, either)) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt index 54f931d604..1a72488035 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -27,7 +27,11 @@ internal class WcPairSdkDelegate : WcSdkObserver { val pairCall = async { sdkPair(url) } val proposal = async { proposalCallback() } - pairCall.await().map { proposal.await() } + pairCall.await().onLeft { + proposal.cancel() + return@coroutineScope it.left() + } + proposal.await().right() } suspend fun approve( @@ -40,11 +44,14 @@ internal class WcPairSdkDelegate : WcSdkObserver { val approveCall = async { sdkApprove(sessionApprove) } val approveCallback = async { approveCallback() } approveCall.await() - .onLeft { return@coroutineScope it.left() } + .onLeft { + approveCallback.cancel() + return@coroutineScope it.left() + } when (val result = approveCallback.await()) { is Wallet.Model.SettledSessionResponse.Result -> result.right() is Wallet.Model.SettledSessionResponse.Error -> - WcPairError.ExternalApprovalError(result.errorMessage).left() + WcPairError.ApprovalFailed(result.errorMessage).left() } } @@ -77,7 +84,7 @@ internal class WcPairSdkDelegate : WcSdkObserver { WalletKit.approveSession( params = sessionApprove, onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.toPairError()) }, + onError = { continuation.resume(it.throwable.toApproveError()) }, ) } } @@ -87,10 +94,20 @@ internal class WcPairSdkDelegate : WcSdkObserver { WalletKit.pair( params = Wallet.Params.Pair(uri), onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.toPairError()) }, + onError = { continuation.resume(it.throwable.toPairError().left()) }, ) } } - private fun Throwable.toPairError() = WcPairError.ExternalApprovalError(this.localizedMessage.orEmpty()).left() + private fun Throwable.toPairError() = when { + pairingExpiredMessages.any { message.orEmpty().contains(it) } -> WcPairError.UriAlreadyUsed(message.orEmpty()) + else -> WcPairError.PairingFailed(this.localizedMessage.orEmpty()) + } + + private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left() + + companion object { + // com.reown.android.pairing.engine.domain.PairingEngine.pair + private val pairingExpiredMessages = listOf("Pairing URI expired", "Pairing expired") + } } \ No newline at end of file diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index bd8e58f43b..e493e6b602 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -196,7 +196,7 @@ internal class DefaultWcPairUseCaseTest { @Test fun `success pair and reject unsupported dApp`() = runTest { coEvery { sdkDelegate.pair(url) } returns unsupportedSdkProposal.right() - val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDApp) + val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDomain) val useCase = useCaseFactory() useCase.invoke().test { @@ -211,7 +211,7 @@ internal class DefaultWcPairUseCaseTest { @Test fun `complete on pair error`() = runTest { - val error = WcPairError.ExternalApprovalError("error") + val error = WcPairError.PairingFailed("error") coEvery { sdkDelegate.pair(url) } returns error.left() val errorState = WcPairState.Error(error) @@ -229,7 +229,7 @@ internal class DefaultWcPairUseCaseTest { @Test fun `complete on approve error`() = runTest { val approveLoading = WcPairState.Approving.Loading(sessionForApprove) - val error = WcPairError.ExternalApprovalError("error").left() + val error = WcPairError.ApprovalFailed("error").left() coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.approve(sdkApprove) } returns error coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt index 5a060eaaba..005daafedc 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt @@ -1,15 +1,19 @@ package com.tangem.domain.walletconnect.model -sealed class WcPairError(override val message: String) : Exception(message) { +sealed class WcPairError( + val code: String, + override val message: String = "", +) : Exception() { - data object UnsupportedDApp : WcPairError("UnsupportedDApp") - data class UnsupportedNetworks( - val chains: Set, - ) : WcPairError("ApprovalErrorMissingNetworks") - - data class ExternalApprovalError( - override val message: String, - ) : WcPairError("ExternalApprovalError") + data class UriAlreadyUsed(override val message: String) : WcPairError("107 001 001") + data class PairingFailed(override val message: String) : WcPairError("107 001 002") + data object InvalidDomainURL : WcPairError("107 001 003") + data object UnsupportedDomain : WcPairError("107 001 004") + data class UnsupportedBlockchains(val chains: Set) : WcPairError("107 001 005") + data object InvalidConnectionRequest : WcPairError("107 002 001") + data object ProposalExpired : WcPairError("107 002 002") + data class ApprovalFailed(override val message: String) : WcPairError("107 002 003") + data object RejectionFailed : WcPairError("107 002 004") data class Unknown(override val message: String) : WcPairError(message) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 0167da0484..08a2dbadc7 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.models.network.Network +import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal @@ -32,7 +33,6 @@ import com.tangem.features.walletconnect.connections.model.transformers.WcDAppVe import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* -import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate @@ -78,6 +78,7 @@ internal class WcPairModel @Inject constructor( } private fun loadDAppInfo() { + fun errorToast(error: WcPairError) = messageSender.send(ToastMessage(message = stringReference(error.message))) wcPairUseCase() .onEach { pairState -> when (pairState) { @@ -88,14 +89,14 @@ internal class WcPairModel @Inject constructor( appInfoUiState.transformerUpdate( WcConnectButtonProgressTransformer(showProgress = false), ) + pairState.result.onLeft(::errorToast) router.pop() } is WcPairState.Error -> { appInfoUiState.transformerUpdate( WcConnectButtonProgressTransformer(showProgress = false), ) - messageSender.send(ToastMessage(message = stringReference(pairState.error.message))) - Timber.e(pairState.error) + errorToast(pairState.error) } is WcPairState.Loading -> appInfoUiState.update { createLoadingState() } is WcPairState.Proposal -> { From 6ad1b0fe5b21f27f8386a2d097aebef71548d189 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Jun 2025 15:32:54 +0300 Subject: [PATCH 07/99] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 34c29de01a..04c6f4b039 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -3,7 +3,7 @@ [versions] # region Classpath -androidGradlePlugin = "8.2.2" +androidGradlePlugin = "8.10.1" firebaseCrashlytics = "3.0.1" googleServices = "4.4.1" kotlin = "2.1.10" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f5e4b2a14c..911e41ca23 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Feb 13 17:04:37 MSK 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From bfd371bc33d67ae36fd32d20d4d6dea6e75d2c89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Jun 2025 13:49:09 +0400 Subject: [PATCH 08/99] Updated on 2026-08-14 --- .../store/DefaultNetworksStatusesStore.kt | 2 +- data/quotes/build.gradle.kts | 7 +- .../data/quotes/DefaultQuotesRepositoryV2.kt | 4 +- .../tangem/data/quotes/di/QuotesDataModule.kt | 8 +- .../quotes/multi/DefaultMultiQuoteFetcher.kt | 12 +- .../quotes/multi/DefaultMultiQuoteUpdater.kt | 4 +- .../single/DefaultSingleQuoteProducer.kt | 4 +- ...oreV2.kt => DefaultQuotesStatusesStore.kt} | 63 +- .../data/quotes/store/QuotesStatusesStore.kt | 46 ++ .../quotes/store/QuotesStatusesStoreExt.kt | 22 + .../tangem/data/quotes/store/QuotesStoreV2.kt | 25 - .../DefaultMultiQuoteStatusFetcherTest.kt | 42 +- .../DefaultMultiQuoteStatusUpdaterTest.kt | 4 +- .../DefaultSingleQuoteStatusFetcherTest.kt | 34 +- .../DefaultSingleQuoteStatusProducerTest.kt | 4 +- .../store/QuotesStatusesStoreExtTest.kt | 133 +++++ .../quotes/store/QuotesStatusesStoreTest.kt | 565 ++++++++++++++++++ .../quotes/store/QuotesStoreGetMethodTest.kt | 92 --- .../store/QuotesStoreInitializationTest.kt | 82 --- .../store/QuotesStoreUpdateMethodsTest.kt | 133 ----- 20 files changed, 869 insertions(+), 417 deletions(-) rename data/quotes/src/main/java/com/tangem/data/quotes/store/{DefaultQuotesStoreV2.kt => DefaultQuotesStatusesStore.kt} (68%) create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt delete mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt delete mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt delete mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt delete mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt index 9bb7985754..7f412108e3 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt @@ -80,7 +80,7 @@ internal class DefaultNetworksStatusesStore( ifNotFound: (Network.ID) -> SimpleNetworkStatus?, ) { if (networks.isEmpty()) { - Timber.d("Nothing to update: networks is empty") + Timber.d("Nothing to update: networks are empty") return } diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index 368d9b3bef..41c386a5c4 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -9,6 +9,10 @@ android { namespace = "com.tangem.data.quotes" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) @@ -30,7 +34,8 @@ dependencies { kapt(deps.hilt.kapt) testImplementation(deps.test.coroutine) - testImplementation(deps.test.junit) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) testImplementation(projects.common.test) diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt index ed18015ad2..af55873878 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt @@ -1,6 +1,6 @@ package com.tangem.data.quotes -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.QuotesRepositoryV2 import com.tangem.domain.tokens.model.QuoteStatus @@ -14,7 +14,7 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ internal class DefaultQuotesRepositoryV2 @Inject constructor( - private val quotesStore: QuotesStoreV2, + private val quotesStore: QuotesStatusesStore, ) : QuotesRepositoryV2 { override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? { diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt index 58803a1a56..fb43a38727 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt @@ -5,8 +5,8 @@ import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi import com.tangem.data.quotes.DefaultQuotesRepositoryV2 -import com.tangem.data.quotes.store.DefaultQuotesStoreV2 -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.DefaultQuotesStatusesStore +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore @@ -33,8 +33,8 @@ internal object QuotesDataModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, dispatchers: CoroutineDispatcherProvider, - ): QuotesStoreV2 { - return DefaultQuotesStoreV2( + ): QuotesStatusesStore { + return DefaultQuotesStatusesStore( runtimeStore = RuntimeSharedStore(), persistenceDataStore = DataStoreFactory.create( serializer = MoshiDataStoreSerializer( diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt index cfa6b766f0..830d3afef4 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt @@ -2,7 +2,9 @@ package com.tangem.data.quotes.multi import arrow.core.Either import com.tangem.data.common.api.safeApiCallWithTimeout -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.data.quotes.store.setSourceAsCache +import com.tangem.data.quotes.store.setSourceAsOnlyCache import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.appcurrency.AppCurrencyResponseStore @@ -29,7 +31,7 @@ import javax.inject.Singleton internal class DefaultMultiQuoteFetcher @Inject constructor( private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, - private val quotesStore: QuotesStoreV2, + private val quotesStore: QuotesStatusesStore, private val dispatchers: CoroutineDispatcherProvider, ) : MultiQuoteFetcher { @@ -41,7 +43,7 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( return@catchOn } - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( currenciesIds = params.currenciesIds.mapTo( @@ -67,11 +69,11 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( filteredIds = replacementIdsResult.idsFiltered, ) - quotesStore.storeActual(values = updatedResponse.quotes) + quotesStore.store(values = updatedResponse.quotes) } .onLeft { Timber.e(it) - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } private suspend fun getAppCurrencyId(params: MultiQuoteFetcher.Params): String { diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt index 8f0ae6297f..f3f01a2e1d 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt @@ -2,7 +2,7 @@ package com.tangem.data.quotes.multi import androidx.annotation.VisibleForTesting import arrow.core.left -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.quotes.multi.MultiQuoteFetcher @@ -33,7 +33,7 @@ import javax.inject.Singleton @Singleton internal class DefaultMultiQuoteUpdater @Inject constructor( private val appCurrencyResponseStore: AppCurrencyResponseStore, - private val quotesStore: QuotesStoreV2, + private val quotesStore: QuotesStatusesStore, private val multiQuoteFetcher: MultiQuoteFetcher, dispatchers: CoroutineDispatcherProvider, ) : MultiQuoteUpdater { diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt index 796d0dceac..490804a7ea 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt @@ -1,6 +1,6 @@ package com.tangem.data.quotes.single -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.quotes.single.SingleQuoteProducer import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.mapNotNull */ internal class DefaultSingleQuoteProducer @AssistedInject constructor( @Assisted val params: SingleQuoteProducer.Params, - private val quotesStore: QuotesStoreV2, + private val quotesStore: QuotesStatusesStore, private val dispatchers: CoroutineDispatcherProvider, ) : SingleQuoteProducer { diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt similarity index 68% rename from data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt index 2158d4d6a8..6cdb7bd157 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt @@ -15,21 +15,22 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch +import timber.log.Timber internal typealias CurrencyIdWithQuote = Map /** - * Default implementation of [QuotesStoreV2] + * Default implementation of [QuotesStatusesStore] * * @property runtimeStore runtime store * @property persistenceDataStore persistence store * @param dispatchers dispatchers */ -internal class DefaultQuotesStoreV2( +internal class DefaultQuotesStatusesStore( private val runtimeStore: RuntimeSharedStore>, private val persistenceDataStore: DataStore, dispatchers: CoroutineDispatcherProvider, -) : QuotesStoreV2 { +) : QuotesStatusesStore { private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) @@ -49,33 +50,28 @@ internal class DefaultQuotesStoreV2( override suspend fun getAllSyncOrNull(): Set? = runtimeStore.getSyncOrNull() - override suspend fun refresh(currenciesIds: Set) { - updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.CACHE) - } - - override suspend fun storeActual(values: Map) { - coroutineScope { - launch { - val quotes = QuoteStatusConverter(isCached = false).convertSet(input = values.entries) - storeInRuntimeStore(values = quotes) - } - launch { storeInPersistenceStore(values = values) } - } - } - - override suspend fun storeError(currenciesIds: Set) { - updateStatusSourceInRuntime( - currenciesIds = currenciesIds, - ifNotFound = ::QuoteStatus, - source = StatusSource.ONLY_CACHE, + override suspend fun updateStatusSource( + currencyId: CryptoCurrency.RawID, + source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus?, + ) { + updateStatusSource( + currenciesIds = setOf(currencyId), + source = source, + ifNotFound = ifNotFound, ) } - private suspend fun updateStatusSourceInRuntime( + override suspend fun updateStatusSource( currenciesIds: Set, - ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus?, ) { + if (currenciesIds.isEmpty()) { + Timber.d("Nothing to update: currencies ids are empty") + return + } + runtimeStore.update(default = emptySet()) { stored -> val updatedQuotes = currenciesIds.mapNotNullTo(hashSetOf()) { id -> val quote = stored.firstOrNull { it.rawCurrencyId == id } @@ -89,13 +85,24 @@ internal class DefaultQuotesStoreV2( } } - private suspend fun storeInRuntimeStore(values: Set) { - runtimeStore.update(default = emptySet()) { saved -> - saved.addOrReplace(items = values) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId } + override suspend fun store(values: CurrencyIdWithQuote) { + if (values.isEmpty()) return + + coroutineScope { + launch { storeInRuntime(values = values) } + launch { storeInPersistence(values = values) } } } - private suspend fun storeInPersistenceStore(values: Map) { + private suspend fun storeInRuntime(values: CurrencyIdWithQuote) { + val quotes = QuoteStatusConverter(isCached = false).convertSet(input = values.entries) + + runtimeStore.update(default = emptySet()) { saved -> + saved.addOrReplace(items = quotes) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId } + } + } + + private suspend fun storeInPersistence(values: CurrencyIdWithQuote) { persistenceDataStore.updateData { storedQuotes -> storedQuotes + values } } } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt new file mode 100644 index 0000000000..140b84c495 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt @@ -0,0 +1,46 @@ +package com.tangem.data.quotes.store + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.tokens.model.QuoteStatus +import kotlinx.coroutines.flow.Flow + +/** Store of [QuoteStatus]'es set */ +internal interface QuotesStatusesStore { + + /** Get flow of quotes */ + fun get(): Flow> + + /** Get all quotes synchronously or null */ + suspend fun getAllSyncOrNull(): Set? + + /** + * Update [source] of [QuoteStatus] by [currencyId]. + * If the status is not found, create a new one by [ifNotFound]. + */ + suspend fun updateStatusSource( + currencyId: CryptoCurrency.RawID, + source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + ) + + /** + * Update [source] of [QuoteStatus]es by [currenciesIds]. + * If the status is not found, create a new one by [ifNotFound]. + */ + suspend fun updateStatusSource( + currenciesIds: Set, + source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + ) + + /** + * Store quotes statuses + * + * @param values map of currency ids and quotes + * + * See complex methods in `QuotesStatusesStoreExt`. + */ + suspend fun store(values: Map) +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt new file mode 100644 index 0000000000..849519a417 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt @@ -0,0 +1,22 @@ +package com.tangem.data.quotes.store + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.tokens.model.QuoteStatus + +/** Set [StatusSource] of quotes statuses as [StatusSource.CACHE] for [currenciesIds] */ +internal suspend fun QuotesStatusesStore.setSourceAsCache(currenciesIds: Set) { + updateStatusSource(currenciesIds = currenciesIds, source = StatusSource.CACHE) +} + +/** + * Set [StatusSource] of quotes statuses as [StatusSource.ONLY_CACHE] for [currenciesIds]. + * If the stored status is not found, store a default [QuoteStatus.Empty] status. + */ +internal suspend fun QuotesStatusesStore.setSourceAsOnlyCache(currenciesIds: Set) { + updateStatusSource( + currenciesIds = currenciesIds, + source = StatusSource.ONLY_CACHE, + ifNotFound = ::QuoteStatus, + ) +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt deleted file mode 100644 index b29bb240af..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.data.quotes.store - -import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus -import kotlinx.coroutines.flow.Flow - -/** Store of [QuoteStatus]'es set */ -internal interface QuotesStoreV2 { - - /** Get flow of quotes */ - fun get(): Flow> - - /** Get all quotes synchronously or null */ - suspend fun getAllSyncOrNull(): Set? - - /** Refresh status of [currenciesIds] */ - suspend fun refresh(currenciesIds: Set) - - /** Store actual map of currency ids and quotes [values] */ - suspend fun storeActual(values: Map) - - /** Store error for [currenciesIds] */ - suspend fun storeError(currenciesIds: Set) -} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt index d49e65f597..4969adec58 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt @@ -2,7 +2,9 @@ package com.tangem.data.quotes.multi import com.google.common.truth.Truth import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.data.quotes.store.setSourceAsCache +import com.tangem.data.quotes.store.setSourceAsOnlyCache import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -27,7 +29,7 @@ internal class DefaultMultiQuoteStatusFetcherTest { private val tangemTechApi = mockk(relaxed = true) private val appCurrencyResponseStore = mockk(relaxed = true) - private val quotesStore = mockk(relaxed = true) + private val quotesStore = mockk(relaxed = true) private val fetcher = DefaultMultiQuoteFetcher( tangemTechApi = tangemTechApi, @@ -50,17 +52,17 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } coVerify(inverse = true) { - quotesStore.storeError(currenciesIds = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -73,11 +75,11 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerify(inverse = true) { - quotesStore.refresh(currenciesIds = any()) + quotesStore.setSourceAsCache(currenciesIds = any()) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) - quotesStore.storeError(currenciesIds = any()) + quotesStore.store(values = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -96,16 +98,16 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } coVerify(inverse = true) { appCurrencyResponseStore.getSyncOrNull() - quotesStore.storeError(currenciesIds = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -119,14 +121,14 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } coVerify(inverse = true) { appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() @@ -150,17 +152,17 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } coVerify(inverse = true) { - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() @@ -175,14 +177,14 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) appCurrencyResponseStore.getSyncOrNull() - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt index d51f1c0460..0e7eeae889 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt @@ -3,7 +3,7 @@ package com.tangem.data.quotes.multi import arrow.core.right import com.google.common.truth.Truth import com.tangem.common.test.utils.getEmittedValues -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.quotes.multi.MultiQuoteFetcher @@ -19,7 +19,7 @@ import org.junit.Test internal class DefaultMultiQuoteStatusUpdaterTest { private val appCurrencyResponseStore: AppCurrencyResponseStore = mockk() - private val quotesStore: QuotesStoreV2 = mockk() + private val quotesStore: QuotesStatusesStore = mockk() private val multiQuoteFetcher: MultiQuoteFetcher = mockk() private val multiQuoteUpdater = DefaultMultiQuoteUpdater( diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt index 83108ee088..131263866d 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt @@ -3,7 +3,9 @@ package com.tangem.data.quotes.single import com.google.common.truth.Truth import com.tangem.common.test.data.quote.MockQuoteResponseFactory import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.data.quotes.store.setSourceAsCache +import com.tangem.data.quotes.store.setSourceAsOnlyCache import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -18,14 +20,14 @@ import io.mockk.coVerify import io.mockk.coVerifyOrder import io.mockk.mockk import kotlinx.coroutines.test.runTest -import org.junit.Test +import org.junit.jupiter.api.Test import java.math.BigDecimal internal class DefaultSingleQuoteStatusFetcherTest { private val tangemTechApi = mockk(relaxed = true) private val appCurrencyResponseStore = mockk(relaxed = true) - private val quotesStore = mockk(relaxed = true) + private val quotesStore = mockk(relaxed = true) private val multiFetcher = DefaultMultiQuoteFetcher( tangemTechApi = tangemTechApi, @@ -50,14 +52,14 @@ internal class DefaultSingleQuoteStatusFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(params.rawCurrencyId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(params.rawCurrencyId)) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } coVerify(inverse = true) { - quotesStore.storeError(currenciesIds = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -76,9 +78,9 @@ internal class DefaultSingleQuoteStatusFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } Truth.assertThat(actual.isRight()).isTrue() @@ -92,8 +94,8 @@ internal class DefaultSingleQuoteStatusFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) - quotesStore.storeError(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) } Truth.assertThat(actual.isLeft()).isTrue() @@ -117,14 +119,14 @@ internal class DefaultSingleQuoteStatusFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeError(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) } coVerify(inverse = true) { - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() @@ -139,14 +141,14 @@ internal class DefaultSingleQuoteStatusFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) appCurrencyResponseStore.getSyncOrNull() - quotesStore.storeError(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) } coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt index 774a1b9d9f..6bca20b07d 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt @@ -2,7 +2,7 @@ package com.tangem.data.quotes.single import com.google.common.truth.Truth import com.tangem.common.test.utils.getEmittedValues -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.single.SingleQuoteProducer @@ -25,7 +25,7 @@ internal class DefaultSingleQuoteStatusProducerTest { rawCurrencyId = CryptoCurrency.RawID(value = "BTC"), ) - private val quotesStore = mockk() + private val quotesStore = mockk() private val producer = DefaultSingleQuoteProducer( params = params, diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt new file mode 100644 index 0000000000..6713800ad0 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt @@ -0,0 +1,133 @@ +package com.tangem.data.quotes.store + +import com.google.common.truth.Truth +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.common.test.data.quote.toDomain +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal +import kotlin.properties.Delegates + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuotesStatusesStoreExtTest { + + private var runtimeStore: RuntimeSharedStore> by Delegates.notNull() + private var persistenceStore: MockStateDataStore by Delegates.notNull() + private var store: DefaultQuotesStatusesStore by Delegates.notNull() + + private val btcQuoteDM = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) + private val btcQuote = btcQuoteDM.toDomain() + + @BeforeEach + fun resetMocks() { + runtimeStore = RuntimeSharedStore() + persistenceStore = MockStateDataStore(default = emptyMap()) + + store = DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SetSourceAsCache { + + @ParameterizedTest + @ProvideTestModels + fun setSourceAsCache(model: SetSourceTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.setSourceAsCache(model.currenciesIds) + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + SetSourceTestModel( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = emptySet(), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.CACHE)), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuoteDM.toDomain(source = StatusSource.CACHE)), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.CACHE)), + ), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SetSourceAsOnlyCache { + + @ParameterizedTest + @ProvideTestModels + fun setSourceAsOnlyCache(model: SetSourceTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.setSourceAsOnlyCache(model.currenciesIds) + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + SetSourceTestModel( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(QuoteStatus(btcQuote.rawCurrencyId)), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.ONLY_CACHE)), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuoteDM.toDomain(source = StatusSource.ONLY_CACHE)), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.ONLY_CACHE)), + ), + ) + } + + data class SetSourceTestModel( + val initialRuntime: Set?, + val currenciesIds: Set, + val expected: Set?, + ) +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt new file mode 100644 index 0000000000..6fb34da4e7 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt @@ -0,0 +1,565 @@ +package com.tangem.data.quotes.store + +import androidx.datastore.core.DataStore +import com.google.common.truth.Truth +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.common.test.data.quote.toDomain +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal +import kotlin.properties.Delegates + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuotesStatusesStoreTest { + + private var runtimeStore: RuntimeSharedStore> by Delegates.notNull() + private var persistenceStore: MockStateDataStore by Delegates.notNull() + private var store: DefaultQuotesStatusesStore by Delegates.notNull() + + // region Data models + private val btcQuoteDM = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) + private val ethQuoteDM = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) + // endregion + + // region Domain models + private val btcQuote = btcQuoteDM.toDomain() + private val ethQuote = ethQuoteDM.toDomain() + private val adaEmptyQuote = QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ADA")) + // endregion + + @BeforeEach + fun resetMocks() { + runtimeStore = RuntimeSharedStore() + persistenceStore = MockStateDataStore(default = emptyMap()) + + store = DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Initialization { + + @Test + fun `initialization if cache store is empty`() = runTest { + // Arrange + val runtimeStore = RuntimeSharedStore>() + val persistenceStore: DataStore = mockk() + + every { persistenceStore.data } returns emptyFlow() + + // Act + DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val actual = runtimeStore.getSyncOrNull() + + // Assert + val expected = null + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `initialization if cache store contains empty map`() = runTest { + // Arrange + val runtimeStore = RuntimeSharedStore>() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + // Act + DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val actual = runtimeStore.getSyncOrNull() + + // Assert + val expected = null + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `initialization if cache store is not empty`() = runTest { + // Arrange + val runtimeStore = RuntimeSharedStore>() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + persistenceStore.updateData { + it.toMutableMap().apply { + this += btcQuoteDM + this += ethQuoteDM + } + } + + // Act + DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val actual = runtimeStore.getSyncOrNull() + + // Assert + val expected = setOf( + btcQuoteDM.toDomain(source = StatusSource.CACHE), + ethQuoteDM.toDomain(source = StatusSource.CACHE), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Get { + + @ParameterizedTest + @ProvideTestModels + fun get(model: GetTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + val actual = getEmittedValues(flow = store.get()) + + // Assert + val expected = listOfNotNull(model.expected) + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetTestModel(initialRuntime = null, expected = null), + GetTestModel(initialRuntime = emptySet(), expected = emptySet()), + GetTestModel(initialRuntime = setOf(btcQuote, ethQuote), expected = setOf(btcQuote, ethQuote)), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetAllSyncOrNull { + + @ParameterizedTest + @ProvideTestModels + fun getAllSyncOrNull(model: GetTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetTestModel(initialRuntime = null, expected = null), + GetTestModel(initialRuntime = emptySet(), expected = emptySet()), + GetTestModel(initialRuntime = setOf(btcQuote, ethQuote), expected = setOf(btcQuote, ethQuote)), + ) + } + + data class GetTestModel(val initialRuntime: Set?, val expected: Set?) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SingleUpdateStatusSource { + + @ParameterizedTest + @ProvideTestModels + fun updateStatusSource(model: UpdateStatusSourceModel.Single) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.updateStatusSource( + currencyId = model.currencyId, + source = model.source, + ifNotFound = model.ifNotFound, + ) + + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + // region runtime store is null + UpdateStatusSourceModel.Single( + initialRuntime = null, + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Single( + initialRuntime = null, + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store is empty + UpdateStatusSourceModel.Single( + initialRuntime = emptySet(), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Single( + initialRuntime = emptySet(), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store contains statuses + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote, ethQuote, adaEmptyQuote), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.CACHE)), + ethQuote, + adaEmptyQuote, + ), + ), + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote, ethQuote), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ONLY_CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.ONLY_CACHE)), + ethQuote, + ), + ), + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote, ethQuote), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ACTUAL, + expected = setOf(btcQuote, ethQuote), + ), + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote), + currencyId = adaEmptyQuote.rawCurrencyId, + source = StatusSource.ACTUAL, + ifNotFound = ::QuoteStatus, + expected = setOf(btcQuote, adaEmptyQuote), + ), + // endregion + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class MultiUpdateStatusSource { + + @ParameterizedTest + @ProvideTestModels + fun updateStatusSource(model: UpdateStatusSourceModel.Multi) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.updateStatusSource( + currenciesIds = model.currenciesIds, + source = model.source, + ifNotFound = model.ifNotFound, + ) + + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + // region runtime store is null + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf(), + source = StatusSource.CACHE, // never-mind + expected = null, + ), + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf( + btcQuote.rawCurrencyId, + CryptoCurrency.RawID(value = "ETH"), + ), + source = StatusSource.ACTUAL, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store is empty + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf(), + source = StatusSource.CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf( + btcQuote.rawCurrencyId, + CryptoCurrency.RawID(value = "ETH"), + ), + source = StatusSource.ACTUAL, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store contains statuses + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote, adaEmptyQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.CACHE)), + adaEmptyQuote, + ), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote, adaEmptyQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.ONLY_CACHE)), + adaEmptyQuote, + ), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote, adaEmptyQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ACTUAL, + expected = setOf(btcQuote, adaEmptyQuote), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ACTUAL, + expected = setOf(btcQuote), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ACTUAL, + ifNotFound = ::QuoteStatus, + expected = setOf(btcQuote, adaEmptyQuote), + ), + // endregion + ) + } + + sealed interface UpdateStatusSourceModel { + + val initialRuntime: Set? + val source: StatusSource + val ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? + val expected: Set? + + data class Single( + override val initialRuntime: Set?, + val currencyId: CryptoCurrency.RawID, + override val source: StatusSource, + override val ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + override val expected: Set?, + ) : UpdateStatusSourceModel + + data class Multi( + override val initialRuntime: Set?, + val currenciesIds: Set, + override val source: StatusSource, + override val ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + override val expected: Set?, + ) : UpdateStatusSourceModel + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Store { + + @ParameterizedTest + @ProvideTestModels + fun store(model: StoreTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + if (model.initialPersistence != null) { + persistenceStore.updateData { model.initialPersistence } + } + + // Act + store.store(values = model.values) + + val runtimeActual = runtimeStore.getSyncOrNull() + val persistenceActual = getEmittedValues(persistenceStore.data) + + // Assert + val runtimeExpected = model.runtimeExpected + val persistenceExpected = listOf(model.persistenceExpected) + + Truth.assertThat(runtimeActual).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceActual).isEqualTo(persistenceExpected) + } + + private fun provideTestModels() = listOf( + // region stores are null + StoreTestModel( + initialRuntime = null, + initialPersistence = null, + values = emptyMap(), + runtimeExpected = null, + persistenceExpected = emptyMap(), + ), + StoreTestModel( + initialRuntime = null, + initialPersistence = null, + values = mapOf(btcQuoteDM), + runtimeExpected = setOf(btcQuote), + persistenceExpected = mapOf(btcQuoteDM), + ), + // endregion + + // region persistence store is null + StoreTestModel( + initialRuntime = setOf(btcQuote), + initialPersistence = null, + values = emptyMap(), + runtimeExpected = setOf(btcQuote), + persistenceExpected = emptyMap(), + ), + StoreTestModel( + initialRuntime = setOf(ethQuote), + initialPersistence = null, + values = mapOf(btcQuoteDM), + runtimeExpected = setOf(ethQuote, btcQuote), + persistenceExpected = mapOf(btcQuoteDM), + ), + // endregion + + // region runtime store is null + StoreTestModel( + initialRuntime = null, + initialPersistence = mapOf(btcQuoteDM), + values = emptyMap(), + runtimeExpected = null, + persistenceExpected = mapOf(btcQuoteDM), + ), + StoreTestModel( + initialRuntime = null, + initialPersistence = mapOf(ethQuoteDM), + values = mapOf(btcQuoteDM), + runtimeExpected = setOf(btcQuote), + persistenceExpected = mapOf(ethQuoteDM, btcQuoteDM), + ), + // endregion + + // region stores contain data + StoreTestModel( + initialRuntime = setOf(btcQuote), + initialPersistence = mapOf(btcQuoteDM), + values = emptyMap(), + runtimeExpected = setOf(btcQuote), + persistenceExpected = mapOf(btcQuoteDM), + ), + StoreTestModel( + initialRuntime = setOf(btcQuote), + initialPersistence = mapOf(btcQuoteDM), + values = mapOf(ethQuoteDM), + runtimeExpected = setOf(ethQuote, btcQuote), + persistenceExpected = mapOf(ethQuoteDM, btcQuoteDM), + ), + // endregion + ) + } + + data class StoreTestModel( + val initialRuntime: Set?, + val initialPersistence: CurrencyIdWithQuote?, + val values: CurrencyIdWithQuote, + val persistenceExpected: CurrencyIdWithQuote?, + val runtimeExpected: Set?, + ) +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt deleted file mode 100644 index b957e0624e..0000000000 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.tangem.data.quotes.store - -import com.google.common.truth.Truth -import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.common.test.data.quote.toDomain -import com.tangem.common.test.datastore.MockStateDataStore -import com.tangem.common.test.utils.getEmittedValues -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.tokens.model.QuoteStatus -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.test.runTest -import org.junit.Test -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -internal class QuotesStoreGetMethodTest { - - private val runtimeStore = RuntimeSharedStore>() - private val persistenceStore = MockStateDataStore(default = emptyMap()) - - private val store = DefaultQuotesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `test get if runtime store is empty`() = runTest { - val actual = store.get() - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values).isEqualTo(emptyList>()) - } - - @Test - fun `test get if runtime store contains empty set`() = runTest { - runtimeStore.store(value = emptySet()) - - val actual = store.get() - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values).isEqualTo(listOf(emptySet())) - } - - @Test - fun `test get if runtime store is not empty`() = runTest { - val btcQuote = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) - val ethQuote = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) - - runtimeStore.store(value = setOf(btcQuote.toDomain(), ethQuote.toDomain())) - - val actual = store.get() - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values).isEqualTo(listOf(setOf(btcQuote.toDomain(), ethQuote.toDomain()))) - } - - @Test - fun `test getAllSyncOrNull if runtime store is empty`() = runTest { - val actual = store.getAllSyncOrNull() - - Truth.assertThat(actual).isEqualTo(null) - } - - @Test - fun `test getAllSyncOrNull if runtime store contains empty set`() = runTest { - runtimeStore.store(value = emptySet()) - - val actual = store.getAllSyncOrNull() - - Truth.assertThat(actual).isEqualTo(emptySet()) - } - - @Test - fun `test getAllSyncOrNull if runtime store is not empty`() = runTest { - val btcQuote = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) - val ethQuote = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) - - runtimeStore.store(value = setOf(btcQuote.toDomain(), ethQuote.toDomain())) - - val actual = store.getAllSyncOrNull() - - val expected = setOf(btcQuote.toDomain(), ethQuote.toDomain()) - Truth.assertThat(actual).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt deleted file mode 100644 index e60dd2bd09..0000000000 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.data.quotes.store - -import androidx.datastore.core.DataStore -import com.google.common.truth.Truth -import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.common.test.data.quote.toDomain -import com.tangem.common.test.datastore.MockStateDataStore -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.QuoteStatus -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.test.runTest -import org.junit.Test -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -internal class QuotesStoreInitializationTest { - - @Test - fun `test initialization if cache store is empty`() = runTest { - val runtimeStore = RuntimeSharedStore>() - val persistenceStore: DataStore = mockk() - - every { persistenceStore.data } returns emptyFlow() - - DefaultQuotesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(null) - } - - @Test - fun `test initialization if cache store contains empty map`() = runTest { - val runtimeStore = RuntimeSharedStore>() - val persistenceStore = MockStateDataStore(default = emptyMap()) - - DefaultQuotesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(null) - } - - @Test - fun `test initialization if cache store is not empty`() = runTest { - val runtimeStore = RuntimeSharedStore>() - val persistenceStore = MockStateDataStore(default = emptyMap()) - - val btcQuote = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) - val ethQuote = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) - - persistenceStore.updateData { - it.toMutableMap().apply { - this += btcQuote - this += ethQuote - } - } - - DefaultQuotesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - val expected = setOf( - btcQuote.toDomain(source = StatusSource.CACHE), - ethQuote.toDomain(source = StatusSource.CACHE), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt deleted file mode 100644 index d3224d5c48..0000000000 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.tangem.data.quotes.store - -import com.google.common.truth.Truth -import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.common.test.data.quote.toDomain -import com.tangem.common.test.datastore.MockStateDataStore -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.test.runTest -import org.junit.Test -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -internal class QuotesStoreUpdateMethodsTest { - - private val runtimeStore = RuntimeSharedStore>() - private val persistenceStore = MockStateDataStore(default = emptyMap()) - - private val store = DefaultQuotesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `refresh if runtime store is empty`() = runTest { - val currenciesIds = setOf( - CryptoCurrency.RawID(value = "BTC"), - CryptoCurrency.RawID(value = "ETH"), - ) - - store.refresh(currenciesIds = currenciesIds) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptySet()) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `refresh if runtime store contains quote with this id`() = runTest { - val quote = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE) - .toDomain(rawCurrencyId = "BTC", source = StatusSource.ACTUAL) - - runtimeStore.store(value = setOf(quote)) - - store.refresh(currenciesIds = setOf(quote.rawCurrencyId)) - - val runtimeExpected = setOf( - quote.copy(value = quote.value.copySealed(source = StatusSource.CACHE)), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `store actual if runtime and cache stores contain quotes with this id`() = runTest { - val prevStatus = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE) - - runtimeStore.store( - value = setOf( - prevStatus.toDomain(rawCurrencyId = "BTC", source = StatusSource.ONLY_CACHE), - ), - ) - - persistenceStore.updateData { - it.toMutableMap().apply { - put("BTC", prevStatus) - } - } - - val newStatus = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.TEN) - - store.storeActual(values = mapOf("BTC" to newStatus)) - - val runtimeExpected = setOf( - newStatus.toDomain(rawCurrencyId = "BTC", source = StatusSource.ACTUAL), - ) - val persistenceExpected = mapOf("BTC" to newStatus) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) - } - - @Test - fun `store error if runtime store is empty`() = runTest { - val currenciesIds = setOf( - CryptoCurrency.RawID(value = "BTC"), - CryptoCurrency.RawID(value = "ETH"), - ) - - store.storeError(currenciesIds = currenciesIds) - - val runtimeExpected = currenciesIds - .map { QuoteStatus(rawCurrencyId = it, value = QuoteStatus.Empty) } - .toSet() - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `store error if runtime store contains status with this network`() = runTest { - val status = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE) - - runtimeStore.store( - value = setOf( - status.toDomain(rawCurrencyId = "BTC", source = StatusSource.CACHE), - QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH"), value = QuoteStatus.Empty), - ), - ) - - store.storeError( - currenciesIds = setOf( - CryptoCurrency.RawID(value = "BTC"), - CryptoCurrency.RawID(value = "ETH"), - ), - ) - - val runtimeExpected = setOf( - status.toDomain(rawCurrencyId = "BTC", source = StatusSource.ONLY_CACHE), - QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH"), value = QuoteStatus.Empty), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } -} \ No newline at end of file From c9e78f485d0eede75f3f91136a38831a28b1fe4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Jun 2025 12:10:53 +0400 Subject: [PATCH 09/99] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 10 +- .../data/quotes/DefaultQuotesRepositoryV2.kt | 26 ----- .../tangem/data/quotes/di/QuotesDataModule.kt | 6 +- .../repository/DefaultQuotesRepository.kt | 35 ++++++ .../repository/DefaultQuotesRepositoryTest.kt | 108 ++++++++++++++++++ ...tesRepositoryV2.kt => QuotesRepository.kt} | 2 +- .../BaseCurrencyStatusOperations.kt | 6 +- .../CachedCurrenciesStatusesOperations.kt | 6 +- .../feature/swap/domain/SwapInteractorImpl.kt | 6 +- .../factory/express/ExchangeStatusFactory.kt | 6 +- 10 files changed, 164 insertions(+), 47 deletions(-) delete mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt rename domain/quotes/src/main/java/com/tangem/domain/quotes/{QuotesRepositoryV2.kt => QuotesRepository.kt} (92%) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 185840f10a..f05324a2f1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -8,7 +8,7 @@ import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.quotes.QuotesRepositoryV2 +import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher @@ -368,7 +368,7 @@ internal object TokensDomainModule { fun provideBaseCurrenciesStatusesOperations( tokensFeatureToggles: TokensFeatureToggles, currenciesRepository: CurrenciesRepository, - quotesRepositoryV2: QuotesRepositoryV2, + quotesRepository: QuotesRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -380,7 +380,7 @@ internal object TokensDomainModule { ): BaseCurrenciesStatusesOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, - quotesRepositoryV2 = quotesRepositoryV2, + quotesRepository = quotesRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, @@ -398,7 +398,7 @@ internal object TokensDomainModule { fun provideBaseCurrencyStatusOperations( tokensFeatureToggles: TokensFeatureToggles, currenciesRepository: CurrenciesRepository, - quotesRepositoryV2: QuotesRepositoryV2, + quotesRepository: QuotesRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -410,7 +410,7 @@ internal object TokensDomainModule { ): BaseCurrencyStatusOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, - quotesRepositoryV2 = quotesRepositoryV2, + quotesRepository = quotesRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt deleted file mode 100644 index af55873878..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.data.quotes - -import com.tangem.data.quotes.store.QuotesStatusesStore -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.QuotesRepositoryV2 -import com.tangem.domain.tokens.model.QuoteStatus -import javax.inject.Inject - -/** - * Default implementation of [QuotesRepositoryV2] - * - * @property quotesStore quotes store - * -[REDACTED_AUTHOR] - */ -internal class DefaultQuotesRepositoryV2 @Inject constructor( - private val quotesStore: QuotesStatusesStore, -) : QuotesRepositoryV2 { - - override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? { - return quotesStore.getAllSyncOrNull()?.mapTo(hashSetOf()) { - it.takeIf { it.rawCurrencyId in currenciesIds } - ?: QuoteStatus(rawCurrencyId = it.rawCurrencyId) - } - } -} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt index fb43a38727..e25057bc21 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi -import com.tangem.data.quotes.DefaultQuotesRepositoryV2 +import com.tangem.data.quotes.repository.DefaultQuotesRepository import com.tangem.data.quotes.store.DefaultQuotesStatusesStore import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.api.tangemTech.models.QuotesResponse @@ -12,7 +12,7 @@ import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes -import com.tangem.domain.quotes.QuotesRepositoryV2 +import com.tangem.domain.quotes.QuotesRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -51,5 +51,5 @@ internal object QuotesDataModule { @Singleton @Provides - fun providesQuotesRepositoryV2(impl: DefaultQuotesRepositoryV2): QuotesRepositoryV2 = impl + fun providesQuotesRepository(impl: DefaultQuotesRepository): QuotesRepository = impl } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt new file mode 100644 index 0000000000..b7f600a91a --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt @@ -0,0 +1,35 @@ +package com.tangem.data.quotes.repository + +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.tokens.model.QuoteStatus +import timber.log.Timber +import javax.inject.Inject + +/** + * Default implementation of [QuotesRepository] + * + * @property quotesStatusesStore quotes statuses store + * +[REDACTED_AUTHOR] + */ +internal class DefaultQuotesRepository @Inject constructor( + private val quotesStatusesStore: QuotesStatusesStore, +) : QuotesRepository { + + override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set { + if (currenciesIds.isEmpty()) { + Timber.e("currenciesIds are empty") + return emptySet() + } + + val storedQuotes = quotesStatusesStore.getAllSyncOrNull() + ?.filter { it.rawCurrencyId in currenciesIds } + + return currenciesIds.mapTo(hashSetOf()) { currencyId -> + storedQuotes?.firstOrNull { it.rawCurrencyId == currencyId } + ?: QuoteStatus(rawCurrencyId = currencyId) + } + } +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt new file mode 100644 index 0000000000..2484c2b302 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt @@ -0,0 +1,108 @@ +package com.tangem.data.quotes.repository + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.tokens.model.QuoteStatus +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultQuotesRepositoryTest { + + private val quotesStatusesStore = mockk() + private val repository: QuotesRepository = DefaultQuotesRepository(quotesStatusesStore = quotesStatusesStore) + + @BeforeEach + fun resetMocks() { + clearMocks(quotesStatusesStore) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetMultiQuoteSyncOrNull { + + private val btcRawId = CryptoCurrency.RawID(value = "BTC") + private val ethRawId = CryptoCurrency.RawID(value = "ETH") + + private val ethQuote = QuoteStatus( + rawCurrencyId = ethRawId, + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ZERO, + priceChange = BigDecimal.ZERO, + ), + ) + + @ParameterizedTest + @ProvideTestModels + fun getMultiQuoteSyncOrNull(model: GetMultiQuoteSyncOrNullModel) = runTest { + // Arrange + coEvery { quotesStatusesStore.getAllSyncOrNull() } returns model.initialStore + + // Act + val actual = repository.getMultiQuoteSyncOrNull(currenciesIds = model.currencyIds) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetMultiQuoteSyncOrNullModel( + initialStore = null, + currencyIds = emptySet(), + expected = emptySet(), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = null, + currencyIds = setOf(ethRawId), + expected = setOf(QuoteStatus(rawCurrencyId = ethRawId)), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = emptySet(), + currencyIds = emptySet(), + expected = emptySet(), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = emptySet(), + currencyIds = setOf(ethRawId), + expected = setOf(QuoteStatus(rawCurrencyId = ethRawId)), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = setOf(ethQuote), + currencyIds = setOf(ethRawId), + expected = setOf(ethQuote), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = setOf(QuoteStatus(rawCurrencyId = btcRawId)), + currencyIds = setOf(ethRawId), + expected = setOf(QuoteStatus(rawCurrencyId = ethRawId)), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = setOf(ethQuote, QuoteStatus(rawCurrencyId = btcRawId)), + currencyIds = setOf(ethRawId), + expected = setOf(ethQuote), + ), + ) + } + + data class GetMultiQuoteSyncOrNullModel( + val initialStore: Set?, + val currencyIds: Set, + val expected: Set?, + ) +} \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt similarity index 92% rename from domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt rename to domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt index dd446b9f0a..7da2b7f682 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt @@ -8,7 +8,7 @@ import com.tangem.domain.tokens.model.QuoteStatus * [REDACTED_AUTHOR] */ -interface QuotesRepositoryV2 { +interface QuotesRepository { /** Get quotes by [currenciesIds] synchronously or null */ suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 44d9a9574c..543594679c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -13,7 +13,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier -import com.tangem.domain.quotes.QuotesRepositoryV2 +import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.single.SingleQuoteProducer import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -41,7 +41,7 @@ import kotlinx.coroutines.flow.* @Suppress("LargeClass", "LongParameterList") abstract class BaseCurrencyStatusOperations( private val currenciesRepository: CurrenciesRepository, - private val quotesRepositoryV2: QuotesRepositoryV2, + private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, @@ -257,7 +257,7 @@ abstract class BaseCurrencyStatusOperations( val (_, currenciesIds) = getIds(nonEmptyCurrencies) val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() - val quotes = quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = rawIds)?.right() + val quotes = quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = rawIds)?.right() val networkStatuses = multiNetworkStatusSupplier( params = MultiNetworkStatusProducer.Params(userWalletId = userWalletId), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index e547b6549b..5f14c36d45 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -17,7 +17,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier -import com.tangem.domain.quotes.QuotesRepositoryV2 +import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.single.SingleQuoteProducer import com.tangem.domain.quotes.single.SingleQuoteSupplier @@ -43,7 +43,7 @@ import kotlinx.coroutines.flow.* @Suppress("LongParameterList", "LargeClass") class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, - quotesRepositoryV2: QuotesRepositoryV2, + quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -56,7 +56,7 @@ class CachedCurrenciesStatusesOperations( ) : BaseCurrenciesStatusesOperations, BaseCurrencyStatusOperations( currenciesRepository = currenciesRepository, - quotesRepositoryV2 = quotesRepositoryV2, + quotesRepository = quotesRepository, stakingRepository = stakingRepository, multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier, 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 672cd819f8..d8f0938dc9 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 @@ -19,7 +19,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.quotes.QuotesRepositoryV2 +import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase @@ -65,7 +65,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase, private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, - private val quotesRepositoryV2: QuotesRepositoryV2, + private val quotesRepository: QuotesRepository, private val swapTransactionRepository: SwapTransactionRepository, private val currencyChecksRepository: CurrencyChecksRepository, private val appCurrencyRepository: AppCurrencyRepository, @@ -1809,7 +1809,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private suspend fun Set.getQuotesOrEmpty(): Set { - return runCatching { quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = this) } + return runCatching { quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this) } .getOrNull() .orEmpty() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index 13c9843f25..8bc88d6346 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -6,7 +6,7 @@ import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.QuotesRepositoryV2 +import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent @@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.map internal class ExchangeStatusFactory @AssistedInject constructor( private val swapTransactionRepository: SwapTransactionRepository, private val swapRepository: SwapRepository, - private val quotesRepositoryV2: QuotesRepositoryV2, + private val quotesRepository: QuotesRepository, private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val swapTransactionStatusStore: SwapTransactionStatusStore, private val analyticsEventsHandler: AnalyticsEventHandler, @@ -190,7 +190,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private suspend fun Set.getQuotesOrEmpty(): Set { val rawIds = mapNotNull { it.rawCurrencyId }.toSet() - return runCatching { quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = rawIds) } + return runCatching { quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = rawIds) } .getOrNull() .orEmpty() } From 86f832d859c6950ee4adb06e879f2f3d62e12ab6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Jun 2025 12:30:17 +0400 Subject: [PATCH 10/99] Updated on 2026-08-14 --- .../com/tangem/common/test/data/quote/QuoteResponseExt.kt | 2 +- .../datasource/local/quote/converter/QuoteStatusConverter.kt | 2 +- .../data/onramp/converters/HotCryptoCurrencyConverter.kt | 2 +- .../com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt | 2 +- .../tangem/data/quotes/repository/DefaultQuotesRepository.kt | 2 +- .../tangem/data/quotes/single/DefaultSingleQuoteProducer.kt | 2 +- .../tangem/data/quotes/store/DefaultQuotesStatusesStore.kt | 2 +- .../java/com/tangem/data/quotes/store/QuotesStatusesStore.kt | 2 +- .../com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt | 2 +- .../data/quotes/repository/DefaultQuotesRepositoryTest.kt | 2 +- .../quotes/single/DefaultSingleQuoteStatusProducerTest.kt | 2 +- .../tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt | 2 +- .../com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt | 2 +- .../com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt | 2 +- .../kotlin/com/tangem/domain/models/quote}/QuoteStatus.kt | 2 +- .../main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt | 2 +- .../com/tangem/domain/onramp/model/HotCryptoCurrency.kt | 2 +- domain/quotes/build.gradle.kts | 1 - .../main/java/com/tangem/domain/quotes/QuotesRepository.kt | 2 +- .../com/tangem/domain/quotes/single/SingleQuoteProducer.kt | 2 +- .../com/tangem/domain/quotes/single/SingleQuoteSupplier.kt | 2 +- .../src/main/java/com/tangem/domain/tokens/model/Token.kt | 2 ++ .../domain/tokens/operations/BaseCurrencyStatusOperations.kt | 2 +- .../tokens/operations/CachedCurrenciesStatusesOperations.kt | 2 +- .../domain/tokens/operations/CurrencyStatusOperations.kt | 2 +- .../tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt | 2 +- .../test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt | 2 +- .../kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt | 4 ++-- .../onramp/hottokens/converter/HotTokenItemStateConverter.kt | 4 ++-- .../java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 2 +- .../factory/TokenDetailsSwapTransactionsStateConverter.kt | 4 ++-- .../state/factory/express/ExchangeStatusFactory.kt | 2 +- 32 files changed, 35 insertions(+), 34 deletions(-) rename domain/{tokens/models/src/main/java/com/tangem/domain/tokens/model => models/src/main/kotlin/com/tangem/domain/models/quote}/QuoteStatus.kt (98%) diff --git a/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt index 5e294cc4c0..5b53d66b34 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt @@ -3,7 +3,7 @@ package com.tangem.common.test.data.quote import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.quote.converter.QuoteStatusConverter import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): QuoteStatus { return QuoteStatusConverter(source = source).convert(value = mapOf(rawCurrencyId to this).entries.first()) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteStatusConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteStatusConverter.kt index d3ee90a729..b2213bd41b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteStatusConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteStatusConverter.kt @@ -3,7 +3,7 @@ package com.tangem.datasource.local.quote.converter import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.orZero diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt index c1ece25f69..2acf7a68bf 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt @@ -9,9 +9,9 @@ import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onramp.model.HotCryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.converter.Converter import java.math.BigDecimal diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt index f3f01a2e1d..dc54b85c71 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt @@ -5,9 +5,9 @@ import arrow.core.left import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.multi.MultiQuoteUpdater -import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt index b7f600a91a..c145d65260 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt @@ -2,8 +2,8 @@ package com.tangem.data.quotes.repository import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.QuotesRepository -import com.tangem.domain.tokens.model.QuoteStatus import timber.log.Timber import javax.inject.Inject diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt index 490804a7ea..bd262c25c8 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt @@ -1,8 +1,8 @@ package com.tangem.data.quotes.single import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt index 6cdb7bd157..5c7cf0f0ae 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt @@ -6,7 +6,7 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.quote.converter.QuoteStatusConverter import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.CoroutineScope diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt index 140b84c495..a9dfe1fac2 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt @@ -3,7 +3,7 @@ package com.tangem.data.quotes.store import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus import kotlinx.coroutines.flow.Flow /** Store of [QuoteStatus]'es set */ diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt index 849519a417..eb860d2de9 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt @@ -2,7 +2,7 @@ package com.tangem.data.quotes.store import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus /** Set [StatusSource] of quotes statuses as [StatusSource.CACHE] for [currenciesIds] */ internal suspend fun QuotesStatusesStore.setSourceAsCache(currenciesIds: Set) { diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt index 2484c2b302..07c29dad8f 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt @@ -5,8 +5,8 @@ import com.tangem.common.test.utils.ProvideTestModels import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.QuotesRepository -import com.tangem.domain.tokens.model.QuoteStatus import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.mockk diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt index 6bca20b07d..7506a5198a 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt @@ -5,8 +5,8 @@ import com.tangem.common.test.utils.getEmittedValues import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt index 6713800ad0..93b3bb2077 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt @@ -8,7 +8,7 @@ import com.tangem.common.test.utils.ProvideTestModels import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt index 6fb34da4e7..b9b3c6ee60 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt @@ -10,7 +10,7 @@ import com.tangem.common.test.utils.getEmittedValues import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt index f73cd4e58d..58a5c60d76 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -4,9 +4,9 @@ import arrow.core.None import arrow.core.Option import arrow.core.toOption import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.single.SingleQuoteProducer import com.tangem.domain.quotes.single.SingleQuoteSupplier -import com.tangem.domain.tokens.model.QuoteStatus import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOf diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/QuoteStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/quote/QuoteStatus.kt similarity index 98% rename from domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/QuoteStatus.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/quote/QuoteStatus.kt index 81fb8a8dd5..1aad20046d 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/QuoteStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/quote/QuoteStatus.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.tokens.model +package com.tangem.domain.models.quote import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt index fa6e95358c..acc51d2f5c 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt @@ -1,12 +1,12 @@ package com.tangem.domain.nft import arrow.core.Either +import com.tangem.domain.models.quote.fold import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.quotes.single.SingleQuoteProducer import com.tangem.domain.quotes.single.SingleQuoteSupplier -import com.tangem.domain.tokens.model.fold import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt index aeb549ea8f..4d8415b915 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt @@ -1,7 +1,7 @@ package com.tangem.domain.onramp.model import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus /** * Hot crypto currency diff --git a/domain/quotes/build.gradle.kts b/domain/quotes/build.gradle.kts index cc097e7300..27f51399d9 100644 --- a/domain/quotes/build.gradle.kts +++ b/domain/quotes/build.gradle.kts @@ -6,5 +6,4 @@ plugins { dependencies { api(projects.domain.core) api(projects.domain.models) - api(projects.domain.tokens.models) } \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt index 7da2b7f682..9431c2b3f2 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt @@ -1,7 +1,7 @@ package com.tangem.domain.quotes import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus /** * Quotes repository diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt index 4a8f30bccb..4815740b9c 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt @@ -2,7 +2,7 @@ package com.tangem.domain.quotes.single import com.tangem.domain.core.flow.FlowProducer import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus /** * Producer of quote [CryptoCurrency.RawID] diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt index dbbda17215..95375e7e6e 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt @@ -1,7 +1,7 @@ package com.tangem.domain.quotes.single import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus /** * Supplier of quote [SingleQuoteProducer.Params] diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt index 244197386c..3e8451b1ed 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt @@ -1,5 +1,7 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.models.quote.QuoteStatus + /** * Represents a domain model for a token (as part of a general list of tokens). * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 543594679c..d31b6b452f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -9,6 +9,7 @@ import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer @@ -23,7 +24,6 @@ import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 5f14c36d45..34df07a877 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -13,6 +13,7 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer @@ -31,7 +32,6 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.extractAddress diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 4e065d68c5..82c396b43b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -3,9 +3,9 @@ package com.tangem.domain.tokens.operations import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.QuoteStatus import java.math.BigDecimal internal class CurrencyStatusOperations( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt index 6ed4a05e88..0dca67d61a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -6,11 +6,11 @@ import arrow.core.raise.either import arrow.core.toNonEmptySetOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.operations.CurrencyStatusOperations diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index ea2555d8c5..12b9de0cbc 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -3,7 +3,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptySetOf import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 3a0e07cb0e..fddd277f37 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -3,9 +3,9 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.quote.fold import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.QuoteStatus -import com.tangem.domain.tokens.model.fold import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt index 887c06034a..4f55af238c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt @@ -9,9 +9,9 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.quote.fold import com.tangem.domain.onramp.model.HotCryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus -import com.tangem.domain.tokens.model.fold import com.tangem.utils.converter.Converter import java.math.BigDecimal 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 d8f0938dc9..cc21bc6469 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 @@ -19,13 +19,13 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency -import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 4c5cb72a5b..01835b8f16 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -17,9 +17,9 @@ import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.quote.mapData import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent -import com.tangem.domain.tokens.model.mapData import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatus.Companion.isFailed import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index 8bc88d6346..c12a0e8048 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -6,9 +6,9 @@ import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.SwapTransactionRepository From 77eed4f146f92db97a092ae2fe74ca63a9a71cc3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Jun 2025 12:15:16 +0400 Subject: [PATCH 11/99] Updated on 2026-08-14 --- .../common/blockaid/models/response/Trace.kt | 2 +- .../network/ethereum/WcEthTxHelper.kt | 10 +- .../model/WcEthTransactionParams.kt | 2 +- .../WcTransactionRequestBlockUMConverter.kt | 39 +++-- .../entity/approve/WcApproveTransactionUM.kt | 31 ---- .../entity/approve/WcCustomAllowanceUM.kt | 11 ++ .../entity/approve/WcSpendAllowanceUM.kt | 7 +- .../WcSendReceiveTransactionCheckResultsUM.kt | 2 + .../WcApproveTransactionModalBottomSheet.kt | 133 ------------------ .../ui/approve/WcSpendAllowanceItem.kt | 20 +-- .../blockaid/TransactionCheckResultsItem.kt | 5 +- .../ui/blockaid/WcEstimatedWalletChangeRow.kt | 2 +- .../WcEstimatedWalletChangeUMConverter.kt | 40 ++++++ .../WcEstimatedWalletChangesLoadingItem.kt | 5 +- .../WcEstimatedWalletChangesNotLoadedItem.kt | 2 +- .../WcSendAndReceiveBlockAidUiConverter.kt | 80 ++++++----- .../blockaid/WcSpendAllowanceUMConverter.kt | 21 +++ .../transaction/ui/common/WcSmallTitleItem.kt | 2 +- 18 files changed, 179 insertions(+), 235 deletions(-) delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcCustomAllowanceUM.kt delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Trace.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Trace.kt index 89f2f3db60..84040fc6a5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Trace.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Trace.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Trace( - @Json(name = "exposed") val exposed: Exposed, + @Json(name = "exposed") val exposed: Exposed?, @Json(name = "asset") val asset: NftAsset, ) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt index fdffc8a26d..a2fc3d4224 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt @@ -49,7 +49,9 @@ internal object WcEthTxHelper { .hexToBigDecimal() .movePointLeft(blockchain.decimals()) - val callData = CompiledSmartContractCallData(txParams.data.removePrefix(HEX_PREFIX).hexToBytes()) + val callData = txParams.data?.removePrefix(HEX_PREFIX)?.hexToBytes()?.let { + CompiledSmartContractCallData(it) + } return TransactionData.Uncompiled( amount = Amount(value, blockchain), fee = dAppFee, @@ -62,10 +64,10 @@ internal object WcEthTxHelper { ) } - fun getApprovedAmount(txData: String, result: CheckTransactionResult): ApprovedAmount? { + fun getApprovedAmount(txData: String?, result: CheckTransactionResult): ApprovedAmount? { val approvalMethodId = ApprovalERC20TokenCallData("", null).methodId - val isApprovalWcMethod = txData.startsWith(approvalMethodId) - if (!isApprovalWcMethod) return null + val isApprovalWcMethod = txData?.startsWith(approvalMethodId) + if (isApprovalWcMethod != true) return null val simulation = result.simulation as? SimulationResult.Success ?: return null val approves = (simulation.data as? SimulationData.Approve)?.approvedAmounts diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthTransactionParams.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthTransactionParams.kt index cdde8975d6..af30e62722 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthTransactionParams.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthTransactionParams.kt @@ -12,7 +12,7 @@ data class WcEthTransactionParams( val to: String?, @Json(name = "data") - val data: String, + val data: String?, @Json(name = "gas") val gas: String?, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt index 188c4bc5d2..241a8ba2bf 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt @@ -8,23 +8,40 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf +import org.json.JSONArray import javax.inject.Inject +private const val DATA_FIELD = "data" + internal class WcTransactionRequestBlockUMConverter @Inject constructor() : Converter { - override fun convert(value: Input) = WcTransactionRequestBlockUM( - persistentListOf( - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_signature_type), - description = value.rawSdkRequest.request.method, + override fun convert(value: Input): WcTransactionRequestBlockUM { + val params = value.rawSdkRequest.request.params + return WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_signature_type), + description = value.rawSdkRequest.request.method, + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_contents), + description = value.signModel?.humanMsg ?: params.tryExtractData() ?: params, + ), ), - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_contents), - description = value.signModel?.humanMsg ?: value.rawSdkRequest.request.params, - ), - ), - ) + ) + } + + private fun String.tryExtractData() = try { + val array = JSONArray(this) + if (array.length() > 0 && array.getJSONObject(0).has(DATA_FIELD)) { + array.getJSONObject(0).getString(DATA_FIELD) + } else { + null + } + } catch (ignore: Exception) { + null + } data class Input( val rawSdkRequest: WcSdkSessionRequest, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt deleted file mode 100644 index b8107b23c1..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.features.walletconnect.transaction.entity.approve - -import androidx.annotation.DrawableRes -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM - -internal data class WcApproveTransactionUM( - val transaction: WcApproveTransactionItemUM, - val transactionRequestInfo: WcTransactionRequestInfoUM, - val customAllowance: WcCustomAllowanceUM, -) - -internal data class WcApproveTransactionItemUM( - val onDismiss: () -> Unit, - val onSend: () -> Unit, - val appInfo: WcTransactionAppInfoContentUM, - val spendAllowance: WcSpendAllowanceUM? = null, - val walletName: String, - val networkInfo: WcNetworkInfoUM, - val networkFee: String? = null, - val isLoading: Boolean = false, -) : TangemBottomSheetConfigContent - -internal data class WcCustomAllowanceUM( - @DrawableRes val networkIconRes: Int, - val tokenIconUrl: String, - val amountText: String, - val isUnlimited: Boolean, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcCustomAllowanceUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcCustomAllowanceUM.kt new file mode 100644 index 0000000000..28f8405259 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcCustomAllowanceUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.walletconnect.transaction.entity.approve + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +internal data class WcCustomAllowanceUM( + @DrawableRes val networkIconRes: Int, + val tokenIconUrl: String, + val amountText: String, + val isUnlimited: Boolean, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt index d3609afa63..1a7b10dca6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt @@ -1,6 +1,9 @@ package com.tangem.features.walletconnect.transaction.entity.approve +import com.tangem.core.ui.extensions.TextReference + internal data class WcSpendAllowanceUM( - val amountText: String, - val tokenImageUrl: String, + val amountText: TextReference, + val tokenSymbol: String, + val tokenImageUrl: String?, ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcSendReceiveTransactionCheckResultsUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcSendReceiveTransactionCheckResultsUM.kt index 418d483031..096af30724 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcSendReceiveTransactionCheckResultsUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcSendReceiveTransactionCheckResultsUM.kt @@ -1,9 +1,11 @@ package com.tangem.features.walletconnect.transaction.entity.blockaid import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM internal data class WcSendReceiveTransactionCheckResultsUM( val estimatedWalletChanges: WcEstimatedWalletChangesUM? = null, + val spendAllowance: WcSpendAllowanceUM? = null, val notificationText: TextReference? = null, val isLoading: Boolean = true, ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt deleted file mode 100644 index 6e1ffd76e9..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.tangem.features.walletconnect.transaction.ui.approve - -import android.content.res.Configuration -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle -import com.tangem.core.ui.components.divider.DividerWithPadding -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState -import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem -import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.approve.WcApproveTransactionItemUM -import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM -import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM -import com.tangem.features.walletconnect.transaction.ui.common.* - -@Composable -internal fun WcApproveTransactionModalBottomSheetContent( - state: WcApproveTransactionItemUM, - onClickTransactionRequest: () -> Unit, -) { - Column( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - Column( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .fillMaxWidth() - .animateContentSize(), - ) { - WcSmallTitleItem(R.string.wc_request_from) - WcAppInfoItem( - iconUrl = state.appInfo.appIcon, - title = state.appInfo.appName, - subtitle = state.appInfo.appSubtitle, - verifiedDAppState = state.appInfo.verifiedState, - ) - DividerWithPadding(start = 0.dp, end = 0.dp) - WcTransactionRequestItem( - iconRes = R.drawable.ic_doc_new_24, - modifier = Modifier - .fillMaxWidth() - .clickable { onClickTransactionRequest() } - .padding(TangemTheme.dimens.spacing12), - ) - } - Column(modifier = Modifier.padding(top = TangemTheme.dimens.spacing16)) { - if (state.spendAllowance != null) { - WcSpendAllowanceItem(state.spendAllowance) - Spacer(Modifier.height(TangemTheme.dimens.spacing16)) - } - WcSendTransactionItems( - walletName = state.walletName, - networkInfo = state.networkInfo, - networkFee = state.networkFee, - ) - WcTransactionRequestButtons( - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), - onDismiss = state.onDismiss, - onClickActiveButton = state.onSend, - activeButtonText = resourceReference(R.string.common_send), - isLoading = state.isLoading, - ) - } - } -} - -@Composable -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun WcApproveTransactionBottomSheetPreview( - @PreviewParameter(WcApproveTransactionStateProvider::class) state: WcApproveTransactionItemUM, -) { - TangemThemePreview { - TangemModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = state, - ), - containerColor = TangemTheme.colors.background.tertiary, - title = { - TangemModalBottomSheetTitle( - title = resourceReference(R.string.wallet_connect_title), - endIconRes = R.drawable.ic_close_24, - onEndClick = {}, - startIconRes = null, - onStartClick = {}, - ) - }, - content = { - WcApproveTransactionModalBottomSheetContent(state = state, onClickTransactionRequest = {}) - }, - ) - } -} - -private class WcApproveTransactionStateProvider : CollectionPreviewParameterProvider( - listOf( - WcApproveTransactionItemUM( - onDismiss = {}, - onSend = {}, - appInfo = WcTransactionAppInfoContentUM( - appName = "React App", - appIcon = "", - verifiedState = VerifiedDAppState.Verified {}, - appSubtitle = "react-app.walletconnect.com", - ), - spendAllowance = WcSpendAllowanceUM(amountText = "Unlimited USDT", tokenImageUrl = ""), - walletName = "Tangem 2.0", - networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - networkFee = "~ 0.22 $", - ), - ), -) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt index 2c1322217f..a6de96e855 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt @@ -13,7 +13,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import com.tangem.features.walletconnect.impl.R import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM @@ -23,17 +25,14 @@ import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: Modifier = Modifier) { Column( modifier = modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .clip(RoundedCornerShape(14.dp)) .background(TangemTheme.colors.background.action) .fillMaxWidth() - .padding( - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), + .padding(end = 12.dp, bottom = 12.dp), ) { WcSmallTitleItem(R.string.wc_allow_to_spend) - Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8)) + Spacer(modifier = Modifier.height(8.dp)) Row( verticalAlignment = Alignment.CenterVertically, @@ -46,15 +45,16 @@ internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: AsyncImage( model = spendAllowance.tokenImageUrl, modifier = Modifier - .size(TangemTheme.dimens.size24) + .padding(start = 12.dp) + .size(24.dp) .clip(CircleShape), contentDescription = null, ) - Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing12)) + Spacer(modifier = Modifier.width(8.dp)) AnimatedContent( - targetState = spendAllowance.amountText, + targetState = "${spendAllowance.amountText.resolveReference()} ${spendAllowance.tokenSymbol}", label = "Animate Spend allowance text", ) { amount -> Text( @@ -75,7 +75,7 @@ internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: color = TangemTheme.colors.text.tertiary, ) - Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing8)) + Spacer(modifier = Modifier.width(8.dp)) Icon( painter = painterResource(id = R.drawable.ic_edit_new_12), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt index 6a8cb9829e..6cf15fa757 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt @@ -19,12 +19,11 @@ import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM +import com.tangem.features.walletconnect.transaction.ui.approve.WcSpendAllowanceItem import kotlinx.collections.immutable.persistentListOf @Composable internal fun TransactionCheckResultsItem(item: WcSendReceiveTransactionCheckResultsUM, modifier: Modifier = Modifier) { - if (item.notificationText == null && item.estimatedWalletChanges == null) return - Column( modifier = modifier .fillMaxWidth(), @@ -38,6 +37,8 @@ internal fun TransactionCheckResultsItem(item: WcSendReceiveTransactionCheckResu } if (item.estimatedWalletChanges != null) { WcEstimatedWalletChangesItem(item.estimatedWalletChanges) + } else if (item.spendAllowance != null) { + WcSpendAllowanceItem(item.spendAllowance) } else { WcEstimatedWalletChangesNotLoadedItem() } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt index e4dcdea2ea..6e3dc9a87d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt @@ -63,7 +63,7 @@ internal fun WcEstimatedWalletChangeRow(item: WcEstimatedWalletChangeUM, modifie } } - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(8.dp)) AsyncImage( model = item.tokenIconUrl, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt new file mode 100644 index 0000000000..c51cc78fbb --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt @@ -0,0 +1,40 @@ +package com.tangem.features.walletconnect.transaction.ui.blockaid + +import com.domain.blockaid.models.transaction.simultation.AmountInfo +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM +import com.tangem.utils.converter.Converter +import java.math.BigDecimal +import javax.inject.Inject + +internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : + Converter { + + override fun convert(value: Input): WcEstimatedWalletChangeUM { + return when (val amountInfo = value.amountInfo) { + is AmountInfo.FungibleTokens -> WcEstimatedWalletChangeUM( + iconRes = value.iconRes, + title = resourceReference(value.titleRes), + description = "${value.sign} ${amountInfo.amount.amountText()} ${amountInfo.token.symbol}", + tokenIconUrl = amountInfo.token.logoUrl, + ) + is AmountInfo.NonFungibleTokens -> WcEstimatedWalletChangeUM( + iconRes = value.iconRes, + title = resourceReference(value.titleRes), + description = amountInfo.name, + tokenIconUrl = amountInfo.logoUrl, + ) + } + } + + private fun BigDecimal.amountText() = format { crypto("", DECIMALS_AMOUNT) } + + data class Input( + val amountInfo: AmountInfo, + val iconRes: Int, + val titleRes: Int, + val sign: String? = null, + ) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesLoadingItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesLoadingItem.kt index abd604faa3..6fee1c04e0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesLoadingItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesLoadingItem.kt @@ -36,7 +36,7 @@ internal fun WcEstimatedWalletChangesLoadingItem(modifier: Modifier = Modifier) Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 14.dp), + modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 14.dp), ) { CircularProgressIndicator( color = TangemTheme.colors.icon.accent, @@ -45,7 +45,8 @@ internal fun WcEstimatedWalletChangesLoadingItem(modifier: Modifier = Modifier) ) Text( modifier = modifier - .fillMaxWidth(), + .fillMaxWidth() + .padding(start = 8.dp), text = stringResourceSafe(R.string.wc_common_loading), color = TangemTheme.colors.text.disabled, style = TangemTheme.typography.body2, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesNotLoadedItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesNotLoadedItem.kt index 04671ce12c..de2a38ce6a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesNotLoadedItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesNotLoadedItem.kt @@ -35,7 +35,7 @@ internal fun WcEstimatedWalletChangesNotLoadedItem(modifier: Modifier = Modifier Text( modifier = modifier .fillMaxWidth() - .padding(start = 12.dp, bottom = 14.dp), + .padding(start = 12.dp, end = 12.dp, bottom = 14.dp), text = stringResourceSafe(R.string.wc_estimated_wallet_changes_not_simulated), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index 89890d3b27..9212d3adf1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -6,28 +6,34 @@ import com.domain.blockaid.models.transaction.ValidationResult import com.domain.blockaid.models.transaction.simultation.AmountInfo import com.domain.blockaid.models.transaction.simultation.SimulationData import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM +import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM +import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal import javax.inject.Inject -private const val DECIMALS_AMOUNT = 2 +internal const val DECIMALS_AMOUNT = 2 -internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor() : +internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( + private val estimatedWalletChangeUMConverter: WcEstimatedWalletChangeUMConverter, + private val spendAllowanceUMConverter: WcSpendAllowanceUMConverter, +) : Converter { override fun convert(value: CheckTransactionResult): WcSendReceiveTransactionCheckResultsUM { return WcSendReceiveTransactionCheckResultsUM( isLoading = false, notificationText = when (value.validation) { ValidationResult.SAFE, ValidationResult.FAILED_TO_VALIDATE -> null - ValidationResult.UNSAFE -> TextReference.Str("") // TODO("Add text after approve with designer") + // TODO("Add text after approve with designer") + ValidationResult.UNSAFE -> + TextReference.Str("The transaction approves tokens to a known malicious address") }, estimatedWalletChanges = (value.simulation as? SimulationResult.Success)?.data?.let { data -> when (data) { @@ -36,24 +42,38 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor() : val items: ImmutableList = ( data.send.map { when (it) { - is AmountInfo.FungibleTokens -> it.toUM( - iconRes = R.drawable.ic_send_new_24, - titleRes = R.string.common_send, + is AmountInfo.FungibleTokens -> estimatedWalletChangeUMConverter.convert( + WcEstimatedWalletChangeUMConverter.Input( + amountInfo = it, + iconRes = R.drawable.ic_receive_new_24, + titleRes = R.string.common_send, + sign = StringsSigns.MINUS, + ), ) - is AmountInfo.NonFungibleTokens -> it.toUM( - iconRes = R.drawable.ic_send_new_24, - titleRes = R.string.common_send, + is AmountInfo.NonFungibleTokens -> estimatedWalletChangeUMConverter.convert( + WcEstimatedWalletChangeUMConverter.Input( + amountInfo = it, + iconRes = R.drawable.ic_send_new_24, + titleRes = R.string.common_send, + ), ) } } + data.receive.map { when (it) { - is AmountInfo.FungibleTokens -> it.toUM( - iconRes = R.drawable.ic_receive_new_24, - titleRes = R.string.common_receive, + is AmountInfo.FungibleTokens -> estimatedWalletChangeUMConverter.convert( + WcEstimatedWalletChangeUMConverter.Input( + amountInfo = it, + iconRes = R.drawable.ic_receive_new_24, + titleRes = R.string.common_receive, + sign = StringsSigns.PLUS, + ), ) - is AmountInfo.NonFungibleTokens -> it.toUM( - iconRes = R.drawable.ic_receive_new_24, - titleRes = R.string.common_receive, + is AmountInfo.NonFungibleTokens -> estimatedWalletChangeUMConverter.convert( + WcEstimatedWalletChangeUMConverter.Input( + amountInfo = it, + iconRes = R.drawable.ic_receive_new_24, + titleRes = R.string.common_receive, + ), ) } } @@ -62,25 +82,15 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor() : } } }, + spendAllowance = (value.simulation as? SimulationResult.Success)?.data?.let { data -> + when (data) { + is SimulationData.SendAndReceive -> null + is SimulationData.Approve -> data.approvedAmounts.map { spendAllowanceUMConverter.convert(it) } + .firstOrNull() + } + }, ) } } -private fun AmountInfo.FungibleTokens.toUM(iconRes: Int, titleRes: Int): WcEstimatedWalletChangeUM { - val amountText = amount.format { crypto(token.symbol, DECIMALS_AMOUNT) } - return WcEstimatedWalletChangeUM( - iconRes = iconRes, - title = resourceReference(titleRes), - description = "- $amountText", - tokenIconUrl = token.logoUrl, - ) -} - -private fun AmountInfo.NonFungibleTokens.toUM(iconRes: Int, titleRes: Int): WcEstimatedWalletChangeUM { - return WcEstimatedWalletChangeUM( - iconRes = iconRes, - title = resourceReference(titleRes), - description = name, - tokenIconUrl = logoUrl, - ) -} \ No newline at end of file +internal fun BigDecimal.amountText() = format { crypto("", DECIMALS_AMOUNT) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt new file mode 100644 index 0000000000..2d9a1a739f --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.features.walletconnect.transaction.ui.blockaid + +import com.domain.blockaid.models.transaction.simultation.ApprovedAmount +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +internal class WcSpendAllowanceUMConverter @Inject constructor() : Converter { + + override fun convert(value: ApprovedAmount) = WcSpendAllowanceUM( + amountText = if (value.isUnlimited) { + TextReference.Res(R.string.wc_common_unlimited) + } else { + TextReference.Str(value.approvedAmount.amountText()) + }, + tokenSymbol = value.tokenInfo.symbol, + tokenImageUrl = value.tokenInfo.logoUrl, + ) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSmallTitleItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSmallTitleItem.kt index 501084213c..5cff971145 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSmallTitleItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSmallTitleItem.kt @@ -15,7 +15,7 @@ fun WcSmallTitleItem(@StringRes textRex: Int, modifier: Modifier = Modifier) { Text( modifier = modifier .fillMaxWidth() - .padding(top = 12.dp, start = 12.dp), + .padding(top = 12.dp, start = 12.dp, end = 12.dp), text = stringResourceSafe(textRex), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, From b5f0c9e0970438a7ffc490ac19ba97aaa07c8bda Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Jun 2025 12:15:26 +0400 Subject: [PATCH 12/99] Updated on 2026-08-14 --- .../connections/utils/WcAlertsFactory.kt | 29 ++++++++++++++++++- .../transaction/routes/WcTransactionRoutes.kt | 1 + 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt index e9ab26ccf8..723b7046bf 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt @@ -2,16 +2,19 @@ package com.tangem.features.walletconnect.connections.utils import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal object WcAlertsFactory { - internal fun createCommonTransactionAppInfoAlertUM(alertType: WcTransactionRoutes.Alert.Type) = when (alertType) { + fun createCommonTransactionAppInfoAlertUM(alertType: WcTransactionRoutes.Alert.Type) = when (alertType) { is WcTransactionRoutes.Alert.Type.Verified -> createVerifiedDomainAlert(alertType.appName) is WcTransactionRoutes.Alert.Type.UnknownDomain -> createUnknownDomainAlert() is WcTransactionRoutes.Alert.Type.UnsafeDomain -> createUnsafeDomainAlert() + is WcTransactionRoutes.Alert.Type.MaliciousDApp -> + createMaliciousDAppAlert(alertType.description, alertType.onClick) } fun createUnknownDomainAlert(activeButtonOnClick: (() -> Unit)? = null): MessageBottomSheetUMV2 { @@ -66,6 +69,30 @@ internal object WcAlertsFactory { } } + private fun createMaliciousDAppAlert( + description: String, + activeButtonOnClick: (() -> Unit), + ): MessageBottomSheetUMV2 { + return messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_knight_shield_32) { + type = MessageBottomSheetUMV2.Icon.Type.Warning + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.security_alert_title) + body = TextReference.Str(description) + } + primaryButton { + text = resourceReference(R.string.common_cancel) + onClick { closeBs() } + } + secondaryButton { + text = resourceReference(R.string.wc_alert_sign_anyway) + onClick { activeButtonOnClick() } + } + } + } + fun createVerifiedDomainAlert(appName: String): MessageBottomSheetUMV2 { return messageBottomSheetUM { infoBlock { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt index b762172be6..bddb365f90 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt @@ -21,6 +21,7 @@ internal sealed class WcTransactionRoutes : TangemBottomSheetConfigContent, Rout data class Verified(val appName: String) : Type() data object UnknownDomain : Type() data object UnsafeDomain : Type() + data class MaliciousDApp(val description: String, val onClick: () -> Unit) : Type() } } } \ No newline at end of file From 14c007d93a1cb90a1c216e41d9ec1c4d25d7b831 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Jun 2025 15:49:10 +0400 Subject: [PATCH 13/99] Updated on 2026-08-14 --- data/quotes/build.gradle.kts | 36 +++-- .../quotes/multi/DefaultMultiQuoteFetcher.kt | 8 +- .../QuotesUnsupportedCurrenciesIdAdapter.kt | 36 +++-- ...uotesUnsupportedCurrenciesIdAdapterTest.kt | 144 ++++++++++++++++++ 4 files changed, 190 insertions(+), 34 deletions(-) rename data/{tokens/src/main/kotlin/com/tangem/data/tokens => quotes/src/main/java/com/tangem/data/quotes}/utils/QuotesUnsupportedCurrenciesIdAdapter.kt (70%) create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index 41c386a5c4..2549fee133 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -14,29 +14,45 @@ tasks.withType().configureEach { } dependencies { + // region Project - Core implementation(projects.core.datasource) - implementation(projects.core.utils) + api(projects.core.utils) + // endregion + // region Project - Data implementation(projects.data.common) - implementation(projects.data.tokens) + // endregion - implementation(projects.domain.appCurrency.models) - implementation(projects.domain.models) - implementation(projects.domain.tokens.models) - implementation(projects.domain.quotes) - implementation(projects.domain.wallets.models) + // region Project - Domain + api(projects.domain.models) + api(projects.domain.quotes) + // endregion + // region Project - Libs + implementation(projects.libs.blockchainSdk) + // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Tangem SDKs + implementation(tangemDeps.blockchain) + // endregion + + // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi.kotlin) implementation(deps.timber) + // endregion - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) - + // region Tests testImplementation(deps.test.coroutine) testImplementation(deps.test.junit5) testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) testImplementation(projects.common.test) + // endregion } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt index 830d3afef4..1f09c109a0 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt @@ -5,7 +5,7 @@ import com.tangem.data.common.api.safeApiCallWithTimeout import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.data.quotes.store.setSourceAsCache import com.tangem.data.quotes.store.setSourceAsOnlyCache -import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter +import com.tangem.data.quotes.utils.QuotesUnsupportedCurrenciesIdAdapter import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.catchOn @@ -35,8 +35,6 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : MultiQuoteFetcher { - private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter() - override suspend fun invoke(params: MultiQuoteFetcher.Params) = Either.catchOn(dispatchers.default) { if (params.currenciesIds.isEmpty()) { Timber.d("No currencies to fetch quotes for") @@ -45,7 +43,7 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) - val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( + val replacementIdsResult = QuotesUnsupportedCurrenciesIdAdapter.replaceUnsupportedCurrencies( currenciesIds = params.currenciesIds.mapTo( destination = hashSetOf(), transform = CryptoCurrency.RawID::value, @@ -64,7 +62,7 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( onError = { error -> throw error }, ) - val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( + val updatedResponse = QuotesUnsupportedCurrenciesIdAdapter.getResponseWithUnsupportedCurrencies( response = response, filteredIds = replacementIdsResult.idsFiltered, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/quotes/src/main/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapter.kt similarity index 70% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index bf7779378a..72c9a1f66c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -1,23 +1,27 @@ -package com.tangem.data.tokens.utils +package com.tangem.data.quotes.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.QuotesResponse -/** - * Adapter to replace unsupported currencies for quotes request if it necessary - */ -class QuotesUnsupportedCurrenciesIdAdapter { +/** Adapter to replace unsupported currencies for quotes request if it necessary */ +internal object QuotesUnsupportedCurrenciesIdAdapter { - /** - * Replaces unsupported currencies id to it replacements for request - */ + /** Ethereum coin ID */ + private val ethCoinId = Blockchain.Ethereum.toCoinId() + + /** Map that contains unsupported currencies and their replacement for request */ + private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = l2BlockchainsCoinIds.associateWith { ethCoinId } + + /** Replaces unsupported currencies id [currenciesIds] to it replacements for request */ fun replaceUnsupportedCurrencies(currenciesIds: Set): ReplacementResult { val idsForRequest = mutableSetOf() val idsFiltered = mutableSetOf() + currenciesIds.forEach { currencyId -> val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[currencyId] + if (replacementId != null) { idsFiltered.add(currencyId) idsForRequest.add(replacementId) @@ -25,16 +29,17 @@ class QuotesUnsupportedCurrenciesIdAdapter { idsForRequest.add(currencyId) } } + return ReplacementResult(idsForRequest = idsForRequest, idsFiltered = idsFiltered) } - /** - * Recover previously replaced currencies to response that uses in application - */ + /** Recover previously replaced currencies [filteredIds] to [response] that uses in application */ fun getResponseWithUnsupportedCurrencies(response: QuotesResponse, filteredIds: Set): QuotesResponse { val updatedQuotes = mutableMapOf() + filteredIds.forEach { filteredId -> val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[filteredId] + if (replacementId != null) { val quoteReplacement = response.quotes[replacementId] if (quoteReplacement != null) { @@ -42,16 +47,9 @@ class QuotesUnsupportedCurrenciesIdAdapter { } } } + return response.copy(quotes = updatedQuotes + response.quotes) } data class ReplacementResult(val idsForRequest: Set, val idsFiltered: Set) - - companion object { - /** - * Map that contains unsupported currencies and their replacement for request - */ - private val ethCoinId = Blockchain.Ethereum.toCoinId() - private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = l2BlockchainsCoinIds.associateWith { ethCoinId } - } } \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt new file mode 100644 index 0000000000..3a4f3abb14 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt @@ -0,0 +1,144 @@ +package com.tangem.data.quotes.utils + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.quotes.utils.QuotesUnsupportedCurrenciesIdAdapter.ReplacementResult +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.datasource.api.tangemTech.models.QuotesResponse.Quote +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuotesUnsupportedCurrenciesIdAdapterTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ReplaceUnsupportedCurrencies { + + @ParameterizedTest + @ProvideTestModels + fun replaceUnsupportedCurrencies(model: ReplaceUnsupportedCurrenciesModel) { + // Act + val actual = QuotesUnsupportedCurrenciesIdAdapter.replaceUnsupportedCurrencies( + currenciesIds = model.currenciesIds, + ) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + ReplaceUnsupportedCurrenciesModel( + currenciesIds = l2BlockchainsCoinIds.toSet(), + expected = ReplacementResult( + idsForRequest = l2BlockchainsCoinIds.mapTo(hashSetOf()) { + Blockchain.Ethereum.toCoinId() + }, + idsFiltered = l2BlockchainsCoinIds.toSet(), + ), + ), + ReplaceUnsupportedCurrenciesModel( + currenciesIds = notL2BlockchainsCoinIds, + expected = ReplacementResult(idsForRequest = notL2BlockchainsCoinIds, idsFiltered = emptySet()), + ), + ReplaceUnsupportedCurrenciesModel( + currenciesIds = blockchainCoinIds, + expected = ReplacementResult( + idsForRequest = Blockchain.entries + .filterNot(Blockchain::isTestnet) + .mapTo(hashSetOf()) { + if (it.isL2EthereumNetwork()) { + Blockchain.Ethereum.toCoinId() + } else { + it.toCoinId() + } + }, + idsFiltered = l2BlockchainsCoinIds.toSet(), + ), + ), + ) + } + + data class ReplaceUnsupportedCurrenciesModel( + val currenciesIds: Set, + val expected: ReplacementResult, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetResponseWithUnsupportedCurrencies { + + private val zeroQuote = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ZERO) + private val ethQuote = "ethereum" to zeroQuote + + @ParameterizedTest + @ProvideTestModels + fun getResponseWithUnsupportedCurrencies(model: GetResponseWithUnsupportedCurrenciesModel) { + // Act + val actual = QuotesUnsupportedCurrenciesIdAdapter.getResponseWithUnsupportedCurrencies( + response = model.response, + filteredIds = model.filteredIds, + ) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetResponseWithUnsupportedCurrenciesModel( + response = QuotesResponse(quotes = emptyMap()), + filteredIds = emptySet(), + expected = QuotesResponse(quotes = emptyMap()), + ), + GetResponseWithUnsupportedCurrenciesModel( + response = createQuotesResponse(ethQuote), + filteredIds = emptySet(), + expected = createQuotesResponse(ethQuote), + ), + GetResponseWithUnsupportedCurrenciesModel( + response = createQuotesResponse( + "arbitrum-one" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE), + ethQuote, + ), + filteredIds = blockchainCoinIds, + expected = createQuotesResponse( + *l2BlockchainsCoinIds.map { it to zeroQuote }.toTypedArray(), + "arbitrum-one" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE), + ethQuote, + ), + ), + ) + + private fun createQuotesResponse(vararg quotes: Pair): QuotesResponse { + return QuotesResponse(quotes = quotes.toMap()) + } + } + + data class GetResponseWithUnsupportedCurrenciesModel( + val response: QuotesResponse, + val filteredIds: Set, + val expected: QuotesResponse, + ) + + private companion object { + + val blockchainCoinIds = Blockchain.entries + .filterNot(Blockchain::isTestnet) + .mapTo(destination = hashSetOf(), transform = Blockchain::toCoinId) + + val notL2BlockchainsCoinIds = Blockchain.entries + .filterNot { it.isTestnet() || it.isL2EthereumNetwork() } + .mapTo(hashSetOf()) { it.toCoinId() } + } +} \ No newline at end of file From 9b281427a1e4bd8836355146d5581a0408d2fe50 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Jun 2025 19:31:09 +0500 Subject: [PATCH 14/99] Updated on 2026-08-14 --- .../features/walletconnect/connections/ui/WcAppInfoBS.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt index 29f564d601..9e2490c0f9 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt @@ -349,12 +349,7 @@ private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier } Icon( modifier = Modifier.size(width = 18.dp, height = 24.dp), - painter = painterResource( - id = when (networksInfo) { - is WcNetworksInfo.ContainsAllRequiredNetworks -> R.drawable.ic_select_18_24 - is WcNetworksInfo.MissingRequiredNetworkInfo -> R.drawable.ic_chevron_right_24 - }, - ), + painter = painterResource(id = R.drawable.ic_select_18_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, ) From c38b5836ffad82338052af9ec403f3274b3ebf64 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Jun 2025 19:16:32 +0400 Subject: [PATCH 15/99] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 6 +-- .../tap/di/domain/MarketsDomainModule.kt | 12 +++--- .../tangem/tap/di/domain/NFTDomainModule.kt | 12 +++--- .../tap/di/domain/TokensDomainModule.kt | 40 +++++++++---------- .../data/quotes/di/QuoteFetcherModule.kt | 30 -------------- .../quotes/di/QuoteProducerFactoryModule.kt | 18 --------- .../quotes/di/QuoteStatusFetcherModule.kt | 24 +++++++++++ .../di/QuoteStatusProducerFactoryModule.kt | 20 ++++++++++ ...Module.kt => QuoteStatusSupplierModule.kt} | 10 ++--- .../tangem/data/quotes/di/QuotesDataModule.kt | 24 ++++++++++- ...r.kt => DefaultMultiQuoteStatusFetcher.kt} | 22 +++++----- .../quotes/multi/DefaultMultiQuoteUpdater.kt | 24 +++++------ .../repository/DefaultQuotesRepository.kt | 3 +- .../single/DefaultSingleQuoteFetcher.kt | 17 -------- .../single/DefaultSingleQuoteStatusFetcher.kt | 22 ++++++++++ ...kt => DefaultSingleQuoteStatusProducer.kt} | 22 +++++----- .../DefaultMultiQuoteStatusFetcherTest.kt | 18 ++++----- .../DefaultMultiQuoteStatusUpdaterTest.kt | 30 +++++++------- .../DefaultSingleQuoteStatusFetcherTest.kt | 20 +++++----- .../DefaultSingleQuoteStatusProducerTest.kt | 8 ++-- .../managetokens/SaveManagedTokensUseCase.kt | 8 ++-- .../markets/GetCurrencyQuotesUseCase.kt | 10 ++--- .../domain/markets/SaveMarketTokensUseCase.kt | 8 ++-- .../tangem/domain/nft/FetchNFTPriceUseCase.kt | 8 ++-- .../tangem/domain/nft/GetNFTPriceUseCase.kt | 10 ++--- ...eFetcher.kt => MultiQuoteStatusFetcher.kt} | 4 +- ...Fetcher.kt => SingleQuoteStatusFetcher.kt} | 5 ++- ...oducer.kt => SingleQuoteStatusProducer.kt} | 4 +- .../single/SingleQuoteStatusSupplier.kt | 17 ++++++++ .../quotes/single/SingleQuoteSupplier.kt | 17 -------- .../tokens/AddCryptoCurrenciesUseCase.kt | 8 ++-- .../tokens/FetchCardTokenListUseCase.kt | 8 ++-- .../tokens/FetchCurrencyStatusUseCase.kt | 8 ++-- .../domain/tokens/FetchTokenListUseCase.kt | 8 ++-- ...RefreshMultiCurrencyWalletQuotesUseCase.kt | 8 ++-- .../BaseCurrencyStatusOperations.kt | 10 ++--- .../CachedCurrenciesStatusesOperations.kt | 24 +++++------ 37 files changed, 285 insertions(+), 262 deletions(-) delete mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt delete mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusFetcherModule.kt create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusProducerFactoryModule.kt rename data/quotes/src/main/java/com/tangem/data/quotes/di/{QuoteSupplierModule.kt => QuoteStatusSupplierModule.kt} (53%) rename data/quotes/src/main/java/com/tangem/data/quotes/multi/{DefaultMultiQuoteFetcher.kt => DefaultMultiQuoteStatusFetcher.kt} (78%) delete mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcher.kt rename data/quotes/src/main/java/com/tangem/data/quotes/single/{DefaultSingleQuoteProducer.kt => DefaultSingleQuoteStatusProducer.kt} (58%) rename domain/quotes/src/main/java/com/tangem/domain/quotes/multi/{MultiQuoteFetcher.kt => MultiQuoteStatusFetcher.kt} (75%) rename domain/quotes/src/main/java/com/tangem/domain/quotes/single/{SingleQuoteFetcher.kt => SingleQuoteStatusFetcher.kt} (61%) rename domain/quotes/src/main/java/com/tangem/domain/quotes/single/{SingleQuoteProducer.kt => SingleQuoteStatusProducer.kt} (82%) create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusSupplier.kt delete mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index c78eeae98f..a10bcc05e1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -5,7 +5,7 @@ import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles @@ -71,7 +71,7 @@ internal object ManageTokensDomainModule { derivationsRepository: DerivationsRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): SaveManagedTokensUseCase { @@ -82,7 +82,7 @@ internal object ManageTokensDomainModule { derivationsRepository = derivationsRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 95b8accbf9..d23de84aaf 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -6,8 +6,8 @@ import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.quotes.multi.MultiQuoteFetcher -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository @@ -52,8 +52,8 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetTokenQuotesUseCase(singleQuoteSupplier: SingleQuoteSupplier): GetCurrencyQuotesUseCase { - return GetCurrencyQuotesUseCase(singleQuoteSupplier = singleQuoteSupplier) + fun provideGetTokenQuotesUseCase(singleQuoteStatusSupplier: SingleQuoteStatusSupplier): GetCurrencyQuotesUseCase { + return GetCurrencyQuotesUseCase(singleQuoteStatusSupplier = singleQuoteStatusSupplier) } @Provides @@ -64,7 +64,7 @@ object MarketsDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): SaveMarketTokensUseCase { @@ -74,7 +74,7 @@ object MarketsDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 07c16b3c3f..5f525bc14c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -3,8 +3,8 @@ package com.tangem.tap.di.domain import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.nft.* import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.quotes.single.SingleQuoteFetcher -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -92,18 +92,18 @@ internal object NFTDomainModule { @Singleton fun provideGetNFTPriceUseCase( nftRepository: NFTRepository, - singleQuoteSupplier: SingleQuoteSupplier, + singleQuoteStatusSupplier: SingleQuoteStatusSupplier, ): GetNFTPriceUseCase { - return GetNFTPriceUseCase(nftRepository, singleQuoteSupplier) + return GetNFTPriceUseCase(nftRepository, singleQuoteStatusSupplier) } @Provides @Singleton fun provideFetchNFTPriceUseCase( nftRepository: NFTRepository, - singleQuoteFetcher: SingleQuoteFetcher, + singleQuoteStatusFetcher: SingleQuoteStatusFetcher, ): FetchNFTPriceUseCase { - return FetchNFTPriceUseCase(nftRepository, singleQuoteFetcher) + return FetchNFTPriceUseCase(nftRepository, singleQuoteStatusFetcher) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index f05324a2f1..625239ab7c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -9,8 +9,8 @@ import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.QuotesRepository -import com.tangem.domain.quotes.multi.MultiQuoteFetcher -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher @@ -42,7 +42,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): AddCryptoCurrenciesUseCase { @@ -50,7 +50,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) @@ -62,7 +62,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchTokenListUseCase { @@ -70,7 +70,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) @@ -161,7 +161,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchCurrencyStatusUseCase { @@ -169,7 +169,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, singleNetworkStatusFetcher = singleNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) @@ -181,7 +181,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchCardTokenListUseCase { @@ -189,7 +189,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) @@ -346,11 +346,11 @@ internal object TokensDomainModule { @Singleton fun provideRefreshMultiCurrencyWalletQuotesUseCase( currenciesRepository: CurrenciesRepository, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, ): RefreshMultiCurrencyWalletQuotesUseCase { return RefreshMultiCurrencyWalletQuotesUseCase( currenciesRepository = currenciesRepository, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, ) } @@ -373,8 +373,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, - singleQuoteSupplier: SingleQuoteSupplier, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, ): BaseCurrenciesStatusesOperations { @@ -385,8 +385,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, - singleQuoteSupplier = singleQuoteSupplier, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, @@ -403,8 +403,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, - singleQuoteSupplier: SingleQuoteSupplier, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, ): BaseCurrencyStatusOperations { @@ -415,8 +415,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, - singleQuoteSupplier = singleQuoteSupplier, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt deleted file mode 100644 index 1bdef12d69..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.data.quotes.di - -import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher -import com.tangem.data.quotes.multi.DefaultMultiQuoteUpdater -import com.tangem.data.quotes.single.DefaultSingleQuoteFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher -import com.tangem.domain.quotes.multi.MultiQuoteUpdater -import com.tangem.domain.quotes.single.SingleQuoteFetcher -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface QuoteFetcherModule { - - @Binds - @Singleton - fun bindMultiQuoteFetcher(impl: DefaultMultiQuoteFetcher): MultiQuoteFetcher - - @Binds - @Singleton - fun bindMultiQuoteUpdater(impl: DefaultMultiQuoteUpdater): MultiQuoteUpdater - - @Binds - @Singleton - fun bindSingleQuoteFetcher(impl: DefaultSingleQuoteFetcher): SingleQuoteFetcher -} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt deleted file mode 100644 index 0fc3d0faed..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.data.quotes.di - -import com.tangem.data.quotes.single.DefaultSingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteProducer -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface QuoteProducerFactoryModule { - - @Binds - @Singleton - fun bindSingleQuoteProducerFactory(impl: DefaultSingleQuoteProducer.Factory): SingleQuoteProducer.Factory -} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusFetcherModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusFetcherModule.kt new file mode 100644 index 0000000000..8f425800fe --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusFetcherModule.kt @@ -0,0 +1,24 @@ +package com.tangem.data.quotes.di + +import com.tangem.data.quotes.multi.DefaultMultiQuoteStatusFetcher +import com.tangem.data.quotes.single.DefaultSingleQuoteStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface QuoteStatusFetcherModule { + + @Binds + @Singleton + fun bindMultiQuoteStatusFetcher(impl: DefaultMultiQuoteStatusFetcher): MultiQuoteStatusFetcher + + @Binds + @Singleton + fun bindSingleQuoteStatusFetcher(impl: DefaultSingleQuoteStatusFetcher): SingleQuoteStatusFetcher +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusProducerFactoryModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusProducerFactoryModule.kt new file mode 100644 index 0000000000..a6b653ba03 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusProducerFactoryModule.kt @@ -0,0 +1,20 @@ +package com.tangem.data.quotes.di + +import com.tangem.data.quotes.single.DefaultSingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface QuoteStatusProducerFactoryModule { + + @Binds + @Singleton + fun bindSingleQuoteStatusProducerFactory( + impl: DefaultSingleQuoteStatusProducer.Factory, + ): SingleQuoteStatusProducer.Factory +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusSupplierModule.kt similarity index 53% rename from data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusSupplierModule.kt index c31b91a7c7..135f3b3460 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusSupplierModule.kt @@ -1,7 +1,7 @@ package com.tangem.data.quotes.di -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -10,12 +10,12 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object QuoteSupplierModule { +internal object QuoteStatusSupplierModule { @Provides @Singleton - fun provideSingleQuoteSupplier(factory: SingleQuoteProducer.Factory): SingleQuoteSupplier { - return object : SingleQuoteSupplier( + fun provideSingleQuoteStatusSupplier(factory: SingleQuoteStatusProducer.Factory): SingleQuoteStatusSupplier { + return object : SingleQuoteStatusSupplier( factory = factory, keyCreator = { "single_quote_${it.rawCurrencyId.value}" }, ) {} diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt index e25057bc21..0cf87628bb 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt @@ -4,15 +4,19 @@ import android.content.Context import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi +import com.tangem.data.quotes.multi.DefaultMultiQuoteUpdater import com.tangem.data.quotes.repository.DefaultQuotesRepository import com.tangem.data.quotes.store.DefaultQuotesStatusesStore import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteUpdater import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -51,5 +55,23 @@ internal object QuotesDataModule { @Singleton @Provides - fun providesQuotesRepository(impl: DefaultQuotesRepository): QuotesRepository = impl + fun providesQuotesRepository(quotesStatusesStore: QuotesStatusesStore): QuotesRepository { + return DefaultQuotesRepository(quotesStatusesStore = quotesStatusesStore) + } + + @Singleton + @Provides + fun bindMultiQuoteUpdater( + appCurrencyResponseStore: AppCurrencyResponseStore, + quotesStatusesStore: QuotesStatusesStore, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + dispatchers: CoroutineDispatcherProvider, + ): MultiQuoteUpdater { + return DefaultMultiQuoteUpdater( + appCurrencyResponseStore = appCurrencyResponseStore, + quotesStatusesStore = quotesStatusesStore, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + dispatchers = dispatchers, + ) + } } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt similarity index 78% rename from data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt index 1f09c109a0..c605e43574 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt @@ -10,7 +10,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber @@ -18,30 +18,30 @@ import javax.inject.Inject import javax.inject.Singleton /** - * Default implementation of [MultiQuoteFetcher] + * Default implementation of [MultiQuoteStatusFetcher] * * @property tangemTechApi tangemTech api * @property appCurrencyResponseStore app currency response store - * @property quotesStore quotes store + * @property quotesStatusesStore quotes store * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ @Singleton -internal class DefaultMultiQuoteFetcher @Inject constructor( +internal class DefaultMultiQuoteStatusFetcher @Inject constructor( private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, - private val quotesStore: QuotesStatusesStore, + private val quotesStatusesStore: QuotesStatusesStore, private val dispatchers: CoroutineDispatcherProvider, -) : MultiQuoteFetcher { +) : MultiQuoteStatusFetcher { - override suspend fun invoke(params: MultiQuoteFetcher.Params) = Either.catchOn(dispatchers.default) { + override suspend fun invoke(params: MultiQuoteStatusFetcher.Params) = Either.catchOn(dispatchers.default) { if (params.currenciesIds.isEmpty()) { Timber.d("No currencies to fetch quotes for") return@catchOn } - quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) + quotesStatusesStore.setSourceAsCache(currenciesIds = params.currenciesIds) val replacementIdsResult = QuotesUnsupportedCurrenciesIdAdapter.replaceUnsupportedCurrencies( currenciesIds = params.currenciesIds.mapTo( @@ -67,14 +67,14 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( filteredIds = replacementIdsResult.idsFiltered, ) - quotesStore.store(values = updatedResponse.quotes) + quotesStatusesStore.store(values = updatedResponse.quotes) } .onLeft { Timber.e(it) - quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) + quotesStatusesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } - private suspend fun getAppCurrencyId(params: MultiQuoteFetcher.Params): String { + private suspend fun getAppCurrencyId(params: MultiQuoteStatusFetcher.Params): String { val appCurrencyId = params.appCurrencyId ?: appCurrencyResponseStore.getSyncOrNull()?.id diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt index dc54b85c71..73e2446946 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt @@ -6,7 +6,7 @@ import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteUpdater import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -17,24 +17,21 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton /** * Default implementation of [MultiQuoteUpdater] which updates quotes when the app currency changes * * @property appCurrencyResponseStore app currency response store - * @property quotesStore quotes store - * @property multiQuoteFetcher multi quote fetcher + * @property quotesStatusesStore quotes store + * @property multiQuoteStatusFetcher multi quote status fetcher * @param dispatchers dispatchers * [REDACTED_AUTHOR] */ -@Singleton -internal class DefaultMultiQuoteUpdater @Inject constructor( +internal class DefaultMultiQuoteUpdater( private val appCurrencyResponseStore: AppCurrencyResponseStore, - private val quotesStore: QuotesStatusesStore, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val quotesStatusesStore: QuotesStatusesStore, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, dispatchers: CoroutineDispatcherProvider, ) : MultiQuoteUpdater { @@ -60,11 +57,14 @@ internal class DefaultMultiQuoteUpdater @Inject constructor( .distinctUntilChanged() .filterNotNull() .mapLatest { appCurrency -> - val currenciesIds = quotesStore.getAllSyncOrNull().orEmpty() + val currenciesIds = quotesStatusesStore.getAllSyncOrNull().orEmpty() .mapTo(destination = hashSetOf(), transform = QuoteStatus::rawCurrencyId) - multiQuoteFetcher( - params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrency.id), + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( + currenciesIds = currenciesIds, + appCurrencyId = appCurrency.id, + ), ) .onLeft(Timber::e) } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt index c145d65260..d170b82c5c 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt @@ -5,7 +5,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.QuotesRepository import timber.log.Timber -import javax.inject.Inject /** * Default implementation of [QuotesRepository] @@ -14,7 +13,7 @@ import javax.inject.Inject * [REDACTED_AUTHOR] */ -internal class DefaultQuotesRepository @Inject constructor( +internal class DefaultQuotesRepository( private val quotesStatusesStore: QuotesStatusesStore, ) : QuotesRepository { diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt deleted file mode 100644 index 9cd56dacc2..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.data.quotes.single - -import com.tangem.domain.quotes.multi.MultiQuoteFetcher -import com.tangem.domain.quotes.single.SingleQuoteFetcher -import javax.inject.Inject - -internal class DefaultSingleQuoteFetcher @Inject constructor( - private val multiQuoteFetcher: MultiQuoteFetcher, -) : SingleQuoteFetcher { - - override suspend fun invoke(params: SingleQuoteFetcher.Params) = multiQuoteFetcher.invoke( - MultiQuoteFetcher.Params( - currenciesIds = setOf(params.rawCurrencyId), - appCurrencyId = params.appCurrencyId, - ), - ) -} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcher.kt new file mode 100644 index 0000000000..44dc7157d8 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcher.kt @@ -0,0 +1,22 @@ +package com.tangem.data.quotes.single + +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher +import javax.inject.Inject + +/** + * Default implementation of [SingleQuoteStatusFetcher] + * + * @property multiQuoteStatusFetcher fetcher of quotes statuses + */ +internal class DefaultSingleQuoteStatusFetcher @Inject constructor( + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, +) : SingleQuoteStatusFetcher { + + override suspend fun invoke(params: SingleQuoteStatusFetcher.Params) = multiQuoteStatusFetcher.invoke( + MultiQuoteStatusFetcher.Params( + currenciesIds = setOf(params.rawCurrencyId), + appCurrencyId = params.appCurrencyId, + ), + ) +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt similarity index 58% rename from data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt index bd262c25c8..692d5cb24a 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt @@ -2,7 +2,7 @@ package com.tangem.data.quotes.single import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -13,28 +13,28 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapNotNull /** - * Default implementation of [SingleQuoteProducer] + * Default implementation of [SingleQuoteStatusProducer] * - * @property params params - * @property quotesStore quotes store + * @property params params + * @property quotesStatusesStore quotes store */ -internal class DefaultSingleQuoteProducer @AssistedInject constructor( - @Assisted val params: SingleQuoteProducer.Params, - private val quotesStore: QuotesStatusesStore, +internal class DefaultSingleQuoteStatusProducer @AssistedInject constructor( + @Assisted val params: SingleQuoteStatusProducer.Params, + private val quotesStatusesStore: QuotesStatusesStore, private val dispatchers: CoroutineDispatcherProvider, -) : SingleQuoteProducer { +) : SingleQuoteStatusProducer { override val fallback: QuoteStatus = QuoteStatus(rawCurrencyId = params.rawCurrencyId) override fun produce(): Flow { - return quotesStore.get() + return quotesStatusesStore.get() .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } } .distinctUntilChanged() .flowOn(dispatchers.default) } @AssistedFactory - interface Factory : SingleQuoteProducer.Factory { - override fun create(params: SingleQuoteProducer.Params): DefaultSingleQuoteProducer + interface Factory : SingleQuoteStatusProducer.Factory { + override fun create(params: SingleQuoteStatusProducer.Params): DefaultSingleQuoteStatusProducer } } \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt index 4969adec58..4db5f02c4f 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt @@ -12,7 +12,7 @@ import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify @@ -31,16 +31,16 @@ internal class DefaultMultiQuoteStatusFetcherTest { private val appCurrencyResponseStore = mockk(relaxed = true) private val quotesStore = mockk(relaxed = true) - private val fetcher = DefaultMultiQuoteFetcher( + private val fetcher = DefaultMultiQuoteStatusFetcher( tangemTechApi = tangemTechApi, appCurrencyResponseStore = appCurrencyResponseStore, - quotesStore = quotesStore, + quotesStatusesStore = quotesStore, dispatchers = TestingCoroutineDispatcherProvider(), ) @Test fun `fetch quotes successfully`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -70,7 +70,7 @@ internal class DefaultMultiQuoteStatusFetcherTest { @Test fun `fetch quotes successfully if currenciesIds from params is empty`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = null) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = null) val actual = fetcher(params) @@ -88,7 +88,7 @@ internal class DefaultMultiQuoteStatusFetcherTest { @Test fun `fetch quotes successfully if appCurrencyId from params is not null`() = runTest { val appCurrencyId = "usd" - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) val coinIds = "BTC,ETH" coEvery { @@ -116,7 +116,7 @@ internal class DefaultMultiQuoteStatusFetcherTest { @Test fun `fetch quotes failure because appCurrencyId from params is blank`() = runTest { val appCurrencyId = "" - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) val actual = fetcher(params) @@ -139,7 +139,7 @@ internal class DefaultMultiQuoteStatusFetcherTest { @Test fun `fetch quotes failure because api request failed`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -170,7 +170,7 @@ internal class DefaultMultiQuoteStatusFetcherTest { @Test fun `fetch quotes failure because app currency not found`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt index 0e7eeae889..9d47ec8857 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt @@ -6,7 +6,7 @@ import com.tangem.common.test.utils.getEmittedValues import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.flow.* @@ -20,12 +20,12 @@ internal class DefaultMultiQuoteStatusUpdaterTest { private val appCurrencyResponseStore: AppCurrencyResponseStore = mockk() private val quotesStore: QuotesStatusesStore = mockk() - private val multiQuoteFetcher: MultiQuoteFetcher = mockk() + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk() private val multiQuoteUpdater = DefaultMultiQuoteUpdater( appCurrencyResponseStore = appCurrencyResponseStore, - quotesStore = quotesStore, - multiQuoteFetcher = multiQuoteFetcher, + quotesStatusesStore = quotesStore, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -36,8 +36,8 @@ internal class DefaultMultiQuoteStatusUpdaterTest { every { appCurrencyResponseStore.get() } returns appCurrencyFlow coEvery { quotesStore.getAllSyncOrNull() } returns emptySet() - val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) - coEvery { multiQuoteFetcher(params) } returns Unit.right() + val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) + coEvery { multiQuoteStatusFetcher(params) } returns Unit.right() val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() @@ -48,7 +48,7 @@ internal class DefaultMultiQuoteStatusUpdaterTest { coVerifyOrder { quotesStore.getAllSyncOrNull() - multiQuoteFetcher(params) + multiQuoteStatusFetcher(params) } } @@ -59,8 +59,8 @@ internal class DefaultMultiQuoteStatusUpdaterTest { every { appCurrencyResponseStore.get() } returns appCurrencyFlow coEvery { quotesStore.getAllSyncOrNull() } returns emptySet() - val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) - coEvery { multiQuoteFetcher(params) } returns Unit.right() + val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) + coEvery { multiQuoteStatusFetcher(params) } returns Unit.right() val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() @@ -71,7 +71,7 @@ internal class DefaultMultiQuoteStatusUpdaterTest { coVerifyOrder { quotesStore.getAllSyncOrNull() - multiQuoteFetcher(params) + multiQuoteStatusFetcher(params) } } @@ -90,7 +90,7 @@ internal class DefaultMultiQuoteStatusUpdaterTest { coVerify(inverse = true) { quotesStore.getAllSyncOrNull() - multiQuoteFetcher(any()) + multiQuoteStatusFetcher(any()) } } @@ -111,8 +111,8 @@ internal class DefaultMultiQuoteStatusUpdaterTest { every { appCurrencyResponseStore.get() } returns appCurrencyFlow coEvery { quotesStore.getAllSyncOrNull() } returns emptySet() - val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) - coEvery { multiQuoteFetcher(params) } returns Unit.right() + val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) + coEvery { multiQuoteStatusFetcher(params) } returns Unit.right() val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() @@ -126,7 +126,7 @@ internal class DefaultMultiQuoteStatusUpdaterTest { coVerify(inverse = true) { quotesStore.getAllSyncOrNull() - multiQuoteFetcher(any()) + multiQuoteStatusFetcher(any()) } innerFlow.emit(value = true) @@ -136,7 +136,7 @@ internal class DefaultMultiQuoteStatusUpdaterTest { coVerifyOrder { quotesStore.getAllSyncOrNull() - multiQuoteFetcher(params) + multiQuoteStatusFetcher(params) } } diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt index 131263866d..245d0364b9 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt @@ -2,7 +2,7 @@ package com.tangem.data.quotes.single import com.google.common.truth.Truth import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher +import com.tangem.data.quotes.multi.DefaultMultiQuoteStatusFetcher import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.data.quotes.store.setSourceAsCache import com.tangem.data.quotes.store.setSourceAsOnlyCache @@ -13,7 +13,7 @@ import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.single.SingleQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify @@ -29,18 +29,18 @@ internal class DefaultSingleQuoteStatusFetcherTest { private val appCurrencyResponseStore = mockk(relaxed = true) private val quotesStore = mockk(relaxed = true) - private val multiFetcher = DefaultMultiQuoteFetcher( + private val multiFetcher = DefaultMultiQuoteStatusFetcher( tangemTechApi = tangemTechApi, appCurrencyResponseStore = appCurrencyResponseStore, - quotesStore = quotesStore, + quotesStatusesStore = quotesStore, dispatchers = TestingCoroutineDispatcherProvider(), ) - private val singleFetcher = DefaultSingleQuoteFetcher(multiFetcher) + private val singleFetcher = DefaultSingleQuoteStatusFetcher(multiFetcher) @Test fun `fetch single quote successfully`() = runTest { - val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -68,7 +68,7 @@ internal class DefaultSingleQuoteStatusFetcherTest { @Test fun `fetch single quote successfully if appCurrencyId from params is not null`() = runTest { val appCurrencyId = "usd" - val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) val coinIds = "BTC" coEvery { @@ -89,7 +89,7 @@ internal class DefaultSingleQuoteStatusFetcherTest { @Test fun `fetch single quote failure because appCurrencyId from params is blank`() = runTest { val appCurrencyId = "" - val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) val actual = singleFetcher(params) @@ -106,7 +106,7 @@ internal class DefaultSingleQuoteStatusFetcherTest { @Test fun `fetch single quote failure because api request failed`() = runTest { - val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -134,7 +134,7 @@ internal class DefaultSingleQuoteStatusFetcherTest { @Test fun `fetch single quote failure because app currency not found`() = runTest { - val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt index 7506a5198a..ef5fccaf7f 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt @@ -6,7 +6,7 @@ import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk @@ -21,15 +21,15 @@ import java.math.BigDecimal */ internal class DefaultSingleQuoteStatusProducerTest { - private val params = SingleQuoteProducer.Params( + private val params = SingleQuoteStatusProducer.Params( rawCurrencyId = CryptoCurrency.RawID(value = "BTC"), ) private val quotesStore = mockk() - private val producer = DefaultSingleQuoteProducer( + private val producer = DefaultSingleQuoteStatusProducer( params = params, - quotesStore = quotesStore, + quotesStatusesStore = quotesStore, dispatchers = TestingCoroutineDispatcherProvider(), ) diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index f93f9ecbc4..42209641ec 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -8,7 +8,7 @@ import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository @@ -25,7 +25,7 @@ class SaveManagedTokensUseCase( private val derivationsRepository: DerivationsRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -127,8 +127,8 @@ class SaveManagedTokensUseCase( } private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, appCurrencyId = null, ), diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt index 58a5c60d76..ec0e1717c6 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -5,15 +5,15 @@ import arrow.core.Option import arrow.core.toOption import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map class GetCurrencyQuotesUseCase( - private val singleQuoteSupplier: SingleQuoteSupplier, + private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, ) { // TODO apply interval parameter [REDACTED_TASK_KEY] operator fun invoke( @@ -23,8 +23,8 @@ class GetCurrencyQuotesUseCase( ): Flow> { val rawId = currencyID.rawCurrencyId ?: return flowOf(None) - return singleQuoteSupplier( - params = SingleQuoteProducer.Params(rawCurrencyId = rawId), + return singleQuoteStatusSupplier( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawId), ) .map { (it.value as? QuoteStatus.Data).toOption() } .catch { emit(None) } diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index f61f911e14..24fd934625 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -6,7 +6,7 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository @@ -30,7 +30,7 @@ class SaveMarketTokensUseCase( private val currenciesRepository: CurrenciesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -108,8 +108,8 @@ class SaveMarketTokensUseCase( } private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, appCurrencyId = null, ), diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt index eaf7a5cdea..1193977eb9 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt @@ -3,11 +3,11 @@ package com.tangem.domain.nft import arrow.core.Either import com.tangem.domain.models.network.Network import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.quotes.single.SingleQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher class FetchNFTPriceUseCase( private val nftRepository: NFTRepository, - private val singleQuoteFetcher: SingleQuoteFetcher, + private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher, ) { suspend operator fun invoke(network: Network, appCurrencyId: String?): Either { @@ -15,8 +15,8 @@ class FetchNFTPriceUseCase( val nftCurrency = nftRepository.getNFTCurrency(network) val rawId = nftCurrency.id.rawCurrencyId ?: error("Invalid nft currency id") - singleQuoteFetcher( - params = SingleQuoteFetcher.Params( + singleQuoteStatusFetcher( + params = SingleQuoteStatusFetcher.Params( rawCurrencyId = rawId, appCurrencyId = appCurrencyId, ), diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt index acc51d2f5c..a728ce7b73 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt @@ -5,15 +5,15 @@ import com.tangem.domain.models.quote.fold import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class GetNFTPriceUseCase( private val nftRepository: NFTRepository, - private val singleQuoteSupplier: SingleQuoteSupplier, + private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, ) { suspend operator fun invoke(userWalletId: UserWalletId, nftAsset: NFTAsset): Either> { @@ -21,8 +21,8 @@ class GetNFTPriceUseCase( val nftCurrency = nftRepository.getNFTCurrency(nftAsset.network) val rawId = nftCurrency.id.rawCurrencyId ?: error("Invalid nft currency id") - singleQuoteSupplier( - params = SingleQuoteProducer.Params(rawCurrencyId = rawId), + singleQuoteStatusSupplier( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawId), ).map { quote -> val nftPrice = nftRepository.getNFTSalePrice( userWalletId = userWalletId, diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteStatusFetcher.kt similarity index 75% rename from domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt rename to domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteStatusFetcher.kt index d75234b93e..4f2ad37795 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteStatusFetcher.kt @@ -4,11 +4,11 @@ import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.models.currency.CryptoCurrency /** - * Fetcher of quotes + * Fetcher of quotes statuses for specified list of [CryptoCurrency.RawID] * [REDACTED_AUTHOR] */ -interface MultiQuoteFetcher : FlowFetcher { +interface MultiQuoteStatusFetcher : FlowFetcher { /** * Params diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusFetcher.kt similarity index 61% rename from domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt rename to domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusFetcher.kt index 5b315bc6f8..062c864638 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusFetcher.kt @@ -4,16 +4,17 @@ import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.models.currency.CryptoCurrency /** - * Fetcher of quote for [CryptoCurrency.RawID] + * Fetcher of quote for specified [CryptoCurrency.RawID] * [REDACTED_AUTHOR] */ -interface SingleQuoteFetcher : FlowFetcher { +interface SingleQuoteStatusFetcher : FlowFetcher { /** * Params * * @property rawCurrencyId crypto currency id + * @property appCurrencyId app currency id, if null then selected app currency will be used */ data class Params( val rawCurrencyId: CryptoCurrency.RawID, diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusProducer.kt similarity index 82% rename from domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt rename to domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusProducer.kt index 4815740b9c..81ff530551 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusProducer.kt @@ -9,9 +9,9 @@ import com.tangem.domain.models.quote.QuoteStatus * [REDACTED_AUTHOR] */ -interface SingleQuoteProducer : FlowProducer { +interface SingleQuoteStatusProducer : FlowProducer { data class Params(val rawCurrencyId: CryptoCurrency.RawID) - interface Factory : FlowProducer.Factory + interface Factory : FlowProducer.Factory } \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusSupplier.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusSupplier.kt new file mode 100644 index 0000000000..a203be8468 --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusSupplier.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.quotes.single + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.models.quote.QuoteStatus + +/** + * Supplier of quote [SingleQuoteStatusProducer.Params] + * + * @property factory factory for creating [SingleQuoteStatusProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class SingleQuoteStatusSupplier( + override val factory: SingleQuoteStatusProducer.Factory, + override val keyCreator: (SingleQuoteStatusProducer.Params) -> String, +) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt deleted file mode 100644 index 95375e7e6e..0000000000 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.domain.quotes.single - -import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.models.quote.QuoteStatus - -/** - * Supplier of quote [SingleQuoteProducer.Params] - * - * @property factory factory for creating [SingleQuoteProducer] - * @property keyCreator key creator - * -[REDACTED_AUTHOR] - */ -abstract class SingleQuoteSupplier( - override val factory: SingleQuoteProducer.Factory, - override val keyCreator: (SingleQuoteProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index 99e6c3fd81..c352a155f0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -7,7 +7,7 @@ import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher @@ -29,7 +29,7 @@ class AddCryptoCurrenciesUseCase( private val currenciesRepository: CurrenciesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -163,8 +163,8 @@ class AddCryptoCurrenciesUseCase( } private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId), appCurrencyId = null, ), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index f1aed7a00d..6eb34de235 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -7,7 +7,7 @@ import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository @@ -23,7 +23,7 @@ class FetchCardTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -83,8 +83,8 @@ class FetchCardTokenListUseCase( } private suspend fun fetchQuotes(currenciesIds: Set) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null), + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null), ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index b1feda7923..908d7f8ed7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -7,7 +7,7 @@ import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher @@ -31,7 +31,7 @@ class FetchCurrencyStatusUseCase( private val currenciesRepository: CurrenciesRepository, private val stakingRepository: StakingRepository, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -123,8 +123,8 @@ class FetchCurrencyStatusUseCase( } private suspend fun Raise.fetchQuote(currencyId: CryptoCurrency.ID) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = setOfNotNull(currencyId.rawCurrencyId), appCurrencyId = null, ), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index e093d9eba6..39bc2364dd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -9,7 +9,7 @@ import arrow.core.toNonEmptyListOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository @@ -33,7 +33,7 @@ class FetchTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -112,8 +112,8 @@ class FetchTokenListUseCase( } private suspend fun Raise.fetchQuotes(currenciesIds: Set) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet(), appCurrencyId = null, ), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt index 446ba616af..e9b9d3925b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt @@ -5,7 +5,7 @@ import arrow.core.getOrElse import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.tokens.error.QuotesError import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -15,7 +15,7 @@ import kotlinx.coroutines.coroutineScope class RefreshMultiCurrencyWalletQuotesUseCase( private val currenciesRepository: CurrenciesRepository, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { @@ -45,8 +45,8 @@ class RefreshMultiCurrencyWalletQuotesUseCase( } private suspend fun fetchQuotes(currenciesIds: Set) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = currenciesIds.mapNotNullTo(hashSetOf(), CryptoCurrency.ID::rawCurrencyId), appCurrencyId = null, ), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index d31b6b452f..09a6e9bbe8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -15,8 +15,8 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.quotes.QuotesRepository -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository @@ -45,7 +45,7 @@ abstract class BaseCurrencyStatusOperations( private val stakingRepository: StakingRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, - private val singleQuoteSupplier: SingleQuoteSupplier, + private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -191,7 +191,7 @@ abstract class BaseCurrencyStatusOperations( } val quote = cryptoCurrencyId.rawCurrencyId?.let { rawId -> - singleQuoteSupplier(params = SingleQuoteProducer.Params(rawCurrencyId = rawId)) + singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawId)) .firstOrNull() } ?.right() @@ -287,7 +287,7 @@ abstract class BaseCurrencyStatusOperations( ) val quotes = currency.id.rawCurrencyId?.let { - singleQuoteSupplier(params = SingleQuoteProducer.Params(rawCurrencyId = it)) + singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(rawCurrencyId = it)) .firstOrNull() } ?.right() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 34df07a877..ac01f26e39 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -19,9 +19,9 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.quotes.QuotesRepository -import com.tangem.domain.quotes.multi.MultiQuoteFetcher -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList @@ -48,8 +48,8 @@ class CachedCurrenciesStatusesOperations( private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, - private val singleQuoteSupplier: SingleQuoteSupplier, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, @@ -60,7 +60,7 @@ class CachedCurrenciesStatusesOperations( stakingRepository = stakingRepository, multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier, - singleQuoteSupplier = singleQuoteSupplier, + singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, tokensFeatureToggles = tokensFeatureToggles, ) { @@ -189,8 +189,8 @@ class CachedCurrenciesStatusesOperations( async { val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } - multiQuoteFetcher( - params = MultiQuoteFetcher.Params(currenciesIds = rawCurrenciesIds, appCurrencyId = null), + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params(currenciesIds = rawCurrenciesIds, appCurrencyId = null), ) }, async { @@ -282,8 +282,8 @@ class CachedCurrenciesStatusesOperations( } override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { - return singleQuoteSupplier( - params = SingleQuoteProducer.Params(rawCurrencyId = id), + return singleQuoteStatusSupplier( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = id), ) .map>> { setOf(it).right() } .distinctUntilChanged() @@ -361,8 +361,8 @@ class CachedCurrenciesStatusesOperations( rawCurrencyIds.onEach { launch { - singleQuoteSupplier( - params = SingleQuoteProducer.Params(rawCurrencyId = it), + singleQuoteStatusSupplier( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = it), ) .onEach { quote -> state.update { loadedStatuses -> From cd1ad461a48ac20c7cbdc463a151917d0e756d17 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Jun 2025 20:31:55 +0500 Subject: [PATCH 16/99] Updated on 2026-08-14 --- .../walletconnect/connections/model/WcConnectedAppInfoModel.kt | 2 +- .../connections/model/transformers/WcAppInfoTransformer.kt | 2 +- .../connections/model/transformers/WcSessionsTransformer.kt | 2 +- .../features/walletconnect/connections/ui/InternalComponents.kt | 1 + .../converter/WcTransactionAppInfoContentUMConverter.kt | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt index f415b0b68e..622f3c98f0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt @@ -58,7 +58,7 @@ internal class WcConnectedAppInfoModel @Inject constructor( appIcon = session.sdkModel.appMetaData.icons.firstOrNull().orEmpty(), isVerified = session.securityStatus == CheckDAppResult.SAFE, verifiedDAppState = extractVerifiedState(session), - appSubtitle = session.sdkModel.appMetaData.description, + appSubtitle = session.sdkModel.appMetaData.url, walletName = session.wallet.name, networks = session.networks .map { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index 2989e66730..99bd7fa6f0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -27,7 +27,7 @@ internal class WcAppInfoTransformer( verifiedDAppState = dAppVerifiedStateConverter.convert( dAppSession.securityStatus to dAppSession.dAppMetaData.name, ), - appSubtitle = dAppSession.dAppMetaData.description, + appSubtitle = dAppSession.dAppMetaData.url, notification = createNotification(dAppSession.securityStatus), walletName = userWallet.name, onWalletClick = onWalletClick, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt index b71e892c4e..b3c3ad5b81 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt @@ -25,7 +25,7 @@ internal class WcSessionsTransformer( WcConnectedAppInfo( name = appMetaData.name, iconUrl = appMetaData.icons.firstOrNull().orEmpty(), - subtitle = stringReference(appMetaData.description), + subtitle = stringReference(appMetaData.url), verifiedState = WcDAppVerifiedStateConverter {} .convert(session.securityStatus to appMetaData.name), onClick = { openAppInfoModal(session) }, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt index 1b8b649805..3150cbc5f6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt @@ -37,6 +37,7 @@ internal fun WcAppInfoItem( }, ) .padding(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { AsyncImage( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt index f7b97c0d9d..05a70969d3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt @@ -15,7 +15,7 @@ internal class WcTransactionAppInfoContentUMConverter @Inject constructor() : verifiedState = WcDAppVerifiedStateConverter(onVerifiedClick = value.onShowVerifiedAlert).convert( value.session.securityStatus to value.session.sdkModel.appMetaData.name, ), - appSubtitle = value.session.sdkModel.appMetaData.description, + appSubtitle = value.session.sdkModel.appMetaData.url, ) data class Input( From 4ef10e66004d9694672c683410339a8fdb2d0dbd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Jun 2025 14:09:53 +0500 Subject: [PATCH 17/99] Updated on 2026-08-14 --- .../components/WcSelectNetworksComponent.kt | 7 ++-- .../connections/model/WcPairModel.kt | 35 ++++++++++++------- .../model/WcSelectNetworksModel.kt | 21 +++++------ .../transformers/WcAppInfoTransformer.kt | 10 +++++- .../WcAppInfoWalletChangedTransformer.kt | 10 +++++- .../transformers/WcNetworksInfoConverter.kt | 19 ++++++---- .../WcNetworksSelectedTransformer.kt | 28 +++++++++++++++ .../WcSelectNetworksCheckedTransformer.kt | 5 +-- .../connections/routes/WcAppInfoRoutes.kt | 2 +- 9 files changed, 95 insertions(+), 42 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksSelectedTransformer.kt diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt index 86acac33bc..73d622b6ce 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt @@ -66,7 +66,7 @@ internal class WcSelectNetworksComponent( } interface ModelCallback { - fun onNetworksSelected(selectedNetworks: Set) + fun onNetworksSelected(selectedNetworks: Set) } data class WcSelectNetworksParams( @@ -74,7 +74,7 @@ internal class WcSelectNetworksComponent( val requiredNetworks: Set, val availableNetworks: Set, val notAddedNetworks: Set, - val enabledAvailableNetworks: Set, + val enabledAvailableNetworks: Set, val onDismiss: () -> Unit, val callback: ModelCallback, ) @@ -87,8 +87,6 @@ private fun WcSelectNetworksBS( onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - if (state.required.isEmpty()) return // required can/must not be empty - TangemModalBottomSheetWithFooter( config = TangemBottomSheetConfig( isShown = true, @@ -117,7 +115,6 @@ private fun WcSelectNetworksBS( .fillMaxWidth(), text = "Done", onClick = state.onDone, - enabled = state.missing.isEmpty(), ) }, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 08a2dbadc7..bf9adcb783 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -26,10 +26,7 @@ import com.tangem.features.walletconnect.connections.components.WcSelectNetworks import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig -import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoTransformer -import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoWalletChangedTransformer -import com.tangem.features.walletconnect.connections.model.transformers.WcConnectButtonProgressTransformer -import com.tangem.features.walletconnect.connections.model.transformers.WcDAppVerifiedStateConverter +import com.tangem.features.walletconnect.connections.model.transformers.* import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -67,7 +64,7 @@ internal class WcPairModel @Inject constructor( MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) private var proposalNetwork by Delegates.notNull() private var sessionProposal by Delegates.notNull() - private val additionallyEnabledNetworks = mutableSetOf() + private var additionallyEnabledNetworks = setOf() private val dAppVerifiedStateConverter = WcDAppVerifiedStateConverter(onVerifiedClick = ::showVerifiedAlert) val appInfoUiState: StateFlow @@ -102,7 +99,7 @@ internal class WcPairModel @Inject constructor( is WcPairState.Proposal -> { sessionProposal = pairState.dAppSession proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWalletFlow.value) - proposalNetwork.available.mapTo(additionallyEnabledNetworks) { it.id.rawId } + additionallyEnabledNetworks = proposalNetwork.available appInfoUiState.transformerUpdate( WcAppInfoTransformer( dAppSession = sessionProposal, @@ -127,6 +124,7 @@ internal class WcPairModel @Inject constructor( }, userWallet = selectedUserWalletFlow.value, proposalNetwork = proposalNetwork, + additionallyEnabledNetworks = additionallyEnabledNetworks, ), ) } @@ -154,7 +152,7 @@ internal class WcPairModel @Inject constructor( private fun connect() { val enabledAvailableNetworks = - proposalNetwork.available.filter { network -> network.id.rawId in additionallyEnabledNetworks } + proposalNetwork.available.filter { network -> network in additionallyEnabledNetworks } wcPairUseCase.approve( WcSessionApprove( wallet = selectedUserWalletFlow.value, @@ -179,14 +177,25 @@ internal class WcPairModel @Inject constructor( val selectedUserWallet = sessionProposal.proposalNetwork.keys.first { it.walletId == userWalletId } proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWallet) selectedUserWalletFlow.update { selectedUserWallet } - appInfoUiState.transformerUpdate(WcAppInfoWalletChangedTransformer(selectedUserWallet, proposalNetwork)) - additionallyEnabledNetworks.clear() - proposalNetwork.available.mapTo(additionallyEnabledNetworks) { it.id.rawId } + additionallyEnabledNetworks = proposalNetwork.available + appInfoUiState.transformerUpdate( + WcAppInfoWalletChangedTransformer( + selectedUserWallet = selectedUserWallet, + proposalNetwork = proposalNetwork, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ) } - override fun onNetworksSelected(selectedNetworks: Set) { - additionallyEnabledNetworks.clear() - additionallyEnabledNetworks.addAll(selectedNetworks) + override fun onNetworksSelected(selectedNetworks: Set) { + additionallyEnabledNetworks = selectedNetworks + appInfoUiState.transformerUpdate( + WcNetworksSelectedTransformer( + missingNetworks = proposalNetwork.missingRequired, + requiredNetworks = proposalNetwork.required, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ) } private fun createLoadingState(): WcAppInfoUM.Loading { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectNetworksModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectNetworksModel.kt index ba3466ee86..c7e1f38b60 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectNetworksModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectNetworksModel.kt @@ -29,7 +29,7 @@ internal class WcSelectNetworksModel @Inject constructor( ) : Model() { private val params = paramsContainer.require() - private val additionallyEnabledNetworks = MutableStateFlow>( + private val additionallyEnabledNetworks = MutableStateFlow>( filterAdditionallyEnabledNetworks( availableNetworks = params.availableNetworks, additionallyEnabledNetworks = params.enabledAvailableNetworks, @@ -39,11 +39,11 @@ internal class WcSelectNetworksModel @Inject constructor( val state: StateFlow field = MutableStateFlow(getInitialState()) - private fun onCheckedChange(isChecked: Boolean, networkId: String) { + private fun onCheckedChange(isChecked: Boolean, network: Network) { additionallyEnabledNetworks.update { - if (isChecked) it.plus(Network.RawID(networkId)) else it.minus(Network.RawID(networkId)) + if (isChecked) it.plus(network) else it.minus(network) } - state.update(WcSelectNetworksCheckedTransformer(networkId, isChecked)) + state.update(WcSelectNetworksCheckedTransformer(network = network, isChecked = isChecked)) } private fun onDone() { @@ -53,12 +53,9 @@ internal class WcSelectNetworksModel @Inject constructor( private fun filterAdditionallyEnabledNetworks( availableNetworks: Set, - additionallyEnabledNetworks: Set, - ): Set { - return availableNetworks - .asSequence() - .filter { it.id.rawId in additionallyEnabledNetworks } - .mapTo(hashSetOf()) { it.id.rawId } + additionallyEnabledNetworks: Set, + ): Set { + return availableNetworks.filterTo(hashSetOf()) { it in additionallyEnabledNetworks } } private fun getInitialState(): WcSelectNetworksUM { @@ -85,8 +82,8 @@ internal class WcSelectNetworksModel @Inject constructor( icon = network.iconResId, name = network.name, symbol = network.currencySymbol, - checked = network.id.rawId in additionallyEnabledNetworks.value, - onCheckedChange = { onCheckedChange(it, network.rawId) }, + checked = network in additionallyEnabledNetworks.value, + onCheckedChange = { onCheckedChange(it, network) }, ) }.toImmutableList(), notAdded = params.notAddedNetworks.map { network -> diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index 99bd7fa6f0..61d9f198d0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.walletconnect.connections.model.transformers import com.domain.blockaid.models.dapp.CheckDAppResult +import com.tangem.domain.models.network.Network import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification @@ -18,6 +19,7 @@ internal class WcAppInfoTransformer( private val onNetworksClick: () -> Unit, private val userWallet: UserWallet, private val proposalNetwork: WcSessionProposal.ProposalNetwork, + private val additionallyEnabledNetworks: Set, ) : Transformer { override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { @@ -31,7 +33,13 @@ internal class WcAppInfoTransformer( notification = createNotification(dAppSession.securityStatus), walletName = userWallet.name, onWalletClick = onWalletClick, - networksInfo = WcNetworksInfoConverter.convert(proposalNetwork), + networksInfo = WcNetworksInfoConverter.convert( + value = WcNetworksInfoConverter.Input( + missingNetworks = proposalNetwork.missingRequired, + requiredNetworks = proposalNetwork.required, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ), onNetworksClick = onNetworksClick, connectButtonConfig = WcPrimaryButtonConfig( showProgress = false, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt index 453ab9b608..21be3da5a5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.connections.model.transformers +import com.tangem.domain.models.network.Network import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM @@ -8,12 +9,19 @@ import com.tangem.utils.transformer.Transformer internal class WcAppInfoWalletChangedTransformer( private val selectedUserWallet: UserWallet, private val proposalNetwork: WcSessionProposal.ProposalNetwork, + private val additionallyEnabledNetworks: Set, ) : Transformer { override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { val contentState = prevState as? WcAppInfoUM.Content ?: return prevState return contentState.copy( walletName = selectedUserWallet.name, - networksInfo = WcNetworksInfoConverter.convert(proposalNetwork), + networksInfo = WcNetworksInfoConverter.convert( + WcNetworksInfoConverter.Input( + missingNetworks = proposalNetwork.missingRequired, + requiredNetworks = proposalNetwork.required, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ), connectButtonConfig = prevState.connectButtonConfig.copy( enabled = proposalNetwork.missingRequired.isEmpty(), ), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt index 85af70aefa..26934b08b7 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt @@ -1,22 +1,21 @@ package com.tangem.features.walletconnect.connections.model.transformers import com.tangem.core.ui.extensions.iconResId -import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.models.network.Network import com.tangem.features.walletconnect.connections.entity.WcNetworkInfoItem import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList -internal object WcNetworksInfoConverter : Converter { - override fun convert(value: WcSessionProposal.ProposalNetwork): WcNetworksInfo { - return if (value.missingRequired.isNotEmpty()) { +internal object WcNetworksInfoConverter : Converter { + override fun convert(value: Input): WcNetworksInfo { + return if (value.missingNetworks.isNotEmpty()) { WcNetworksInfo.MissingRequiredNetworkInfo( - networks = value.missingRequired - .joinToString { it.name }, + networks = value.missingNetworks.joinToString { it.name }, ) } else { WcNetworksInfo.ContainsAllRequiredNetworks( - items = (value.required + value.available) + items = (value.requiredNetworks + value.additionallyEnabledNetworks) .map { WcNetworkInfoItem.Required( id = it.rawId, @@ -29,4 +28,10 @@ internal object WcNetworksInfoConverter : Converter, + val requiredNetworks: Set, + val additionallyEnabledNetworks: Set, + ) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksSelectedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksSelectedTransformer.kt new file mode 100644 index 0000000000..ee953b6130 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksSelectedTransformer.kt @@ -0,0 +1,28 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.domain.models.network.Network +import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.utils.transformer.Transformer + +internal class WcNetworksSelectedTransformer( + private val missingNetworks: Set, + private val requiredNetworks: Set, + private val additionallyEnabledNetworks: Set, +) : Transformer { + override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { + val contentState = prevState as? WcAppInfoUM.Content ?: return prevState + return contentState.copy( + connectButtonConfig = contentState.connectButtonConfig.copy( + enabled = missingNetworks.isEmpty() && + (requiredNetworks.isNotEmpty() || additionallyEnabledNetworks.isNotEmpty()), + ), + networksInfo = WcNetworksInfoConverter.convert( + value = WcNetworksInfoConverter.Input( + missingNetworks = missingNetworks, + requiredNetworks = requiredNetworks, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksCheckedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksCheckedTransformer.kt index 582c0465dd..c8901e01b8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksCheckedTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksCheckedTransformer.kt @@ -1,17 +1,18 @@ package com.tangem.features.walletconnect.connections.model.transformers +import com.tangem.domain.models.network.Network import com.tangem.features.walletconnect.connections.entity.WcSelectNetworksUM import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toImmutableList internal class WcSelectNetworksCheckedTransformer( - private val networkId: String, + private val network: Network, private val isChecked: Boolean, ) : Transformer { override fun transform(prevState: WcSelectNetworksUM): WcSelectNetworksUM { return prevState.copy( available = prevState.available - .map { item -> if (item.id == networkId) item.copy(checked = isChecked) else item } + .map { item -> if (item.id == network.rawId) item.copy(checked = isChecked) else item } .toImmutableList(), ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index 9732c1e0aa..6c83d5ac07 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -21,7 +21,7 @@ internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent, Route { val missingRequiredNetworks: Set, val requiredNetworks: Set, val availableNetworks: Set, - val enabledAvailableNetworks: Set, + val enabledAvailableNetworks: Set, val notAddedNetworks: Set, ) : WcAppInfoRoutes() From 45bd51c738b35d0d3d732895a1b566f2533c8447 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Jun 2025 12:48:44 +0300 Subject: [PATCH 18/99] Updated on 2026-08-14 --- .../tangem/tap/data/DefaultVisaAuthTokenStorage.kt | 12 ++++-------- .../com/tangem/tap/data/DefaultVisaOTPStorage.kt | 12 ++++-------- .../di/UserWalletsListManagerModule.kt | 5 +++++ gradle/tangem_dependencies.toml | 4 ++-- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt index 9859e3e4be..28f8fdf2a6 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt @@ -3,11 +3,9 @@ package com.tangem.tap.data import android.content.Context import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import com.tangem.common.services.secure.SecureStorage import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.visa.model.VisaAuthTokens -import com.tangem.sdk.storage.AndroidSecureStorage -import com.tangem.sdk.storage.createEncryptedSharedPreferences +import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.withContext @@ -21,11 +19,9 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor( ) : VisaAuthTokenStorage { private val secureStorage by lazy { - AndroidSecureStorage( - preferences = SecureStorage.createEncryptedSharedPreferences( - context = applicationContext, - storageName = "visa_auth_storage", - ), + AndroidSecureStorageV2( + appContext = applicationContext, + name = "visa_auth_storage", ) } diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt index b87d3fbfdf..e6ad08f73d 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt @@ -3,11 +3,9 @@ package com.tangem.tap.data import android.content.Context import com.tangem.common.extensions.toByteArray import com.tangem.common.extensions.toInt -import com.tangem.common.services.secure.SecureStorage import com.tangem.datasource.local.visa.VisaOTPStorage import com.tangem.datasource.local.visa.VisaOtpData -import com.tangem.sdk.storage.AndroidSecureStorage -import com.tangem.sdk.storage.createEncryptedSharedPreferences +import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.withContext @@ -21,11 +19,9 @@ class DefaultVisaOTPStorage @Inject constructor( ) : VisaOTPStorage { private val secureStorage by lazy { - AndroidSecureStorage( - preferences = SecureStorage.createEncryptedSharedPreferences( - context = applicationContext, - storageName = "visa_otp_storage", - ), + AndroidSecureStorageV2( + appContext = applicationContext, + name = "visa_otp_storage", ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 6fdd327d88..f2e8fb0312 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -13,6 +13,7 @@ import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.sdk.storage.AndroidSecureStorage +import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.createEncryptedSharedPreferences import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager @@ -80,6 +81,10 @@ internal object UserWalletsListManagerModule { context = applicationContext, storageName = "user_wallets_storage", ), + androidSecureStorageV2 = AndroidSecureStorageV2( + appContext = applicationContext, + name = "user_wallets_storage2", + ), ) val authenticatedStorage = AuthenticatedStorage( diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 31f29bcef0..14daca0245 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.25.0-1082" +tangemBlockchainSdk = "develop-1081" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.25.0-481" +tangemCardSdk = "develop-482" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 717b8e2df228952daa4834b690b5afff042771c5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Jun 2025 12:24:35 +0400 Subject: [PATCH 19/99] Updated on 2026-08-14 --- .../test/data/quote/QuoteResponseExt.kt | 14 ++- .../quotes}/converter/QuoteStatusConverter.kt | 13 +- .../store/DefaultQuotesStatusesStore.kt | 6 +- .../converter/QuoteStatusConverterTest.kt | 118 ++++++++++++++++++ 4 files changed, 134 insertions(+), 17 deletions(-) rename {core/datasource/src/main/java/com/tangem/datasource/local/quote => data/quotes/src/main/java/com/tangem/data/quotes}/converter/QuoteStatusConverter.kt (74%) create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt diff --git a/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt index 5b53d66b34..67ff49b2b2 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt @@ -1,14 +1,22 @@ package com.tangem.common.test.data.quote import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.datasource.local.quote.converter.QuoteStatusConverter import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.utils.extensions.orZero fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): QuoteStatus { - return QuoteStatusConverter(source = source).convert(value = mapOf(rawCurrencyId to this).entries.first()) + return QuoteStatus( + rawCurrencyId = CryptoCurrency.RawID(rawCurrencyId), + value = QuoteStatus.Data( + source = source, + fiatRate = price.orZero(), + priceChange = priceChange24h.orZero().movePointLeft(2), + ), + ) } fun Pair.toDomain(source: StatusSource = StatusSource.ACTUAL): QuoteStatus { - return QuoteStatusConverter(source = source).convert(value = mapOf(this).entries.first()) + return second.toDomain(rawCurrencyId = first, source = source) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteStatusConverter.kt b/data/quotes/src/main/java/com/tangem/data/quotes/converter/QuoteStatusConverter.kt similarity index 74% rename from core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteStatusConverter.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/converter/QuoteStatusConverter.kt index b2213bd41b..6fe470b39a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteStatusConverter.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/converter/QuoteStatusConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.local.quote.converter +package com.tangem.data.quotes.converter import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.domain.models.StatusSource @@ -14,19 +14,10 @@ import com.tangem.utils.extensions.orZero * [REDACTED_AUTHOR] */ -class QuoteStatusConverter( +internal class QuoteStatusConverter( private val source: StatusSource, ) : Converter, QuoteStatus> { - /** - * Secondary constructor - * - * @param isCached flag that determines whether the quote is a cache - */ - constructor(isCached: Boolean) : this( - source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, - ) - override fun convert(value: Map.Entry): QuoteStatus { val (currencyId, quote) = value diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt index 5c7cf0f0ae..52b464efa0 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt @@ -1,9 +1,9 @@ package com.tangem.data.quotes.store import androidx.datastore.core.DataStore +import com.tangem.data.quotes.converter.QuoteStatusConverter import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.quote.converter.QuoteStatusConverter import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus @@ -41,7 +41,7 @@ internal class DefaultQuotesStatusesStore( if (cachedStatuses.isNullOrEmpty()) return@launch runtimeStore.store( - value = QuoteStatusConverter(isCached = true).convertSet(input = cachedStatuses.entries), + value = QuoteStatusConverter(source = StatusSource.CACHE).convertSet(input = cachedStatuses.entries), ) } } @@ -95,7 +95,7 @@ internal class DefaultQuotesStatusesStore( } private suspend fun storeInRuntime(values: CurrencyIdWithQuote) { - val quotes = QuoteStatusConverter(isCached = false).convertSet(input = values.entries) + val quotes = QuoteStatusConverter(source = StatusSource.ACTUAL).convertSet(input = values.entries) runtimeStore.update(default = emptySet()) { saved -> saved.addOrReplace(items = quotes) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId } diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt new file mode 100644 index 0000000000..e6cb1e498c --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt @@ -0,0 +1,118 @@ +package com.tangem.data.quotes.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.common.test.data.quote.toDomain +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuoteStatusConverterTest { + + private val ethQuoteDM = "ETH" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE) + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertTestModel) { + // Act + val actual = QuoteStatusConverter(source = model.source).convert(value = model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + ConvertTestModel( + source = StatusSource.CACHE, + value = createEntry(value = ethQuoteDM), + expected = ethQuoteDM.toDomain(source = StatusSource.CACHE), + ), + ConvertTestModel( + source = StatusSource.ONLY_CACHE, + value = createEntry(value = ethQuoteDM), + expected = ethQuoteDM.toDomain(source = StatusSource.ONLY_CACHE), + ), + ConvertTestModel( + source = StatusSource.ACTUAL, + value = createEntry(value = ethQuoteDM), + expected = ethQuoteDM.toDomain(source = StatusSource.ACTUAL), + ), + ConvertTestModel( + source = StatusSource.ACTUAL, + value = createEntry( + value = "ETH" to QuotesResponse.Quote( + price = null, + priceChange24h = null, + priceChange1w = null, + priceChange30d = null, + ), + ), + expected = QuoteStatus( + rawCurrencyId = CryptoCurrency.RawID("ETH"), + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ZERO, + priceChange = BigDecimal("0.00"), + ), + ), + ), + ConvertTestModel( + source = StatusSource.ACTUAL, + value = createEntry( + value = "ETH" to QuotesResponse.Quote( + price = null, + priceChange24h = null, + priceChange1w = BigDecimal.ZERO, + priceChange30d = BigDecimal.ZERO, + ), + ), + expected = QuoteStatus( + rawCurrencyId = CryptoCurrency.RawID("ETH"), + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ZERO, + priceChange = BigDecimal("0.00"), + ), + ), + ), + ConvertTestModel( + source = StatusSource.ACTUAL, + value = createEntry( + value = "ETH" to QuotesResponse.Quote( + price = BigDecimal.ONE, + priceChange24h = BigDecimal.ONE, + priceChange1w = null, + priceChange30d = null, + ), + ), + expected = QuoteStatus( + rawCurrencyId = CryptoCurrency.RawID("ETH"), + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal("0.01"), + ), + ), + ), + ) + + private fun createEntry(value: Pair): Map.Entry { + return mapOf(value).entries.first() + } + + data class ConvertTestModel( + val source: StatusSource, + val value: Map.Entry, + val expected: QuoteStatus, + ) +} \ No newline at end of file From b808b4561d3e89755d204e17c142b85b2e796e5a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Jun 2025 07:11:27 +0000 Subject: [PATCH 20/99] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 31f29bcef0..14daca0245 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.25.0-1082" +tangemBlockchainSdk = "develop-1081" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.25.0-481" +tangemCardSdk = "develop-482" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From c5211efa73d784e386f1a4c09b10b5b02ac4197c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Jun 2025 12:14:43 +0500 Subject: [PATCH 21/99] Updated on 2026-08-14 --- app/build.gradle.kts | 5 +++ data/express/.gitignore | 1 + data/express/build.gradle.kts | 36 +++++++++++++++++ data/swap/.gitignore | 1 + data/swap/build.gradle.kts | 54 ++++++++++++++++++++++++++ domain/express/.gitignore | 1 + domain/express/build.gradle.kts | 24 ++++++++++++ domain/express/models/.gitignore | 1 + domain/express/models/build.gradle.kts | 12 ++++++ domain/swap/.gitignore | 1 + domain/swap/build.gradle.kts | 31 +++++++++++++++ domain/swap/models/.gitignore | 1 + domain/swap/models/build.gradle.kts | 24 ++++++++++++ features/swap/domain/build.gradle.kts | 2 +- settings.gradle.kts | 2 + 15 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 data/express/.gitignore create mode 100644 data/express/build.gradle.kts create mode 100644 data/swap/.gitignore create mode 100644 data/swap/build.gradle.kts create mode 100644 domain/express/.gitignore create mode 100644 domain/express/build.gradle.kts create mode 100644 domain/express/models/.gitignore create mode 100644 domain/express/models/build.gradle.kts create mode 100644 domain/swap/.gitignore create mode 100644 domain/swap/build.gradle.kts create mode 100644 domain/swap/models/.gitignore create mode 100644 domain/swap/models/build.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8d88a7037..4501d6d6ff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -114,6 +114,8 @@ dependencies { implementation(projects.domain.notifications) implementation(projects.domain.notifications.models) implementation(projects.domain.notifications.toggles) + implementation(projects.domain.swap.models) + implementation(projects.domain.swap) implementation(projects.common) implementation(projects.common.routing) @@ -160,6 +162,7 @@ dependencies { implementation(projects.data.quotes) implementation(projects.data.blockaid) implementation(projects.data.notifications) + implementation(projects.data.swap) /** Features */ implementation(projects.features.referral.impl) @@ -170,6 +173,8 @@ dependencies { implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.data) + implementation(projects.features.swapV2.api) + implementation(projects.features.swapV2.impl) implementation(projects.features.tester.api) implementation(projects.features.tester.impl) implementation(projects.features.wallet.api) diff --git a/data/express/.gitignore b/data/express/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/express/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/express/build.gradle.kts b/data/express/build.gradle.kts new file mode 100644 index 0000000000..f9a4304009 --- /dev/null +++ b/data/express/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + alias(deps.plugins.ksp) + id("configuration") +} + +android { + namespace = "com.tangem.data.express" +} + +dependencies { + + /** Core */ + implementation(projects.core.datasource) + implementation(projects.core.utils) + + /** Data */ + implementation(projects.data.common) + + /** Domain */ + implementation(projects.domain.express.models) + implementation(projects.domain.express) + implementation(projects.domain.wallets.models) + + /** Other */ + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/swap/.gitignore b/data/swap/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/swap/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/swap/build.gradle.kts b/data/swap/build.gradle.kts new file mode 100644 index 0000000000..26eb11f40e --- /dev/null +++ b/data/swap/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + alias(deps.plugins.ksp) + id("configuration") +} + +android { + namespace = "com.tangem.data.swap" +} + +dependencies { + /** Core */ + implementation(projects.core.datasource) + implementation(projects.core.utils) + + /** Data */ + implementation(projects.data.common) + implementation(projects.data.express) + + /** Domain */ + implementation(projects.domain.express.models) + implementation(projects.domain.express) + implementation(projects.domain.swap.models) + implementation(projects.domain.swap) + implementation(projects.domain.wallets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.tokens.models) + implementation(projects.domain.tokens) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + + /** Tangem SDK */ + implementation(tangemDeps.blockchain) { + exclude(module = "joda-time") + } + + /** Libs */ + implementation(projects.libs.blockchainSdk) + + /** Other */ + implementation(deps.androidx.datastore) + implementation(deps.jodatime) + implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/domain/express/.gitignore b/domain/express/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/express/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/express/build.gradle.kts b/domain/express/build.gradle.kts new file mode 100644 index 0000000000..b5a4095dac --- /dev/null +++ b/domain/express/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.domain.express.models" +} + +dependencies { + /** Core */ + implementation(projects.core.utils) + implementation(projects.core.datasource) + + /** Domain */ + implementation(projects.domain.express.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) + + /** Other */ + implementation(deps.moshi.adapters) +} \ No newline at end of file diff --git a/domain/express/models/.gitignore b/domain/express/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/express/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/express/models/build.gradle.kts b/domain/express/models/build.gradle.kts new file mode 100644 index 0000000000..a7e2520b52 --- /dev/null +++ b/domain/express/models/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + /** Domain */ + implementation(projects.domain.tokens.models) + + /* Other */ + implementation(deps.moshi.adapters) +} \ No newline at end of file diff --git a/domain/swap/.gitignore b/domain/swap/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/swap/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/swap/build.gradle.kts b/domain/swap/build.gradle.kts new file mode 100644 index 0000000000..c0231df3f5 --- /dev/null +++ b/domain/swap/build.gradle.kts @@ -0,0 +1,31 @@ +import org.gradle.kotlin.dsl.projects + +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.domain.swap" +} + + +dependencies { + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.express.models) + implementation(projects.domain.swap.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) + + /** Util */ + implementation(projects.core.utils) + + /** Other */ + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + +} \ No newline at end of file diff --git a/domain/swap/models/.gitignore b/domain/swap/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/swap/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/swap/models/build.gradle.kts b/domain/swap/models/build.gradle.kts new file mode 100644 index 0000000000..0b8ed0f521 --- /dev/null +++ b/domain/swap/models/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.domain.swap.models" +} + +dependencies { + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.express.models) + implementation(projects.domain.tokens.models) + + /** Core */ + implementation(projects.core.datasource) + + /** Other */ + implementation(deps.jodatime) + implementation(deps.moshi) +} \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 7d52289e23..256c69cbac 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } android { - namespace = "com.tangem.domain.swap" + namespace = "com.tangem.features.domain.swap" } dependencies { diff --git a/settings.gradle.kts b/settings.gradle.kts index c46c443982..3a52dfa932 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -185,6 +185,8 @@ include(":features:swap:domain") include(":features:swap:domain:models") include(":features:swap:domain:api") include(":features:swap:impl") +include(":features:swap-v2:api") +include(":features:swap-v2:impl") include(":features:tester:api") include(":features:tester:impl") From 7f81a2912ebf9819e5672f505c783774046eb6ee Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Jun 2025 12:14:55 +0500 Subject: [PATCH 22/99] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 8 ++ features/swap-v2/api/.gitignore | 1 + features/swap-v2/api/build.gradle.kts | 25 ++++++ .../swap/v2/api/SwapFeatureToggles.kt | 5 ++ features/swap-v2/impl/.gitignore | 1 + features/swap-v2/impl/build.gradle.kts | 79 +++++++++++++++++++ .../swap/v2/impl/DefaultSwapFeatureToggles.kt | 11 +++ .../swap/v2/impl/di/SwapFeatureModules.kt | 21 +++++ settings.gradle.kts | 8 +- 9 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 features/swap-v2/api/.gitignore create mode 100644 features/swap-v2/api/build.gradle.kts create mode 100644 features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt create mode 100644 features/swap-v2/impl/.gitignore create mode 100644 features/swap-v2/impl/build.gradle.kts create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 41a2ec1111..602bd04e20 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -62,5 +62,13 @@ { "name": "USEDESK_ENABLED", "version": "undefined" + }, + { + "name": "SEND_VIA_SWAP_ENABLED", + "version": "undefined" + }, + { + "name": "SWAP_REDESIGN_ENABLED", + "version": "undefined" } ] diff --git a/features/swap-v2/api/.gitignore b/features/swap-v2/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap-v2/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap-v2/api/build.gradle.kts b/features/swap-v2/api/build.gradle.kts new file mode 100644 index 0000000000..7530654088 --- /dev/null +++ b/features/swap-v2/api/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.swap.v2.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain */ + implementation(projects.domain.wallets.models) + implementation(projects.domain.express.models) + implementation(projects.domain.swap.models) + implementation(projects.domain.manageTokens.models) + implementation(projects.domain.models) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt new file mode 100644 index 0000000000..2bc15c06e7 --- /dev/null +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.swap.v2.api + +interface SwapFeatureToggles { + val isSwapRedesignEnabled: Boolean +} \ No newline at end of file diff --git a/features/swap-v2/impl/.gitignore b/features/swap-v2/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap-v2/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts new file mode 100644 index 0000000000..cb247c509f --- /dev/null +++ b/features/swap-v2/impl/build.gradle.kts @@ -0,0 +1,79 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.swap.v2.impl" + packaging { + resources { + // To build and run composable preview + merges += "paymentrequest.proto" + } + } +} + + +dependencies { + /** Feature */ + implementation(projects.features.swapV2.api) + implementation(projects.features.manageTokens.api) + implementation(projects.features.sendV2.api) + implementation(projects.features.feeSelector.api) + + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.navigation) + implementation(projects.core.configToggles) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + implementation(tangemDeps.blockchain) { + exclude(module = "joda-time") + } + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.tokens.models) + implementation(projects.domain.tokens) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.express.models) + implementation(projects.domain.swap.models) + implementation(projects.domain.swap) + implementation(projects.domain.manageTokens.models) + implementation(projects.domain.manageTokens) + implementation(projects.domain.transaction.models) + implementation(projects.domain.transaction) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.runtime) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.androidx.activity.compose) + implementation(deps.compose.constraintLayout) + implementation(deps.compose.coil) + + /** Other */ + implementation(deps.arrow.core) + implementation(deps.decompose) + implementation(deps.decompose.ext.compose) + implementation(deps.timber) + implementation(deps.kotlin.immutable.collections) + implementation(deps.coil) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt new file mode 100644 index 0000000000..61d092f28a --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.swap.v2.impl + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.swap.v2.api.SwapFeatureToggles + +internal class DefaultSwapFeatureToggles( + private val featureToggles: FeatureTogglesManager, +) : SwapFeatureToggles { + override val isSwapRedesignEnabled: Boolean + get() = featureToggles.isFeatureEnabled("SWAP_REDESIGN_ENABLED") +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt new file mode 100644 index 0000000000..3318c6d6c3 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt @@ -0,0 +1,21 @@ +package com.tangem.features.swap.v2.impl.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.swap.v2.api.SwapFeatureToggles +import com.tangem.features.swap.v2.impl.DefaultSwapFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SwapFeatureModules { + + @Provides + @Singleton + fun provideSwapFeatureToggles(featureTogglesManager: FeatureTogglesManager): SwapFeatureToggles { + return DefaultSwapFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3a52dfa932..42a99e5d99 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -301,6 +301,10 @@ include(":domain:blockaid:models") include(":domain:notifications") include(":domain:notifications:models") include(":domain:notifications:toggles") +include(":domain:express") +include(":domain:express:models") +include(":domain:swap") +include(":domain:swap:models") // endregion Domain modules // region Data modules @@ -330,4 +334,6 @@ include(":data:onramp") include(":data:quotes") include(":data:notifications") include(":data:blockaid") -// endregion Data modules \ No newline at end of file +// endregion Data modules +include(":data:swap") +include(":data:express") \ No newline at end of file From 422e406d156f1b4a31edb096b363f381a2908daa Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Jun 2025 12:17:29 +0400 Subject: [PATCH 23/99] Updated on 2026-08-14 --- .../models/response/ValidationResponse.kt | 1 + .../tangem/data/blockaid/BlockAidMapper.kt | 1 + .../data/blockaid/BlockAidMapperTest.kt | 12 +- .../ethereum/WcEthSendTransactionUseCase.kt | 2 +- .../ethereum/WcEthSignTransactionUseCase.kt | 2 +- .../transaction/CheckTransactionResult.kt | 1 + .../usecase/method/WcTransactionUseCase.kt | 2 +- .../WcCommonTransactionComponentDelegate.kt | 1 + .../components/common/WcNavigationUtils.kt | 9 + .../send/WcCustomAllowanceComponent.kt | 39 ++++ .../send/WcSendTransactionComponent.kt | 1 + .../entity/approve/WcSpendAllowanceUM.kt | 7 +- .../entity/send/WcSendTransactionUM.kt | 2 + .../model/WcSendTransactionModel.kt | 20 +- .../transaction/routes/WcTransactionRoutes.kt | 3 + .../ui/approve/WcCustomAllowanceContent.kt | 210 +++++++++++------- .../ui/approve/WcSpendAllowanceItem.kt | 10 +- .../blockaid/TransactionCheckResultsItem.kt | 10 +- .../WcSendAndReceiveBlockAidUiConverter.kt | 28 ++- .../blockaid/WcSpendAllowanceUMConverter.kt | 4 + .../send/WcSendTransactionModalBottomSheet.kt | 5 +- 21 files changed, 258 insertions(+), 112 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcCustomAllowanceComponent.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt index 08e86a0697..2e27aab60b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt @@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass data class ValidationResponse( @Json(name = "status") val status: String, @Json(name = "result_type") val resultType: String, + @Json(name = "description") val description: String?, ) \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt index 64daa2aaf6..e245687538 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt @@ -39,6 +39,7 @@ internal object BlockAidMapper { } else { mapSimulationSuccessResult(from.simulation.accountSummary) }, + description = from.validation.description, ) } diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt index 6d83fc64db..1980d63605 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt @@ -46,7 +46,7 @@ class BlockAidMapperTest { spenders = mapOf("spender" to spenderDetails), ) val response = TransactionScanResponse( - validation = ValidationResponse(status = "Success", resultType = "Benign"), + validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""), simulation = SimulationResponse( status = "Success", accountSummary = AccountSummaryResponse( @@ -79,7 +79,7 @@ class BlockAidMapperTest { outTransfer = listOf(Transfer(value = "1.5", rawValue = "0x2")), ) val response = TransactionScanResponse( - validation = ValidationResponse(status = "Success", resultType = "Benign"), + validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""), simulation = SimulationResponse( status = "Success", accountSummary = AccountSummaryResponse( @@ -105,7 +105,7 @@ class BlockAidMapperTest { @Test fun `when response error validation rhen returns failed to validate`() { val response = TransactionScanResponse( - validation = ValidationResponse(status = "Error", resultType = "Benign"), + validation = ValidationResponse(status = "Error", resultType = "Benign", description = ""), simulation = SimulationResponse( status = "Success", accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null), @@ -120,7 +120,7 @@ class BlockAidMapperTest { @Test fun `when response not benign then returns validation unsafe`() { val response = TransactionScanResponse( - validation = ValidationResponse(status = "Success", resultType = "Phishing"), + validation = ValidationResponse(status = "Success", resultType = "Phishing", description = ""), simulation = SimulationResponse( status = "Success", accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null), @@ -134,7 +134,7 @@ class BlockAidMapperTest { @Test fun `when response simulation not success then returns simulation failed ro simulate`() { val response = TransactionScanResponse( - validation = ValidationResponse(status = "Success", resultType = "Benign"), + validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""), simulation = SimulationResponse( status = "Error", accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null), @@ -148,7 +148,7 @@ class BlockAidMapperTest { @Test fun `when response simulation is empty then returns failed to simulate`() { val txResponse = TransactionScanResponse( - validation = ValidationResponse(status = "Success", resultType = "Benign"), + validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""), simulation = SimulationResponse( status = "Success", accountSummary = AccountSummaryResponse( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index f5ef109750..9a4e8e8d7c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -119,7 +119,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( emitAll(delegate.invoke(transactionData)) } - override suspend fun getAmount(): Amount? { + override fun getAmount(): Amount? { return approvalAmount } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index e372f35b8a..2726cb8389 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -127,7 +127,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( return dAppFee } - override suspend fun getAmount(): Amount? { + override fun getAmount(): Amount? { return approvalAmount } diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt index d7244d6023..6080ac8921 100644 --- a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt @@ -8,5 +8,6 @@ package com.domain.blockaid.models.transaction */ data class CheckTransactionResult( val validation: ValidationResult, + val description: String? = null, val simulation: SimulationResult, ) \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt index 53de930b75..d4f126ead7 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt @@ -44,6 +44,6 @@ interface WcMutableFee { * [updateAmount] triggered a new [TransactionData] emit */ interface WcApproval { - suspend fun getAmount(): Amount? + fun getAmount(): Amount? fun updateAmount(amount: Amount?) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt index 0dd02cb797..1be9296325 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt @@ -60,6 +60,7 @@ internal abstract class WcCommonTransactionComponentDelegate( is WcTransactionRoutes.Transaction, is WcTransactionRoutes.TransactionRequestInfo, is WcTransactionRoutes.Alert, + is WcTransactionRoutes.CustomAllowance, -> stackNavigation?.pop() else -> Unit } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt index d06bc28646..c38c4ce6bd 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt @@ -4,7 +4,9 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.walletconnect.connections.components.AlertsComponentV2 import com.tangem.features.walletconnect.connections.utils.WcAlertsFactory.createCommonTransactionAppInfoAlertUM +import com.tangem.features.walletconnect.transaction.components.send.WcCustomAllowanceComponent import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionModel +import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal fun getWcCommonScreen( @@ -23,5 +25,12 @@ internal fun getWcCommonScreen( appComponentContext = appComponentContext, model = model, ) + is WcTransactionRoutes.CustomAllowance -> WcCustomAllowanceComponent( + appComponentContext = appComponentContext, + model = model, + onClickDoneCustomAllowance = { value, isUnlimited -> + (model as? WcSendTransactionModel)?.onClickDoneCustomAllowance(value, isUnlimited) + }, + ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcCustomAllowanceComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcCustomAllowanceComponent.kt new file mode 100644 index 0000000000..afe90c5207 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcCustomAllowanceComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.walletconnect.transaction.components.send + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionModel +import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionUM +import com.tangem.features.walletconnect.transaction.ui.approve.WcCustomAllowanceContent +import java.math.BigDecimal + +internal class WcCustomAllowanceComponent( + private val appComponentContext: AppComponentContext, + private val model: WcCommonTransactionModel, + private val onClickDoneCustomAllowance: (BigDecimal, Boolean) -> Unit, +) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + + override fun dismiss() { + model.dismiss() + } + + @Composable + override fun BottomSheet() { + val content by model.uiState.collectAsStateWithLifecycle() + val spendAllowance = (content as? WcSendTransactionUM)?.spendAllowance + if (spendAllowance != null) { + WcCustomAllowanceContent( + state = spendAllowance, + onClickDone = { value, isUnlimited -> + router.pop() + onClickDoneCustomAllowance(value, isUnlimited) + }, + onBack = router::pop, + onDismiss = ::dismiss, + ) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt index 648dc13467..2953ccb838 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt @@ -27,6 +27,7 @@ internal class WcSendTransactionComponent( onClickTransactionRequest = model::showTransactionRequest, onBack = router::pop, onDismiss = ::dismiss, + onClickAllowToSpend = model::onClickAllowToSpend, ) } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt index 1a7b10dca6..0bdad0e725 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt @@ -1,9 +1,14 @@ package com.tangem.features.walletconnect.transaction.entity.approve +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference +import java.math.BigDecimal internal data class WcSpendAllowanceUM( + val amountValue: BigDecimal, + val isUnlimited: Boolean, val amountText: TextReference, val tokenSymbol: String, val tokenImageUrl: String?, -) \ No newline at end of file + val networkIconRes: Int?, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt index e7bfe5f339..89a9e3c554 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.walletconnect.transaction.entity.send import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM @@ -10,6 +11,7 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction internal data class WcSendTransactionUM( val transaction: WcSendTransactionItemUM, override val transactionRequestInfo: WcTransactionRequestInfoUM, + val spendAllowance: WcSpendAllowanceUM? = null, ) : WcCommonTransactionUM internal data class WcSendTransactionItemUM( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 84765ddb0b..4d8311c79c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -24,6 +24,7 @@ import com.tangem.features.walletconnect.transaction.ui.blockaid.WcSendAndReceiv import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList") @@ -46,6 +47,8 @@ internal class WcSendTransactionModel @Inject constructor( val stackNavigation = StackNavigation() + private var wcApproval: WcApproval? = null + init { @Suppress("UnusedPrivateMember") modelScope.launch { @@ -77,6 +80,7 @@ internal class WcSendTransactionModel @Inject constructor( val isSecurityCheckContent = securityCheck is Lce.Content var isApprovalMethod = isSecurityCheckContent && securityCheck.content is BlockAidTransactionCheck.Result.Approval + wcApproval = useCase as? WcApproval buildUiState(securityCheck, useCase, signState) } else -> unknownMethodRunnable() @@ -90,7 +94,9 @@ internal class WcSendTransactionModel @Inject constructor( signState: WcSignState<*>, ) { val blockAidState = when (securityCheck) { - is Lce.Content -> blockAidUiConverter.convert(securityCheck.content.result) + is Lce.Content -> blockAidUiConverter.convert( + WcSendAndReceiveBlockAidUiConverter.Input(securityCheck.content.result, wcApproval?.getAmount()), + ) is Lce.Error -> WcSendReceiveTransactionCheckResultsUM(isLoading = false) is Lce.Loading -> WcSendReceiveTransactionCheckResultsUM(isLoading = true) } @@ -108,6 +114,7 @@ internal class WcSendTransactionModel @Inject constructor( ) as? WcSendTransactionUM transactionUM = transactionUM?.copy( transaction = transactionUM.transaction.copy(estimatedWalletChanges = blockAidState), + spendAllowance = blockAidState.spendAllowance, ) _uiState.emit(transactionUM) } @@ -120,6 +127,17 @@ internal class WcSendTransactionModel @Inject constructor( stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo) } + fun onClickDoneCustomAllowance(value: BigDecimal, isUnlimited: Boolean) { + val maxValue = if (isUnlimited) Double.MAX_VALUE.toBigDecimal() else value + wcApproval?.getAmount()?.let { currentAmount -> + wcApproval?.updateAmount(currentAmount.copy(maxValue = maxValue)) + } + } + + fun onClickAllowToSpend() { + wcApproval?.let { stackNavigation.pushNew(WcTransactionRoutes.CustomAllowance) } + } + private fun showVerifiedAlert(appName: String) { stackNavigation.pushNew(WcTransactionRoutes.Alert(WcTransactionRoutes.Alert.Type.Verified(appName))) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt index bddb365f90..b08fafc300 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt @@ -14,6 +14,9 @@ internal sealed class WcTransactionRoutes : TangemBottomSheetConfigContent, Rout @Serializable data object TransactionRequestInfo : WcTransactionRoutes() + @Serializable + data object CustomAllowance : WcTransactionRoutes() + @Serializable data class Alert(val type: Type) : WcTransactionRoutes() { @Serializable diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt index 08f144454d..ffb2eea5a6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt @@ -12,7 +12,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -23,91 +23,126 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.approve.WcCustomAllowanceUM +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM +import java.math.BigDecimal @Composable internal fun WcCustomAllowanceContent( - allowance: WcCustomAllowanceUM, - onAmountChange: (String) -> Unit, - onToggleChange: (Boolean) -> Unit, - onClickDone: () -> Unit, - modifier: Modifier = Modifier, + state: WcSpendAllowanceUM, + onClickDone: (BigDecimal, Boolean) -> Unit, + onBack: () -> Unit, + onDismiss: () -> Unit, ) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth() - .padding(TangemTheme.dimens.size16), - ) { - AmountTextField( - allowance = allowance, - onAmountChange = onAmountChange, - ) + var amountText by remember { mutableStateOf(state.amountValue.toPlainString()) } + var isUnlimited by remember { mutableStateOf(state.isUnlimited) } - Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) { - Text( - text = stringResourceSafe(R.string.wc_unlimited_amount), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - modifier = Modifier.weight(1f), - ) - TangemSwitch( - checked = allowance.isUnlimited, - onCheckedChange = onToggleChange, - ) - } - - Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing60)) - - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(R.string.common_done), - onClick = onClickDone, - ) + val amount: BigDecimal by remember(amountText) { + mutableStateOf(amountText.toBigDecimalOrNull() ?: state.amountValue) } + + TangemModalBottomSheetWithFooter( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onDismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + containerColor = TangemTheme.colors.background.tertiary, + onBack = onBack, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wc_custom_allowance_title), + startIconRes = R.drawable.ic_back_24, + onStartClick = onBack, + ) + }, + content = { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .fillMaxWidth() + .padding(16.dp), + ) { + AmountTextField( + amountText = amountText, + onAmountChange = { amountText = it }, + isEnabled = !isUnlimited, + tokenImageUrl = state.tokenImageUrl, + networkIconRes = state.networkIconRes, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(14.dp)) + .background(TangemTheme.colors.background.action) + .padding(16.dp) + .fillMaxWidth(), + ) { + Text( + text = stringResourceSafe(R.string.wc_unlimited_amount), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.weight(1f), + ) + TangemSwitch( + checked = isUnlimited, + onCheckedChange = { isUnlimited = it }, + ) + } + + Spacer(modifier = Modifier.height(60.dp)) + } + }, + footer = { + PrimaryButton( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_done), + onClick = { onClickDone(amount, isUnlimited) }, + ) + }, + ) } @Composable private fun AmountTextField( - allowance: WcCustomAllowanceUM, + amountText: String, onAmountChange: (String) -> Unit, + isEnabled: Boolean, + tokenImageUrl: String?, + @DrawableRes networkIconRes: Int?, modifier: Modifier = Modifier, ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .padding( - end = TangemTheme.dimens.size8, - top = TangemTheme.dimens.size4, - bottom = TangemTheme.dimens.size4, - ) + .clip(RoundedCornerShape(14.dp)) + .background(TangemTheme.colors.background.action) + .padding(end = 8.dp, top = 4.dp, bottom = 4.dp) .fillMaxWidth(), ) { TextField( - value = allowance.amountText, + value = amountText, onValueChange = onAmountChange, + enabled = isEnabled, label = { Text( text = stringResourceSafe(R.string.send_amount_label), @@ -127,6 +162,9 @@ private fun AmountTextField( unfocusedTextColor = TangemTheme.colors.text.primary1, focusedPlaceholderColor = TangemTheme.colors.text.disabled, unfocusedPlaceholderColor = TangemTheme.colors.text.disabled, + disabledContainerColor = Color.Transparent, + disabledTextColor = TangemTheme.colors.text.disabled, + disabledIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, cursorColor = TangemTheme.colors.icon.primary1, @@ -134,40 +172,44 @@ private fun AmountTextField( ) TokenWithNetworkIcon( - tokenIconUrl = allowance.tokenIconUrl, - networkIconRes = allowance.networkIconRes, + tokenIconUrl = tokenImageUrl, + networkIconRes = networkIconRes, ) } } @Composable private fun TokenWithNetworkIcon( - tokenIconUrl: String, - @DrawableRes networkIconRes: Int, + tokenIconUrl: String?, + @DrawableRes networkIconRes: Int?, modifier: Modifier = Modifier, ) { Box( - modifier = modifier - .size(TangemTheme.dimens.size48), + modifier = modifier.size(48.dp), ) { AsyncImage( + model = tokenIconUrl, + contentDescription = null, modifier = Modifier .aspectRatio(1f) .clip(CircleShape) .align(Alignment.Center) - .padding(TangemTheme.dimens.spacing8), - model = tokenIconUrl, - contentDescription = null, - ) - Image( - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size18) - .border(TangemTheme.dimens.size2, TangemTheme.colors.background.action, CircleShape) - .padding(TangemTheme.dimens.spacing2), - painter = painterResource(id = networkIconRes), - contentDescription = null, + .padding(8.dp), + placeholder = painterResource(R.drawable.ic_nft_placeholder_20), + error = painterResource(R.drawable.ic_nft_placeholder_20), ) + + if (networkIconRes != null) { + Image( + modifier = Modifier + .align(Alignment.TopEnd) + .size(18.dp) + .border(2.dp, TangemTheme.colors.background.action, CircleShape) + .padding(2.dp), + painter = painterResource(networkIconRes), + contentDescription = null, + ) + } } } @@ -175,10 +217,10 @@ private fun TokenWithNetworkIcon( @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) internal fun PreviewWcCustomAllowanceContent( - @PreviewParameter(WcCustomAllowanceStateProvider::class) state: WcCustomAllowanceUM, + @PreviewParameter(WcCustomAllowanceStateProvider::class) state: WcSpendAllowanceUM, ) { TangemThemePreview { - TangemModalBottomSheet( + TangemModalBottomSheet( config = TangemBottomSheetConfig( isShown = true, onDismissRequest = {}, @@ -196,22 +238,24 @@ internal fun PreviewWcCustomAllowanceContent( }, content = { WcCustomAllowanceContent( - allowance = state, - onAmountChange = {}, - onToggleChange = {}, - onClickDone = {}, + state = state, + onClickDone = { _, _ -> }, + onBack = {}, + onDismiss = {}, ) }, ) } } -private class WcCustomAllowanceStateProvider : CollectionPreviewParameterProvider( +private class WcCustomAllowanceStateProvider : CollectionPreviewParameterProvider( listOf( - WcCustomAllowanceUM( + WcSpendAllowanceUM( networkIconRes = R.drawable.img_eth_22, - tokenIconUrl = "https://tangem.com", - amountText = "100", + tokenImageUrl = "https://tangem.com", + amountText = TextReference.Str("100"), + amountValue = BigDecimal("100"), + tokenSymbol = "ETH", isUnlimited = false, ), ), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt index a6de96e855..92ec4f6e17 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt @@ -2,6 +2,7 @@ package com.tangem.features.walletconnect.transaction.ui.approve import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -22,13 +23,18 @@ import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllow import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem @Composable -internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: Modifier = Modifier) { +internal fun WcSpendAllowanceItem( + spendAllowance: WcSpendAllowanceUM, + onClickAllowToSpend: () -> Unit, + modifier: Modifier = Modifier, +) { Column( modifier = modifier .clip(RoundedCornerShape(14.dp)) .background(TangemTheme.colors.background.action) .fillMaxWidth() - .padding(end = 12.dp, bottom = 12.dp), + .padding(end = 12.dp, bottom = 12.dp) + .clickable { onClickAllowToSpend() }, ) { WcSmallTitleItem(R.string.wc_allow_to_spend) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt index 6cf15fa757..0ff92d4802 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt @@ -23,7 +23,11 @@ import com.tangem.features.walletconnect.transaction.ui.approve.WcSpendAllowance import kotlinx.collections.immutable.persistentListOf @Composable -internal fun TransactionCheckResultsItem(item: WcSendReceiveTransactionCheckResultsUM, modifier: Modifier = Modifier) { +internal fun TransactionCheckResultsItem( + item: WcSendReceiveTransactionCheckResultsUM, + onClickAllowToSpend: () -> Unit, + modifier: Modifier = Modifier, +) { Column( modifier = modifier .fillMaxWidth(), @@ -38,7 +42,7 @@ internal fun TransactionCheckResultsItem(item: WcSendReceiveTransactionCheckResu if (item.estimatedWalletChanges != null) { WcEstimatedWalletChangesItem(item.estimatedWalletChanges) } else if (item.spendAllowance != null) { - WcSpendAllowanceItem(item.spendAllowance) + WcSpendAllowanceItem(item.spendAllowance, onClickAllowToSpend) } else { WcEstimatedWalletChangesNotLoadedItem() } @@ -57,7 +61,7 @@ private fun TransactionCheckResultsItemPreview( modifier = Modifier .background(TangemTheme.colors.background.tertiary), ) { - TransactionCheckResultsItem(item = item) + TransactionCheckResultsItem(item = item, {}) } } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index 9212d3adf1..14d3abede0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -5,6 +5,7 @@ import com.domain.blockaid.models.transaction.SimulationResult import com.domain.blockaid.models.transaction.ValidationResult import com.domain.blockaid.models.transaction.simultation.AmountInfo import com.domain.blockaid.models.transaction.simultation.SimulationData +import com.tangem.blockchain.common.Amount import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format @@ -21,21 +22,20 @@ import javax.inject.Inject internal const val DECIMALS_AMOUNT = 2 +@Suppress("CyclomaticComplexMethod") internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( private val estimatedWalletChangeUMConverter: WcEstimatedWalletChangeUMConverter, private val spendAllowanceUMConverter: WcSpendAllowanceUMConverter, -) : - Converter { - override fun convert(value: CheckTransactionResult): WcSendReceiveTransactionCheckResultsUM { +) : Converter { + override fun convert(value: Input): WcSendReceiveTransactionCheckResultsUM { + val description = value.result.description return WcSendReceiveTransactionCheckResultsUM( isLoading = false, - notificationText = when (value.validation) { + notificationText = when (value.result.validation) { ValidationResult.SAFE, ValidationResult.FAILED_TO_VALIDATE -> null - // TODO("Add text after approve with designer") - ValidationResult.UNSAFE -> - TextReference.Str("The transaction approves tokens to a known malicious address") + ValidationResult.UNSAFE -> if (!description.isNullOrEmpty()) TextReference.Str(description) else null }, - estimatedWalletChanges = (value.simulation as? SimulationResult.Success)?.data?.let { data -> + estimatedWalletChanges = (value.result.simulation as? SimulationResult.Success)?.data?.let { data -> when (data) { is SimulationData.Approve -> null is SimulationData.SendAndReceive -> { @@ -82,15 +82,21 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( } } }, - spendAllowance = (value.simulation as? SimulationResult.Success)?.data?.let { data -> + spendAllowance = (value.result.simulation as? SimulationResult.Success)?.data?.let { data -> when (data) { is SimulationData.SendAndReceive -> null - is SimulationData.Approve -> data.approvedAmounts.map { spendAllowanceUMConverter.convert(it) } - .firstOrNull() + is SimulationData.Approve -> value.approvedAmount?.let { + data.approvedAmounts.map { spendAllowanceUMConverter.convert(it) }.firstOrNull() + } } }, ) } + + data class Input( + val result: CheckTransactionResult, + val approvedAmount: Amount?, + ) } internal fun BigDecimal.amountText() = format { crypto("", DECIMALS_AMOUNT) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt index 2d9a1a739f..96d797734c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt @@ -2,6 +2,7 @@ package com.tangem.features.walletconnect.transaction.ui.blockaid import com.domain.blockaid.models.transaction.simultation.ApprovedAmount import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.utils.converter.Converter @@ -10,12 +11,15 @@ import javax.inject.Inject internal class WcSpendAllowanceUMConverter @Inject constructor() : Converter { override fun convert(value: ApprovedAmount) = WcSpendAllowanceUM( + amountValue = value.approvedAmount, amountText = if (value.isUnlimited) { TextReference.Res(R.string.wc_common_unlimited) } else { TextReference.Str(value.approvedAmount.amountText()) }, + isUnlimited = value.isUnlimited, tokenSymbol = value.tokenInfo.symbol, tokenImageUrl = value.tokenInfo.logoUrl, + networkIconRes = value.tokenInfo.chainId?.toString()?.let { getActiveIconRes(it) }, ) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index f6a2782b93..70a4e5ec02 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -46,6 +46,7 @@ internal fun WcSendTransactionModalBottomSheet( onClickTransactionRequest: () -> Unit, onBack: () -> Unit, onDismiss: () -> Unit, + onClickAllowToSpend: () -> Unit, ) { TangemModalBottomSheetWithFooter( config = TangemBottomSheetConfig( @@ -92,7 +93,7 @@ internal fun WcSendTransactionModalBottomSheet( } Column(modifier = Modifier.padding(top = 16.dp)) { if (state.estimatedWalletChanges != null) { - TransactionCheckResultsItem(state.estimatedWalletChanges) + TransactionCheckResultsItem(state.estimatedWalletChanges, onClickAllowToSpend) } Spacer(Modifier.height(16.dp)) WcSendTransactionItems( @@ -140,7 +141,7 @@ private fun WcSendTransactionBottomSheetPreview( ) }, content = { - WcSendTransactionModalBottomSheet(state, {}, {}, {}) + WcSendTransactionModalBottomSheet(state, {}, {}, {}, {}) }, ) } From 7c91472d9960f9a92044fec139c30ecb81df6253 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Jun 2025 11:20:30 +0300 Subject: [PATCH 24/99] Updated on 2026-08-14 --- .../tap/domain/visa/VisaCardScanHandler.kt | 24 +++++++++++-------- .../GenerateNoneByCardWalletRequest.kt | 1 + .../GetAccessTokenByCardWalletRequest.kt | 1 + .../data/visa/DefaultVisaAuthRepository.kt | 3 +++ .../domain/visa/model/VisaAuthChallenge.kt | 3 ++- .../visa/model/VisaAuthSignedChallenge.kt | 1 + .../visa/repository/VisaAuthRepository.kt | 5 +++- 7 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index 46a8dadc5e..4d9a809e11 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -83,11 +83,14 @@ internal class VisaCardScanHandler @Inject constructor( Timber.i("Requesting challenge for wallet authorization") - val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value) - .getOrElse { - Timber.i("Failed to get Access token for Wallet public key authorization") - return CompletionResult.Failure(it.tangemError) - } + val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge( + cardId = card.cardId, + // This is the wallet public key, not the address and it's alright, as the API expects it in this format + cardWalletAddress = wallet.publicKey.toHexString(), + ).getOrElse { + Timber.i("Failed to get Access token for Wallet public key authorization") + return CompletionResult.Failure(it.tangemError) + } val signChallengeResult = signChallengeWithWallet( publicKey = wallet.publicKey, @@ -96,15 +99,16 @@ internal class VisaCardScanHandler @Inject constructor( return when (signChallengeResult) { is CompletionResult.Success -> { - val signature = signChallengeResult.data.cardSignature ?: run { - Timber.i("Failed to sign challenge with Wallet public key") - return CompletionResult.Failure(VisaCardScanError.FailedToSignChallenge.tangemError) - } + val signature = signChallengeResult.data.walletSignature + val salt = signChallengeResult.data.salt Timber.i("Challenge signed with Wallet public key") handleWalletAuthorizationTokens( cardWalletAddress = walletAddress.value, - signedChallenge = challengeResponse.toSignedChallenge(signature.toHexString()), + signedChallenge = challengeResponse.toSignedChallenge( + signedChallenge = signature.toHexString(), + salt = salt.toHexString(), + ), ) } is CompletionResult.Failure -> { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt index 172c39e727..69c2f7a7d5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt @@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass data class GenerateNoneByCardWalletRequest( @Json(name = "auth_type") val authType: String = "card_wallet", @Json(name = "card_wallet_address") val cardWalletAddress: String, + @Json(name = "card_id") val cardId: String, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt index c80e97b0c9..df5d0c3935 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt @@ -8,4 +8,5 @@ data class GetAccessTokenByCardWalletRequest( @Json(name = "auth_type") val authType: String = "card_wallet", @Json(name = "session_id") val sessionId: String, @Json(name = "signature") val signature: String, + @Json(name = "salt") val salt: String, ) \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index a0b6429f32..b0f2b5e284 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -47,12 +47,14 @@ internal class DefaultVisaAuthRepository @Inject constructor( } override suspend fun getCardWalletAuthChallenge( + cardId: String, cardWalletAddress: String, ): Either = withContext(dispatchers.io) { request { visaAuthApi.generateNonceByCardWallet( GenerateNoneByCardWalletRequest( cardWalletAddress = cardWalletAddress, + cardId = cardId, ), ).getOrThrow() }.map { response -> @@ -82,6 +84,7 @@ internal class DefaultVisaAuthRepository @Inject constructor( GetAccessTokenByCardWalletRequest( sessionId = signedChallenge.challenge.session.sessionId, signature = signedChallenge.signature, + salt = signedChallenge.salt, ), ).getOrThrow() } diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt index 536b7207dc..eebd03482d 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt @@ -23,9 +23,10 @@ fun VisaAuthChallenge.Card.toSignedChallenge(signedChallenge: String, salt: Stri ) } -fun VisaAuthChallenge.Wallet.toSignedChallenge(signedChallenge: String): VisaAuthSignedChallenge { +fun VisaAuthChallenge.Wallet.toSignedChallenge(signedChallenge: String, salt: String): VisaAuthSignedChallenge { return VisaAuthSignedChallenge.ByWallet( challenge = this, signature = signedChallenge, + salt = salt, ) } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt index a16916a887..607dabccbb 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt @@ -11,5 +11,6 @@ sealed class VisaAuthSignedChallenge { data class ByWallet( val challenge: VisaAuthChallenge.Wallet, val signature: String, + val salt: String, ) : VisaAuthSignedChallenge() } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt index 256587dce1..f7f19ca0e9 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -13,7 +13,10 @@ interface VisaAuthRepository { cardPublicKey: String, ): Either - suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): Either + suspend fun getCardWalletAuthChallenge( + cardId: String, + cardWalletAddress: String, + ): Either suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either From 56157412acbb8aa2221521c05047fc1def0944ca Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Jun 2025 11:20:44 +0300 Subject: [PATCH 25/99] Updated on 2026-08-14 --- .../core/ui/components/fields/PinTextField.kt | 159 ++++++++++++++++++ .../java/com/tangem/utils/StringsSigns.kt | 1 + 2 files changed, 160 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt new file mode 100644 index 0000000000..bc6d664fb1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -0,0 +1,159 @@ +package com.tangem.core.ui.components.fields + +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.utils.StringsSigns.PASSWORD_VISUAL_CHAR +import kotlin.random.Random + +@Composable +fun PinTextField( + value: String, + length: Int, + isPasswordVisual: Boolean, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val focusRequester = remember { FocusRequester() } + val textFieldValue = remember(value) { + TextFieldValue(value, selection = TextRange(value.length)) + } + + BasicTextField( + value = textFieldValue, + onValueChange = { + if (it.text.length <= length) { + onValueChange(it.text) + } + }, + modifier = modifier + .clickable { + focusRequester.requestFocus() + } + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + singleLine = true, + textStyle = TangemTheme.typography.body1, + maxLines = 1, + cursorBrush = SolidColor(TangemTheme.colors.text.primary1), + decorationBox = { innerTextField -> + Box(modifier = Modifier.width(0.dp)) { + innerTextField() + } + CellDecoration( + length = length, + isPasswordVisual = isPasswordVisual, + value = value, + ) + }, + ) +} + +@Suppress("MagicNumber") +@Composable +private fun CellDecoration( + length: Int, + value: String, + modifier: Modifier = Modifier, + isPasswordVisual: Boolean = false, +) { + val textMeasurer = rememberTextMeasurer() + val width = textMeasurer.measure("0") + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + repeat(length) { + val char = if (it < value.length) { + if (isPasswordVisual) PASSWORD_VISUAL_CHAR.toString() else value[it].toString() + } else { + "" + } + + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.tertiary, + shape = RoundedCornerShape(8.dp), + ), + contentAlignment = Alignment.Center, + ) { + AnimatedContent( + targetState = char, + transitionSpec = { + ( + fadeIn(animationSpec = tween(220, delayMillis = 90)) + + slideInVertically(animationSpec = tween(330, delayMillis = 0)) + ) + .togetherWith( + fadeOut(animationSpec = tween(90)) + slideOutVertically(tween(220)), + ) + }, + ) { text -> + Text( + modifier = Modifier.sizeIn(minWidth = width.size.width.dp + 8.dp, minHeight = 48.dp), + text = text, + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + lineHeight = 48.sp, + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + var text by remember { mutableStateOf("123") } + + Column { + PinTextField( + value = text, + onValueChange = { text = it }, + isPasswordVisual = false, + length = 6, + ) + + Row { + Button(onClick = { text += Random.nextInt(0, 10).toString() }) { + Text("Add Character") + } + Button(onClick = { text = text.dropLast(1) }) { + Text("Delete") + } + } + } + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index 7d53361c16..36945de43e 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -13,4 +13,5 @@ object StringsSigns { const val NON_BREAKING_SPACE = '\u00A0' const val PERCENT = "%" const val THREE_STARS = "\u2217\u2217\u2217" + const val PASSWORD_VISUAL_CHAR = '\u2022' } \ No newline at end of file From 55ffa595d72cce317b47ca0ec39e30dffbb3c841 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Jun 2025 14:19:09 +0500 Subject: [PATCH 26/99] Updated on 2026-08-14 --- app/build.gradle.kts | 2 - features/fee-selector/api/.gitignore | 1 - features/fee-selector/api/build.gradle.kts | 27 ------ features/fee-selector/impl/.gitignore | 1 - features/fee-selector/impl/build.gradle.kts | 57 ----------- features/send-v2/api/build.gradle.kts | 1 + .../send/v2/api}/FeeSelectorComponent.kt | 2 +- features/send-v2/impl/build.gradle.kts | 1 - .../DefaultFeeSelectorComponent.kt | 13 ++- .../di/FeeSelectorFeatureModule.kt | 6 +- .../feeselector}/di/FeeSelectorModelModule.kt | 4 +- .../v2/feeselector}/entity/FeeSelectorUM.kt | 4 +- .../v2/feeselector}/model/FeeSelectorModel.kt | 6 +- .../ui/FeeSelectorModalBottomSheet.kt | 16 ++-- .../fee/model/converters/FeeConverter.kt | 2 +- .../converters/SendFeeCustomFieldConverter.kt | 2 +- .../converters/custom/CustomFeeConverter.kt | 2 +- .../bitcoin/BitcoinCustomFeeConverter.kt | 2 +- .../BaseEthereumCustomFeeConverter.kt | 2 +- .../ethereum/EthereumCustomFeeConverter.kt | 2 +- .../ethereum/EthereumEIPCustomFeeConverter.kt | 2 +- .../EthereumLegacyCustomFeeConverter.kt | 2 +- .../custom/kaspa/KaspaCustomFeeConverter.kt | 2 +- .../v2/subcomponents/fee/ui/SendCustomFee.kt | 2 +- .../fee/ui/state}/CustomFeeFieldUM.kt | 2 +- .../fee/ui/state/FeeSelectorUM.kt | 1 - .../v2/subcomponents/fee/ui/state/FeeUM.kt | 1 - features/send/impl/build.gradle.kts | 95 ------------------- settings.gradle.kts | 3 - 29 files changed, 40 insertions(+), 223 deletions(-) delete mode 100644 features/fee-selector/api/.gitignore delete mode 100644 features/fee-selector/api/build.gradle.kts delete mode 100644 features/fee-selector/impl/.gitignore delete mode 100644 features/fee-selector/impl/build.gradle.kts rename features/{fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/component => send-v2/api/src/main/java/com/tangem/features/send/v2/api}/FeeSelectorComponent.kt (89%) rename features/{fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/component => send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector}/DefaultFeeSelectorComponent.kt (75%) rename features/{fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl => send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector}/di/FeeSelectorFeatureModule.kt (64%) rename features/{fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl => send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector}/di/FeeSelectorModelModule.kt (78%) rename features/{fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl => send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector}/entity/FeeSelectorUM.kt (90%) rename features/{fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl => send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector}/model/FeeSelectorModel.kt (82%) rename features/{fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl => send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector}/ui/FeeSelectorModalBottomSheet.kt (97%) rename features/{fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/entity => send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state}/CustomFeeFieldUM.kt (90%) delete mode 100644 features/send/impl/build.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8d88a7037..43ea7eb451 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -210,8 +210,6 @@ dependencies { implementation(projects.features.walletconnect.impl) implementation(projects.features.usedesk.api) implementation(projects.features.usedesk.impl) - implementation(projects.features.feeSelector.api) - implementation(projects.features.feeSelector.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/features/fee-selector/api/.gitignore b/features/fee-selector/api/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/features/fee-selector/api/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/features/fee-selector/api/build.gradle.kts b/features/fee-selector/api/build.gradle.kts deleted file mode 100644 index f224bf5622..0000000000 --- a/features/fee-selector/api/build.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) - id("configuration") -} - -android { - namespace = "com.tangem.features.feeselector.api" -} - -dependencies { - /** Project - Core */ - implementation(projects.core.ui) - implementation(projects.core.decompose) - - /** Domain models */ - // api(projects.domain.models) - implementation(projects.domain.appCurrency.models) - // implementation(projects.domain.wallets.models) - - /** Compose */ - implementation(deps.compose.runtime) - implementation(deps.compose.foundation) - - /** Other */ - implementation(deps.kotlin.immutable.collections) -} \ No newline at end of file diff --git a/features/fee-selector/impl/.gitignore b/features/fee-selector/impl/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/features/fee-selector/impl/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/features/fee-selector/impl/build.gradle.kts b/features/fee-selector/impl/build.gradle.kts deleted file mode 100644 index 90c8927b9b..0000000000 --- a/features/fee-selector/impl/build.gradle.kts +++ /dev/null @@ -1,57 +0,0 @@ -plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) - alias(deps.plugins.kotlin.serialization) - alias(deps.plugins.kotlin.kapt) - alias(deps.plugins.hilt.android) - id("configuration") -} - -android { - namespace = "com.tangem.features.feeselector.impl" -} - -dependencies { - /** Project - API */ - implementation(projects.features.feeSelector.api) - - /** Project - Core */ - implementation(projects.common.ui) - implementation(projects.common.routing) - implementation(projects.core.decompose) - implementation(projects.core.ui) - implementation(projects.core.pagination) - implementation(projects.core.navigation) - - /** Project - Domain */ - implementation(projects.domain.appCurrency.models) - implementation(projects.domain.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) - implementation(deps.lifecycle.compose) - - /** Compose */ - implementation(deps.compose.ui) - implementation(deps.compose.ui.tooling) - implementation(deps.compose.foundation) - implementation(deps.compose.material3) - implementation(deps.compose.shimmer) - - /*** Tangem SDK */ - implementation(tangemDeps.blockchain) - - /** DI */ - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) - - /** Other */ - implementation(deps.arrow.core) - implementation(deps.kotlin.immutable.collections) - implementation(deps.decompose.ext.compose) - implementation(deps.timber) -} \ No newline at end of file diff --git a/features/send-v2/api/build.gradle.kts b/features/send-v2/api/build.gradle.kts index 2147785720..9aa3093878 100644 --- a/features/send-v2/api/build.gradle.kts +++ b/features/send-v2/api/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /** Domain models */ api(projects.domain.models) + implementation(projects.domain.appCurrency.models) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) implementation(projects.domain.nft.models) diff --git a/features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/component/FeeSelectorComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt similarity index 89% rename from features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/component/FeeSelectorComponent.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt index 726aab533a..e3a8a20cc7 100644 --- a/features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/component/FeeSelectorComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feeselector.api.component +package com.tangem.features.send.v2.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index f3961499eb..dc97603af5 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -16,7 +16,6 @@ dependencies { implementation(projects.features.sendV2.api) implementation(projects.features.txhistory.api) implementation(projects.features.nft.api) - implementation(projects.features.feeSelector.api) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/component/DefaultFeeSelectorComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt similarity index 75% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/component/DefaultFeeSelectorComponent.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt index 0fda871e10..925d689d4f 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/component/DefaultFeeSelectorComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt @@ -1,13 +1,15 @@ -package com.tangem.features.feeselector.impl.component +package com.tangem.features.send.v2.feeselector import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.features.feeselector.api.component.FeeSelectorComponent -import com.tangem.features.feeselector.impl.model.FeeSelectorModel -import com.tangem.features.feeselector.impl.ui.FeeSelectorModalBottomSheet +import com.tangem.features.send.v2.api.FeeSelectorComponent +import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel +import com.tangem.features.send.v2.feeselector.ui.FeeSelectorModalBottomSheet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -31,8 +33,9 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor( // Temporary workaround, to use test this component @Composable override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() BackHandler(onBack = router::pop) - FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = TODO()) + FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = state) } @AssistedFactory diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorFeatureModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorFeatureModule.kt similarity index 64% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorFeatureModule.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorFeatureModule.kt index d47efb12e3..ac270168fe 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorFeatureModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorFeatureModule.kt @@ -1,7 +1,7 @@ -package com.tangem.features.feeselector.impl.di +package com.tangem.features.send.v2.feeselector.di -import com.tangem.features.feeselector.api.component.FeeSelectorComponent -import com.tangem.features.feeselector.impl.component.DefaultFeeSelectorComponent +import com.tangem.features.send.v2.api.FeeSelectorComponent +import com.tangem.features.send.v2.feeselector.DefaultFeeSelectorComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt similarity index 78% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorModelModule.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt index 2144133681..9afa349960 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt @@ -1,8 +1,8 @@ -package com.tangem.features.feeselector.impl.di +package com.tangem.features.send.v2.feeselector.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model -import com.tangem.features.feeselector.impl.model.FeeSelectorModel +import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/entity/FeeSelectorUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt similarity index 90% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/entity/FeeSelectorUM.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt index 9fba9272a5..fd759f5d7a 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/entity/FeeSelectorUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt @@ -1,10 +1,10 @@ -package com.tangem.features.feeselector.impl.entity +package com.tangem.features.send.v2.feeselector.entity import androidx.compose.runtime.Immutable import com.tangem.blockchain.common.Amount import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt similarity index 82% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/model/FeeSelectorModel.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index ea2ae3c857..fa4ea825a6 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -1,12 +1,12 @@ -package com.tangem.features.feeselector.impl.model +package com.tangem.features.send.v2.feeselector.model import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router -import com.tangem.features.feeselector.api.component.FeeSelectorComponent -import com.tangem.features.feeselector.impl.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.FeeSelectorComponent +import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt similarity index 97% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/ui/FeeSelectorModalBottomSheet.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 4b6215a295..2b79756637 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feeselector.impl.ui +package com.tangem.features.send.v2.feeselector.ui import android.content.res.Configuration import androidx.annotation.DrawableRes @@ -46,18 +46,20 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM -import com.tangem.features.feeselector.impl.R -import com.tangem.features.feeselector.impl.entity.FeeFiatRateDataHolder -import com.tangem.features.feeselector.impl.entity.FeeItem -import com.tangem.features.feeselector.impl.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateDataHolder +import com.tangem.features.send.v2.feeselector.entity.FeeItem +import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @Composable -internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM.Content, onDismiss: () -> Unit) { +internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () -> Unit) { + if (state !is FeeSelectorUM.Content) return + TangemModalBottomSheetWithFooter( config = TangemBottomSheetConfig( isShown = true, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt index b8c9fecfcf..2a5863ed1d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt @@ -4,8 +4,8 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt index 26603db4d2..78304904a5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt @@ -5,11 +5,11 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.utils.converter.TwoWayConverter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt index 19645e440b..4250d48580 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom import com.tangem.blockchain.common.transaction.Fee -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt index 548887f56d..a2f5349672 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt @@ -11,11 +11,11 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.lib.crypto.BlockchainUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt index fbdd94c589..c0828d3d60 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt @@ -1,8 +1,8 @@ package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum import com.tangem.blockchain.common.transaction.Fee -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList /** diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt index f083aaa40c..ce2ba52261 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt @@ -10,10 +10,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt index 4a40819826..2a7d389e13 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt @@ -11,7 +11,6 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance @@ -20,6 +19,7 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt index fe9af7c37b..8b2f86beb7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt @@ -11,7 +11,6 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance @@ -20,6 +19,7 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt index af0c7a97b4..2714e7d9f6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt @@ -11,10 +11,10 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt index d597bdd4b3..cf31789446 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt @@ -12,7 +12,7 @@ import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowEnterAmount import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import kotlinx.collections.immutable.ImmutableList diff --git a/features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/entity/CustomFeeFieldUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt similarity index 90% rename from features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/entity/CustomFeeFieldUM.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt index cc823ad6d6..cb3a73dae0 100644 --- a/features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/entity/CustomFeeFieldUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feeselector.api.entity +package com.tangem.features.send.v2.subcomponents.fee.ui.state import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt index ee4677b452..a9f5aae1b4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt index e310e424af..6bf499e1c4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt @@ -3,7 +3,6 @@ package com.tangem.features.send.v2.subcomponents.fee.ui.state import androidx.compose.runtime.Stable import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts deleted file mode 100644 index f104eedf8b..0000000000 --- a/features/send/impl/build.gradle.kts +++ /dev/null @@ -1,95 +0,0 @@ -plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) - alias(deps.plugins.kotlin.kapt) - alias(deps.plugins.hilt.android) - id("configuration") -} - -android { - namespace = "com.tangem.features.send.impl" -} - -dependencies { - /** AndroidX */ - implementation(deps.androidx.fragment.ktx) - implementation(deps.androidx.appCompat) - implementation(deps.androidx.paging.runtime) - - /** Other dependencies */ - implementation(deps.kotlin.immutable.collections) - implementation(deps.material) - implementation(deps.arrow.core) - implementation(deps.lifecycle.compose) - implementation(deps.jodatime) - implementation(deps.timber) - implementation(deps.reKotlin) - implementation(deps.kotlin.serialization) - - /** Compose */ - implementation(deps.compose.accompanist.systemUiController) - implementation(deps.compose.material3) - implementation(deps.compose.material) - implementation(deps.compose.foundation) - implementation(deps.compose.ui) - implementation(deps.compose.ui.tooling) - implementation(deps.compose.navigation) - implementation(deps.compose.navigation.hilt) - implementation(deps.compose.paging) - implementation(deps.compose.constraintLayout) - - /** Tangem SDKs */ - implementation(tangemDeps.card.core) - implementation(tangemDeps.blockchain) - - /** Core modules */ - implementation(projects.core.configToggles) - implementation(projects.core.ui) - implementation(projects.core.utils) - implementation(projects.core.navigation) - implementation(projects.core.analytics) - implementation(projects.core.analytics.models) - implementation(projects.core.datasource) - implementation(projects.core.decompose) - - /** Common */ - implementation(projects.common.ui) - implementation(projects.common.routing) - - /** Libs */ - implementation(projects.libs.crypto) - - /** Domain modules */ - implementation(projects.domain.models) - implementation(projects.domain.legacy) - implementation(projects.libs.blockchainSdk) - implementation(projects.domain.tokens) - implementation(projects.domain.tokens.models) - implementation(projects.domain.wallets) - implementation(projects.domain.wallets.models) - implementation(projects.domain.appCurrency) - implementation(projects.domain.appCurrency.models) - implementation(projects.domain.txhistory) - implementation(projects.domain.txhistory.models) - implementation(projects.domain.transaction) - implementation(projects.domain.transaction.models) - implementation(projects.domain.card) - implementation(projects.domain.balanceHiding) - implementation(projects.domain.balanceHiding.models) - implementation(projects.domain.feedback) - implementation(projects.domain.feedback.models) - implementation(projects.domain.qrScanning) - implementation(projects.domain.qrScanning.models) - implementation(projects.domain.settings) - implementation(projects.domain.notifications) - - /** Feature modules */ - implementation(projects.features.send.api) - implementation(projects.features.tokendetails.api) - implementation(projects.features.txhistory.api) - implementation(projects.features.qrScanning.api) - - /** DI */ - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c46c443982..10052d3ba6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -242,9 +242,6 @@ include(":features:nft:impl") include(":features:walletconnect:api") include(":features:walletconnect:impl") - -include(":features:fee-selector:api") -include(":features:fee-selector:impl") // endregion Feature modules // region Domain modules From a92d3bc692f73c6c0920c568013982308b604aea Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Jun 2025 19:53:54 +0500 Subject: [PATCH 27/99] Updated on 2026-08-14 --- features/hot-wallet/api/.gitignore | 1 + features/hot-wallet/api/build.gradle.kts | 22 ++++++ .../hotwallet/HotWalletFeatureToggles.kt | 5 ++ features/hot-wallet/impl/.gitignore | 1 + features/hot-wallet/impl/build.gradle.kts | 67 +++++++++++++++++++ .../DefaultHotWalletFeatureToggles.kt | 10 +++ .../hotwallet/di/HotWalletFeatureModule.kt | 25 +++++++ settings.gradle.kts | 3 + 8 files changed, 134 insertions(+) create mode 100644 features/hot-wallet/api/.gitignore create mode 100644 features/hot-wallet/api/build.gradle.kts create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt create mode 100644 features/hot-wallet/impl/.gitignore create mode 100644 features/hot-wallet/impl/build.gradle.kts create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt diff --git a/features/hot-wallet/api/.gitignore b/features/hot-wallet/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/hot-wallet/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/hot-wallet/api/build.gradle.kts b/features/hot-wallet/api/build.gradle.kts new file mode 100644 index 0000000000..6260c97d71 --- /dev/null +++ b/features/hot-wallet/api/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.hotwallet.api" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.models) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt new file mode 100644 index 0000000000..f8ecf58ffe --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.hotwallet + +interface HotWalletFeatureToggles { + val isHotWalletEnabled: Boolean +} \ No newline at end of file diff --git a/features/hot-wallet/impl/.gitignore b/features/hot-wallet/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/hot-wallet/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts new file mode 100644 index 0000000000..7b63af71b6 --- /dev/null +++ b/features/hot-wallet/impl/build.gradle.kts @@ -0,0 +1,67 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.hotwallet.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.hotWallet.api) + + /** Core modules */ + implementation(projects.core.configToggles) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.res) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.datasource) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** Tangem libraries */ + implementation(projects.libs.tangemSdkApi) + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) { + exclude(module = "joda-time") + } + + /** AndroidX libraries */ + implementation(deps.androidx.core.ktx) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.lottie.compose) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + implementation(deps.androidx.datastore) + + /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.timber) + implementation(deps.firebase.crashlytics) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt new file mode 100644 index 0000000000..cadb09331f --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +internal class DefaultHotWalletFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : HotWalletFeatureToggles { + override val isHotWalletEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_ENABLED") +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt new file mode 100644 index 0000000000..3b8b7412a8 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt @@ -0,0 +1,25 @@ +package com.tangem.features.hotwallet.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.hotwallet.DefaultHotWalletFeatureToggles +import com.tangem.features.hotwallet.HotWalletFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object HotWalletFeatureModule { + + @Provides + @Singleton + fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): HotWalletFeatureToggles { + return DefaultHotWalletFeatureToggles(featureTogglesManager) + } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface HotWalletFeatureModuleBinds \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 10052d3ba6..09f3a2c803 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -242,6 +242,9 @@ include(":features:nft:impl") include(":features:walletconnect:api") include(":features:walletconnect:impl") + +include(":features:hot-wallet:api") +include(":features:hot-wallet:impl") // endregion Feature modules // region Domain modules From 3dc6ff9548cf80b90fe27739be3bd46801483bad Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Jun 2025 18:21:03 +0500 Subject: [PATCH 28/99] Updated on 2026-08-14 --- .../data/express/DefaultExpressRepository.kt | 36 ++++++++ .../converter/ExpressErrorConverter.kt | 91 +++++++++++++++++++ .../converter/ExpressProviderConverter.kt | 33 +++++++ .../data/express/di/ExpressDataModule.kt | 43 +++++++++ .../error/DefaultExpressErrorResolver.kt | 20 ++++ .../domain/express/models/ExpressError.kt | 64 +++++++++++++ .../domain/express/models/ExpressProvider.kt | 43 +++++++++ .../express/models/ExpressProviderType.kt | 24 +++++ .../domain/express/models/ExpressRateType.kt | 18 ++++ .../domain/express/ExpressErrorResolver.kt | 11 +++ .../domain/express/ExpressRepository.kt | 17 ++++ features/swap-v2/impl/build.gradle.kts | 1 - 12 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt create mode 100644 data/express/src/main/java/com/tangem/data/express/converter/ExpressErrorConverter.kt create mode 100644 data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt create mode 100644 data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt create mode 100644 data/express/src/main/java/com/tangem/data/express/error/DefaultExpressErrorResolver.kt create mode 100644 domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressError.kt create mode 100644 domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt create mode 100644 domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProviderType.kt create mode 100644 domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressRateType.kt create mode 100644 domain/express/src/main/java/com/tangem/domain/express/ExpressErrorResolver.kt create mode 100644 domain/express/src/main/java/com/tangem/domain/express/ExpressRepository.kt diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt new file mode 100644 index 0000000000..4963d92a82 --- /dev/null +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt @@ -0,0 +1,36 @@ +package com.tangem.data.express + +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.express.converter.ExpressProviderConverter +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.exchangeservice.swap.ExpressUtils +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.ExpressRepository +import com.tangem.domain.wallets.models.UserWallet +import timber.log.Timber + +internal class DefaultExpressRepository( + private val tangemExpressApi: TangemExpressApi, + private val appPreferencesStore: AppPreferencesStore, +) : ExpressRepository { + + override suspend fun getProviders(userWallet: UserWallet): List { + return safeApiCall( + call = { + tangemExpressApi.getProviders( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ).getOrThrow().map(ExpressProviderConverter()::convert) + }, + onError = { + Timber.w(it, "Unable to fetch express providers") + throw it + }, + ) + } +} \ No newline at end of file diff --git a/data/express/src/main/java/com/tangem/data/express/converter/ExpressErrorConverter.kt b/data/express/src/main/java/com/tangem/data/express/converter/ExpressErrorConverter.kt new file mode 100644 index 0000000000..b191da795b --- /dev/null +++ b/data/express/src/main/java/com/tangem/data/express/converter/ExpressErrorConverter.kt @@ -0,0 +1,91 @@ +package com.tangem.data.express.converter + +import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.api.express.models.response.ExpressErrorResponse +import com.tangem.domain.express.models.ExpressError +import com.tangem.utils.converter.Converter +import com.tangem.datasource.api.express.models.response.ExpressError as ExpressErrorDTO + +class ExpressErrorConverter( + private val jsonAdapter: JsonAdapter, +) : Converter { + + @Suppress("MagicNumber", "CyclomaticComplexMethod") + override fun convert(value: String): ExpressError { + try { + val error: ExpressErrorDTO = jsonAdapter.fromJson(value)?.error ?: return ExpressError.UnknownError + + return when (error.code) { + 2010 -> ExpressError.BadRequest(code = error.code) + 2020 -> ExpressError.Forbidden(code = error.code) + + 2200 -> ExpressError.InternalError(code = error.code) + 2210 -> ExpressError.ProviderNotFoundError(code = error.code) + 2220 -> ExpressError.ProviderNotActiveError(code = error.code) + 2230 -> ExpressError.ProviderNotAvailableError(code = error.code) + 2231 -> ExpressError.ProviderInternalError(code = error.code) + 2240 -> ExpressError.ExchangeNotPossibleError(code = error.code) + 2250 -> error.toTooSmallAmountError() + 2251 -> error.toTooBigAmountError() + 2260 -> error.toNotEnoughAllowanceError() + 2270 -> ExpressError.NotEnoughBalanceError(code = error.code) + 2280 -> ExpressError.InvalidAddressError(code = error.code) + 2290 -> error.toInvalidFromDecimalsError() + + 2320 -> error.toProviderDifferentAmountError() + else -> ExpressError.DataError(error.code, error.description) + } + } catch (e: Exception) { + return ExpressError.UnknownError + } + } + + private fun ExpressErrorDTO.toTooSmallAmountError(): ExpressError { + val minAmount = value?.minAmount ?: return ExpressError.DataError(code, description) + val decimals = value?.decimals ?: return ExpressError.DataError(code, description) + val requiredAmount = minAmount.toBigDecimalOrNull()?.movePointLeft(decimals) + ?: return ExpressError.DataError(code, description) + + return ExpressError.AmountError.TooSmallError(code = code, amount = requiredAmount) + } + + private fun ExpressErrorDTO.toTooBigAmountError(): ExpressError { + val maxAmount = value?.maxAmount ?: return ExpressError.DataError(code, description) + val decimals = value?.decimals ?: return ExpressError.DataError(code, description) + val requiredAmount = maxAmount.toBigDecimalOrNull()?.movePointLeft(decimals) + ?: return ExpressError.DataError(code, description) + + return ExpressError.AmountError.TooBigError(code = code, amount = requiredAmount) + } + + private fun ExpressErrorDTO.toNotEnoughAllowanceError(): ExpressError { + val currentAllowance = value?.currentAllowance ?: return ExpressError.DataError(code, description) + + return ExpressError.AmountError.NotEnoughAllowanceError(code = code, amount = currentAllowance) + } + + private fun ExpressErrorDTO.toInvalidFromDecimalsError(): ExpressError { + val receivedFromDecimals = value?.receivedFromDecimals ?: return ExpressError.DataError(code, description) + val expressFromDecimals = value?.expressFromDecimals ?: return ExpressError.DataError(code, description) + + return ExpressError.InvalidFromDecimalsError( + code = code, + receivedFromDecimals = receivedFromDecimals, + expressFromDecimals = expressFromDecimals, + ) + } + + private fun ExpressErrorDTO.toProviderDifferentAmountError(): ExpressError { + val decimals = value?.decimals ?: return ExpressError.DataError(code, description) + val fromAmount = value?.fromAmount?.toBigDecimalOrNull() ?: return ExpressError.DataError(code, description) + val fromAmountProvider = value?.fromAmountProvider?.toBigDecimalOrNull() + ?: return ExpressError.DataError(code, description) + + return ExpressError.ProviderDifferentAmountError( + code = code, + decimals = decimals, + fromAmount = fromAmount.movePointLeft(decimals), + fromProviderAmount = fromAmountProvider.movePointLeft(decimals), + ) + } +} \ No newline at end of file diff --git a/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt b/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt new file mode 100644 index 0000000000..36c5e88460 --- /dev/null +++ b/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.express.converter + +import com.tangem.datasource.api.express.models.response.ExchangeProvider +import com.tangem.datasource.api.express.models.response.ExchangeProviderType +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.utils.converter.Converter + +internal class ExpressProviderConverter : Converter { + + override fun convert(value: ExchangeProvider): ExpressProvider { + return ExpressProvider( + providerId = value.id, + rateTypes = emptyList(), + name = value.name, + type = convertExchangeType(value.type), + imageLarge = value.imageLargeUrl, + termsOfUse = value.termsOfUse, + privacyPolicy = value.privacyPolicy, + isRecommended = value.isRecommended, + slippage = value.slippage, + ) + } + + private fun convertExchangeType(type: ExchangeProviderType): ExpressProviderType { + return when (type) { + ExchangeProviderType.DEX -> ExpressProviderType.DEX + ExchangeProviderType.CEX -> ExpressProviderType.CEX + ExchangeProviderType.DEX_BRIDGE -> ExpressProviderType.DEX_BRIDGE + ExchangeProviderType.ONRAMP -> ExpressProviderType.ONRAMP + } + } +} \ No newline at end of file diff --git a/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt new file mode 100644 index 0000000000..75bfe50bf5 --- /dev/null +++ b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt @@ -0,0 +1,43 @@ +package com.tangem.data.express.di + +import com.squareup.moshi.Moshi +import com.tangem.data.express.DefaultExpressRepository +import com.tangem.data.express.converter.ExpressErrorConverter +import com.tangem.data.express.error.DefaultExpressErrorResolver +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.response.ExpressErrorResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.express.ExpressErrorResolver +import com.tangem.domain.express.ExpressRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object ExpressDataModule { + + @Provides + @Singleton + fun provideExpressErrorResolver(@NetworkMoshi moshi: Moshi): ExpressErrorResolver { + val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java) + return DefaultExpressErrorResolver( + ExpressErrorConverter(jsonAdapter), + ) + } + + @Provides + @Singleton + fun provideExpressRepository( + tangemExpressApi: TangemExpressApi, + appPreferencesStore: AppPreferencesStore, + ): ExpressRepository { + return DefaultExpressRepository( + tangemExpressApi = tangemExpressApi, + appPreferencesStore = appPreferencesStore, + ) + } +} \ No newline at end of file diff --git a/data/express/src/main/java/com/tangem/data/express/error/DefaultExpressErrorResolver.kt b/data/express/src/main/java/com/tangem/data/express/error/DefaultExpressErrorResolver.kt new file mode 100644 index 0000000000..96ca3d07a8 --- /dev/null +++ b/data/express/src/main/java/com/tangem/data/express/error/DefaultExpressErrorResolver.kt @@ -0,0 +1,20 @@ +package com.tangem.data.express.error + +import com.tangem.data.express.converter.ExpressErrorConverter +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.domain.express.ExpressErrorResolver +import com.tangem.domain.express.models.ExpressError + +internal class DefaultExpressErrorResolver( + private val expressErrorConverter: ExpressErrorConverter, +) : ExpressErrorResolver { + + override fun resolve(throwable: Throwable): ExpressError { + return when (throwable) { + is ApiResponseError.HttpException -> { + expressErrorConverter.convert(throwable.errorBody.orEmpty()) + } + else -> ExpressError.UnknownError + } + } +} \ No newline at end of file diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressError.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressError.kt new file mode 100644 index 0000000000..7d153e9bf1 --- /dev/null +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressError.kt @@ -0,0 +1,64 @@ +package com.tangem.domain.express.models + +import java.math.BigDecimal + +sealed class ExpressError { + + abstract val code: Int + + data class DataError( + override val code: Int, + val description: String?, + ) : ExpressError() + + data class BadRequest(override val code: Int) : ExpressError() + + data class Forbidden(override val code: Int) : ExpressError() + + data class InternalError(override val code: Int) : ExpressError() + + data class ProviderNotFoundError(override val code: Int) : ExpressError() + + data class ProviderNotActiveError(override val code: Int) : ExpressError() + + data class ProviderNotAvailableError(override val code: Int) : ExpressError() + + data class ProviderInternalError(override val code: Int) : ExpressError() + + data class ExchangeNotPossibleError(override val code: Int) : ExpressError() + + data class NotEnoughBalanceError(override val code: Int) : ExpressError() + + data class InvalidAddressError(override val code: Int) : ExpressError() + + sealed class AmountError : ExpressError() { + abstract val amount: BigDecimal + + data class TooSmallError(override val code: Int, override val amount: BigDecimal) : AmountError() + data class TooBigError(override val code: Int, override val amount: BigDecimal) : AmountError() + data class NotEnoughAllowanceError(override val code: Int, override val amount: BigDecimal) : AmountError() + } + + data class ProviderDifferentAmountError( + override val code: Int, + val fromAmount: BigDecimal, + val fromProviderAmount: BigDecimal, + val decimals: Int, + ) : ExpressError() + + data class InvalidFromDecimalsError( + override val code: Int, + val receivedFromDecimals: Int, + val expressFromDecimals: Int, + ) : ExpressError() + + data class InvalidSignatureError(override val code: Int = 990) : ExpressError() + + data class InvalidRequestIdError(override val code: Int = 991) : ExpressError() + + data class InvalidPayoutAddressError(override val code: Int = 992) : ExpressError() + + data object UnknownError : ExpressError() { + override val code: Int = -1 + } +} \ No newline at end of file diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt new file mode 100644 index 0000000000..9a26b89580 --- /dev/null +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt @@ -0,0 +1,43 @@ +package com.tangem.domain.express.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.math.BigDecimal + +/** + * Express provider data model + * + * @property providerId provider id + * @property rateTypes supported rate types + * @property name provider name + * @property type provider type + * @property imageLarge provider large logo image + * @property termsOfUse terms of use link + * @property privacyPolicy privacy policy link + * @property isRecommended flag that indicates if this provider is recommended + * @property slippage provider slippage + * + * Uses to store transaction data in datastore, when extends - should always add default value + * to support backward compatibility + */ +@JsonClass(generateAdapter = true) +data class ExpressProvider( + @Json(name = "providerId") + val providerId: String, + @Json(name = "rateTypes") + val rateTypes: List = emptyList(), + @Json(name = "name") + val name: String, + @Json(name = "type") + val type: ExpressProviderType, + @Json(name = "imageLarge") + val imageLarge: String, + @Json(name = "termsOfUse") + val termsOfUse: String?, + @Json(name = "privacyPolicy") + val privacyPolicy: String?, + @Json(name = "isRecommended") + val isRecommended: Boolean = false, + @Json(name = "slippage") + val slippage: BigDecimal?, +) \ No newline at end of file diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProviderType.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProviderType.kt new file mode 100644 index 0000000000..3c77da80fb --- /dev/null +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProviderType.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.express.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Express provider type + * + * @property typeName provider type name + */ +@JsonClass(generateAdapter = false) +enum class ExpressProviderType(val typeName: String) { + @Json(name = "DEX") + DEX(typeName = "DEX"), + + @Json(name = "CEX") + CEX(typeName = "CEX"), + + @Json(name = "DEX_BRIDGE") + DEX_BRIDGE(typeName = "DEX/Bridge"), + + @Json(name = "onramp") + ONRAMP(typeName = "ONRAMP"), +} \ No newline at end of file diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressRateType.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressRateType.kt new file mode 100644 index 0000000000..4b9efd00b0 --- /dev/null +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressRateType.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.express.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Rate type. + * + * Current implementation contains only float type, fixed will be supported later. + */ +@JsonClass(generateAdapter = false) +enum class ExpressRateType { + @Json(name = "FLOAT") + Float, + + @Json(name = "FIXED") + Fixed, +} \ No newline at end of file diff --git a/domain/express/src/main/java/com/tangem/domain/express/ExpressErrorResolver.kt b/domain/express/src/main/java/com/tangem/domain/express/ExpressErrorResolver.kt new file mode 100644 index 0000000000..05e981033d --- /dev/null +++ b/domain/express/src/main/java/com/tangem/domain/express/ExpressErrorResolver.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.express + +import com.tangem.domain.express.models.ExpressError + +/** + * Common express error resolver + */ +interface ExpressErrorResolver { + + fun resolve(throwable: Throwable): ExpressError +} \ No newline at end of file diff --git a/domain/express/src/main/java/com/tangem/domain/express/ExpressRepository.kt b/domain/express/src/main/java/com/tangem/domain/express/ExpressRepository.kt new file mode 100644 index 0000000000..d7c6518b46 --- /dev/null +++ b/domain/express/src/main/java/com/tangem/domain/express/ExpressRepository.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.express + +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.wallets.models.UserWallet + +/** + * Express repository + */ +interface ExpressRepository { + + /** + * Returns list of express providers + * + * @param userWallet selected user wallet info + */ + suspend fun getProviders(userWallet: UserWallet): List +} \ No newline at end of file diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index cb247c509f..456c959e50 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -23,7 +23,6 @@ dependencies { implementation(projects.features.swapV2.api) implementation(projects.features.manageTokens.api) implementation(projects.features.sendV2.api) - implementation(projects.features.feeSelector.api) /** Core */ implementation(projects.core.decompose) From a692abaffb2f5b7967ce8d6da2ae3ea928192d43 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Jun 2025 18:38:24 +0500 Subject: [PATCH 29/99] Updated on 2026-08-14 --- .../tangem/tap/di/domain/SwapDomainModule.kt | 19 +- .../data/swap/DefaultSwapErrorResolver.kt | 19 ++ .../data/swap/DefaultSwapRepositoryV2.kt | 200 ++++++++++++++++++ .../data/swap/converter/TokenInfoConverter.kt | 23 ++ .../com/tangem/data/swap/di/SwapDataModule.kt | 52 +++++ .../domain/swap/models/SwapCurrencies.kt | 20 ++ .../domain/swap/models/SwapPairModel.kt | 17 ++ .../tangem/domain/swap/models/TokenInfo.kt | 6 + .../tangem/domain/swap/SwapErrorResolver.kt | 7 + .../tangem/domain/swap/SwapRepositoryV2.kt | 29 +++ .../usecase/GetSwapSupportedPairsUseCase.kt | 85 ++++++++ 11 files changed, 475 insertions(+), 2 deletions(-) create mode 100644 data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt create mode 100644 domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt create mode 100644 domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt create mode 100644 domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt index e94429b646..a392d9c30e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt @@ -1,12 +1,15 @@ package com.tangem.tap.di.domain +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase -import com.tangem.feature.swap.domain.api.SwapRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository /** [REDACTED_AUTHOR] @@ -17,7 +20,19 @@ internal object SwapDomainModule { @Provides @Singleton - fun provideGetAvailablePairsUseCase(swapRepository: SwapRepository): GetAvailablePairsUseCase { + fun provideGetAvailablePairsUseCase(swapRepository: OldSwapRepository): GetAvailablePairsUseCase { return GetAvailablePairsUseCase(swapRepository = swapRepository) } + + @Provides + @Singleton + fun provideGetSwapSupportedPairsUseCase( + swapRepositoryV2: SwapRepositoryV2, + swapErrorResolver: SwapErrorResolver, + ): GetSwapSupportedPairsUseCase { + return GetSwapSupportedPairsUseCase( + swapRepositoryV2 = swapRepositoryV2, + swapErrorResolver = swapErrorResolver, + ) + } } \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt new file mode 100644 index 0000000000..8827a8bc7c --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt @@ -0,0 +1,19 @@ +package com.tangem.data.swap + +import com.tangem.data.express.converter.ExpressErrorConverter +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.swap.SwapErrorResolver + +internal class DefaultSwapErrorResolver( + private val expressErrorConverter: ExpressErrorConverter, +) : SwapErrorResolver { + override fun resolve(throwable: Throwable): ExpressError { + return when (throwable) { + is ApiResponseError.HttpException -> { + expressErrorConverter.convert(throwable.errorBody.orEmpty()) + } + else -> ExpressError.UnknownError + } + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt new file mode 100644 index 0000000000..1931f346f2 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -0,0 +1,200 @@ +package com.tangem.data.swap + +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.swap.converter.TokenInfoConverter +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.request.PairsRequestBody +import com.tangem.datasource.exchangeservice.swap.ExpressUtils +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.express.ExpressRepository +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class DefaultSwapRepositoryV2 @Inject constructor( + private val tangemExpressApi: TangemExpressApi, + private val expressRepository: ExpressRepository, + private val coroutineDispatcher: CoroutineDispatcherProvider, + private val appPreferencesStore: AppPreferencesStore, + private val currencyStatusOperations: BaseCurrencyStatusOperations, +) : SwapRepositoryV2 { + + private val tokenInfoConverter = TokenInfoConverter() + override suspend fun getPairs( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyStatusList: List, + ): List = withContext(coroutineDispatcher.io) { + val cryptoCurrencyList = cryptoCurrencyStatusList.map { it.currency } + + val allPairs = getPairsInternal( + userWallet = userWallet, + initialCurrency = initialCurrency, + cryptoCurrencyList = cryptoCurrencyList, + ) + + val providers = expressRepository.getProviders(userWallet = userWallet) + val mappedProviders = providers.associateBy(ExpressProvider::providerId) + + allPairs.map { pair -> + async { + val statusFrom = cryptoCurrencyStatusList + .firstOrNull { + it.currency.getContractAddress() == pair.from.contractAddress && + it.currency.network.backendId == pair.from.network + } + val statusTo = cryptoCurrencyStatusList + .firstOrNull { + it.currency.getContractAddress() == pair.to.contractAddress && + it.currency.network.backendId == pair.to.network + } + + if (statusFrom != null && statusTo != null) { + SwapPairModel( + from = statusFrom, + to = statusTo, + providers = pair.providers.mapNotNull { + mappedProviders[it.providerId] + }, + ) + } else { + null + } + } + }.awaitAll().filterNotNull() + } + + override suspend fun getPairsOnly( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ): List = withContext(coroutineDispatcher.io) { + val allPairs = getPairsInternal( + userWallet = userWallet, + initialCurrency = initialCurrency, + cryptoCurrencyList = cryptoCurrencyList, + ) + + allPairs.map { pair -> + async { + val statusFromDeferred = async { + cryptoCurrencyList + .firstOrNull { + it.getContractAddress() == pair.from.contractAddress && + it.network.backendId == pair.from.network + } + } + val statusToDeferred = async { + cryptoCurrencyList + .firstOrNull { + it.getContractAddress() == pair.to.contractAddress && + it.network.backendId == pair.to.network + } + } + + createPairModelOnly( + currencyFrom = statusFromDeferred.await(), + currencyTo = statusToDeferred.await(), + userWalletId = userWallet.walletId, + ) + } + }.awaitAll().filterNotNull() + } + + private suspend fun CoroutineScope.getPairsInternal( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ) = awaitAll( + // original pairs + async { + invokePairRequest( + userWallet = userWallet, + from = arrayListOf(initialCurrency), + to = cryptoCurrencyList, + ) + }, + // reversed pairs + async { + invokePairRequest( + userWallet = userWallet, + from = cryptoCurrencyList, + to = arrayListOf(initialCurrency), + ) + }, + ).flatten() + + private suspend fun invokePairRequest( + userWallet: UserWallet, + from: List, + to: List, + ) = safeApiCall( + call = { + tangemExpressApi.getPairs( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + body = PairsRequestBody( + from = tokenInfoConverter.convertList(from), + to = tokenInfoConverter.convertList(to), + ), + ).getOrThrow() + }, + onError = { + Timber.w(it, "Unable to get pairs") + throw it + }, + ) + + private suspend fun CoroutineScope.createPairModelOnly( + currencyFrom: CryptoCurrency?, + currencyTo: CryptoCurrency?, + userWalletId: UserWalletId, + ): SwapPairModel? { + return if (currencyFrom != null && currencyTo != null) { + val statusFrom = currencyStatusOperations.getCurrencyStatusSync( + userWalletId = userWalletId, + cryptoCurrencyId = currencyFrom.id, + ).getOrNull() + val statusTo = currencyStatusOperations.getCurrencyStatusSync( + userWalletId = userWalletId, + cryptoCurrencyId = currencyTo.id, + ).getOrNull() + + if (statusFrom != null && statusTo != null) { + SwapPairModel( + from = statusFrom, + to = statusTo, + providers = emptyList(), + ) + } else { + null + } + } else { + null + } + } + + private fun CryptoCurrency.getContractAddress(): String { + return when (this) { + is CryptoCurrency.Token -> this.contractAddress + is CryptoCurrency.Coin -> "0" + } + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt new file mode 100644 index 0000000000..04e3203f28 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.data.swap.converter + +import com.tangem.datasource.api.express.models.request.LeastTokenInfo +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.TokenInfo +import com.tangem.utils.converter.Converter + +class TokenInfoConverter : Converter { + + override fun convert(value: CryptoCurrency): LeastTokenInfo { + return LeastTokenInfo( + contractAddress = (value as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = value.network.backendId, + ) + } + + fun convert(value: TokenInfo): LeastTokenInfo { + return LeastTokenInfo( + contractAddress = value.contractAddress, + network = value.network, + ) + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt new file mode 100644 index 0000000000..0ee1971b25 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt @@ -0,0 +1,52 @@ +package com.tangem.data.swap.di + +import com.squareup.moshi.Moshi +import com.tangem.data.express.converter.ExpressErrorConverter +import com.tangem.data.swap.DefaultSwapErrorResolver +import com.tangem.data.swap.DefaultSwapRepositoryV2 +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.response.ExpressErrorResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.express.ExpressRepository +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal object SwapDataModule { + + @Provides + @Singleton + fun provideSwapErrorResolver(@NetworkMoshi moshi: Moshi): SwapErrorResolver { + val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java) + return DefaultSwapErrorResolver( + ExpressErrorConverter(jsonAdapter), + ) + } + + @Provides + @Singleton + fun provideSwapRepository( + tangemExpressApi: TangemExpressApi, + expressRepository: ExpressRepository, + coroutineDispatcher: CoroutineDispatcherProvider, + appPreferencesStore: AppPreferencesStore, + currencyStatusOperations: BaseCurrencyStatusOperations, + ): SwapRepositoryV2 { + return DefaultSwapRepositoryV2( + tangemExpressApi = tangemExpressApi, + expressRepository = expressRepository, + coroutineDispatcher = coroutineDispatcher, + appPreferencesStore = appPreferencesStore, + currencyStatusOperations = currencyStatusOperations, + ) + } +} \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt new file mode 100644 index 0000000000..6a1f16be09 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.swap.models + +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus + +data class SwapCurrencies( + val fromGroup: SwapCurrenciesGroup, + val toGroup: SwapCurrenciesGroup, +) + +data class SwapCurrenciesGroup( + val available: List, + val unavailable: List, + val afterSearch: Boolean, +) + +data class SwapCryptoCurrency( + val currencyStatus: CryptoCurrencyStatus, + val providers: List, +) \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt new file mode 100644 index 0000000000..5e8e0479f9 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.swap.models + +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus + +/** + * Domain layer representation of SwapPair data network model. + * + * @property from Short information about the token we want to change + * @property to Short information about the token we want to exchange for + * @property providers Exchange providers + */ +data class SwapPairModel( + val from: CryptoCurrencyStatus, + val to: CryptoCurrencyStatus, + val providers: List, +) \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt new file mode 100644 index 0000000000..9bd09cc984 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.swap.models + +data class TokenInfo( + val contractAddress: String, + val network: String, +) \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt new file mode 100644 index 0000000000..b9f75cb5c1 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.swap + +import com.tangem.domain.express.models.ExpressError + +interface SwapErrorResolver { + fun resolve(throwable: Throwable): ExpressError +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt new file mode 100644 index 0000000000..b2c768cdc7 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.swap + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet + +interface SwapRepositoryV2 { + + /** + * Express swap pairs, both direct and reversed + * + * @param userWallet selected user wallet + * @param initialCurrency currency being swapped (either to or from) + * @param cryptoCurrencyStatusList list of currencies might be swapped + */ + suspend fun getPairs( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyStatusList: List, + ): List + + /** Express getPairs request variant without providers request */ + suspend fun getPairsOnly( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ): List +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt new file mode 100644 index 0000000000..1954bc4f85 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt @@ -0,0 +1,85 @@ +package com.tangem.domain.swap.usecase + +import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.models.SwapCryptoCurrency +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapCurrenciesGroup +import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet + +/** + * Returns pais + */ +class GetSwapSupportedPairsUseCase( + private val swapRepositoryV2: SwapRepositoryV2, + private val swapErrorResolver: SwapErrorResolver, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ) = Either.catch { + val pairs = swapRepositoryV2.getPairsOnly( + userWallet = userWallet, + initialCurrency = initialCurrency, + cryptoCurrencyList = cryptoCurrencyList, + ) + + val filteredOutInitial = cryptoCurrencyList.filterNot { it.id == initialCurrency.id } + + val fromGroup = pairs.groupPairs( + initialCurrency = initialCurrency, + filteringCurrency = { it.from }, + groupingCurrency = { it.to }, + cryptoCurrencyList = filteredOutInitial, + ) + val toGroup = pairs.groupPairs( + initialCurrency = initialCurrency, + filteringCurrency = { it.to }, + groupingCurrency = { it.from }, + cryptoCurrencyList = filteredOutInitial, + ) + + SwapCurrencies( + fromGroup = fromGroup, + toGroup = toGroup, + ) + }.mapLeft(swapErrorResolver::resolve) + + private fun List.groupPairs( + initialCurrency: CryptoCurrency, + filteringCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + groupingCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + cryptoCurrencyList: List, + ): SwapCurrenciesGroup { + val availableCryptoCurrencies = filter { pair -> filteringCurrency(pair).currency.id == initialCurrency.id } + // Search available to swap currency + .filter { pair -> + cryptoCurrencyList.any { currencyStatus -> + currencyStatus.id == pair.to.currency.id + } + }.map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) } + + val unavailableCryptoCurrencies = + cryptoCurrencyList - availableCryptoCurrencies.map { it.currencyStatus.currency }.toSet() + + return SwapCurrenciesGroup( + available = availableCryptoCurrencies, + unavailable = unavailableCryptoCurrencies.map { currency -> + SwapCryptoCurrency( + CryptoCurrencyStatus( + currency = currency, + value = CryptoCurrencyStatus.Loading, // todo select token unavailable + ), + emptyList(), + ) + }, + afterSearch = false, + ) + } +} \ No newline at end of file From 27819281a24cce369a5501a00d6223dc87631c36 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Jun 2025 16:22:44 +0500 Subject: [PATCH 30/99] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tap/features/home/DefaultHomeComponent.kt | 23 +- .../com/tangem/tap/features/home/HomeModel.kt | 8 + .../features/home/compose/StoriesScreenV2.kt | 252 ++++++++++++++++++ .../home/compose/views/HomeButtonsV2.kt | 124 +++++++++ .../tap/features/home/redux/HomeState.kt | 1 + core/res/src/main/res/values/strings.xml | 2 + .../java/com/tangem/core/ui/test/TestTags.kt | 2 + 8 files changed, 408 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt create mode 100644 app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d53a38d574..d592ad2b98 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -215,6 +215,8 @@ dependencies { implementation(projects.features.walletconnect.impl) implementation(projects.features.usedesk.api) implementation(projects.features.usedesk.impl) + implementation(projects.features.hotWallet.api) + implementation(projects.features.hotWallet.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt b/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt index 85e57a4ae2..ade00b0024 100644 --- a/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt @@ -16,6 +16,7 @@ import com.tangem.core.ui.utils.findActivity import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.api.HomeComponent import com.tangem.tap.features.home.compose.StoriesScreen +import com.tangem.tap.features.home.compose.StoriesScreenV2 import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.store @@ -57,12 +58,22 @@ internal class DefaultHomeComponent @AssistedInject constructor( val activity = LocalContext.current.findActivity() BackHandler(onBack = activity::finish) SystemBarsIconsDisposable(darkIcons = false) - StoriesScreen( - homeState = homeState, - onScanButtonClick = model::onScanClick, - onShopButtonClick = model::onShopClick, - onSearchTokensClick = model::onSearchClick, - ) + if (homeState.value.isV2StoriesEnabled) { + StoriesScreenV2( + homeState = homeState, + onCreateNewWalletButtonClick = model::onCreateNewWalletScreen, + onAddExistingWalletButtonClick = model::onAddExistingWalletScreen, + onScanButtonClick = model::onScanClick, + ) + } else { + StoriesScreen( + homeState = homeState, + onScanButtonClick = model::onScanClick, + onShopButtonClick = model::onShopClick, + onSearchTokensClick = model::onSearchClick, + ) + } + ChangeRootBackgroundColorEffect(Color(color = 0xFF010101)) } diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt index 132cbd1773..8936e89b41 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt @@ -71,6 +71,14 @@ internal class HomeModel @Inject constructor( .launchIn(modelScope) } + fun onCreateNewWalletScreen() { + // TODO implement navigation to create new wallet + } + + fun onAddExistingWalletScreen() { + // TODO implement navigation to add existing wallet + } + fun onScanClick() { analyticsEventHandler.send(IntroductionProcess.ButtonScanCard()) scanCard() diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt new file mode 100644 index 0000000000..a17f707453 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt @@ -0,0 +1,252 @@ +@file:Suppress("MagicNumber") + +package com.tangem.tap.features.home.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +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.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags +import com.tangem.tap.features.home.compose.content.* +import com.tangem.tap.features.home.compose.views.HomeButtonsV2 +import com.tangem.tap.features.home.compose.views.StoriesProgressBar +import com.tangem.tap.features.home.redux.HomeState +import com.tangem.tap.features.home.redux.Stories +import com.tangem.wallet.R +import kotlin.math.max + +@Composable +internal fun StoriesScreenV2( + homeState: MutableState, + onCreateNewWalletButtonClick: () -> Unit, + onAddExistingWalletButtonClick: () -> Unit, + onScanButtonClick: () -> Unit, +) { + val state = homeState.value + + var currentStory by remember { mutableStateOf(state.firstStory) } + val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory)) + + val goToPreviousStory = remember(currentStory, currentStoryIndex) { + { currentStory = state.stories[max(0, currentStoryIndex - 1)] } + } + val goToNextStory = remember(currentStory, currentStoryIndex) { + { + currentStory = if (currentStoryIndex < state.stories.lastIndex) { + state.stories[currentStoryIndex + 1] + } else { + state.firstStory + } + } + } + + // todo refactor [REDACTED_TASK_KEY] + StoriesScreenContentV2( + modifier = Modifier + .fillMaxSize() + .testTag(TestTags.STORIES_SCREEN), + config = StoriesScreenContentV2Config( + storiesSize = state.stories.lastIndex, + currentStoryIndex = currentStoryIndex, + currentStory = currentStory, + isScanInProgress = homeState.value.scanInProgress, + onGoToPreviousStory = goToPreviousStory, + onGoToNextStory = goToNextStory, + onCreateNewWalletButtonClick = onCreateNewWalletButtonClick, + onAddExistingWalletButtonClick = onAddExistingWalletButtonClick, + onScanButtonClick = onScanButtonClick, + ), + ) +} + +@Deprecated("Use StoriesContainer from core/ui") +@Suppress("LongMethod") +@Composable +private fun StoriesScreenContentV2(config: StoriesScreenContentV2Config, modifier: Modifier = Modifier) { + var isPressed by remember { mutableStateOf(value = false) } + + val isPaused = isPressed || config.isScanInProgress + val currentStoryDuration = config.currentStory.duration + + Box( + modifier = modifier.background(Color(0xFF010101)), + ) { + Row( + modifier = Modifier.fillMaxSize(), + ) { + Box( + Modifier + .weight(1f) + .fillMaxHeight() + .pointerInput(Unit) { + detectTapGestures( + onPress = { + val pressStartTime = System.currentTimeMillis() + isPressed = true + this.tryAwaitRelease() + val pressEndTime = System.currentTimeMillis() + val totalPressTime = pressEndTime - pressStartTime + if (totalPressTime < 200) config.onGoToPreviousStory() + isPressed = false + }, + ) + }, + ) + Box( + Modifier + .weight(1f) + .fillMaxHeight() + .pointerInput(Unit) { + detectTapGestures( + onPress = { + val pressStartTime = System.currentTimeMillis() + isPressed = true + this.tryAwaitRelease() + val pressEndTime = System.currentTimeMillis() + val totalPressTime = pressEndTime - pressStartTime + if (totalPressTime < 200) config.onGoToNextStory() + isPressed = false + }, + ) + }, + ) + } + + Column( + modifier = Modifier + .statusBarsPadding() + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + StoriesProgressBar( + steps = config.storiesSize, + currentStep = config.currentStoryIndex, + stepDuration = currentStoryDuration, + paused = isPaused, + onStepFinish = config.onGoToNextStory, + ) + Image( + painter = painterResource(id = R.drawable.ic_tangem_logo), + contentDescription = null, + contentScale = ContentScale.FillHeight, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing16, + ) + .height(TangemTheme.dimens.size18) + .align(Alignment.Start), + ) + when (config.currentStory) { + Stories.TangemIntro -> FirstStoriesContent( + isPaused = isPaused, + duration = currentStoryDuration, + ) + Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet() + Stories.UltraSecureBackup -> StoriesUltraSecureBackup( + isPaused = isPaused, + stepDuration = currentStoryDuration, + ) + Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration) + Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration) + Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStoryDuration) + } + } + Column( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = TangemTheme.dimens.spacing16) + .padding(horizontal = TangemTheme.dimens.spacing16) + .align(Alignment.BottomCenter) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + HomeButtonsV2( + modifier = Modifier.fillMaxWidth(), + btnScanStateInProgress = config.isScanInProgress, + onScanButtonClick = config.onScanButtonClick, + onCreateNewWalletButtonClick = config.onCreateNewWalletButtonClick, + onAddExistingWalletButtonClick = config.onAddExistingWalletButtonClick, + ) + } + } +} + +private data class StoriesScreenContentV2Config( + val storiesSize: Int, + val currentStoryIndex: Int, + val currentStory: Stories, + val isScanInProgress: Boolean, + val onGoToPreviousStory: () -> Unit = {}, + val onGoToNextStory: () -> Unit = {}, + val onCreateNewWalletButtonClick: () -> Unit = {}, + val onAddExistingWalletButtonClick: () -> Unit = {}, + val onScanButtonClick: () -> Unit = {}, +) + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun StoriesScreenContentV2Preview( + @PreviewParameter(StoriesScreenContentV2ConfigProvider::class) config: StoriesScreenContentV2Config, +) { + TangemThemePreview { + StoriesScreenContentV2(config = config) + } +} + +private class StoriesScreenContentV2ConfigProvider : CollectionPreviewParameterProvider( + collection = listOf( + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 0, + currentStory = Stories.TangemIntro, + isScanInProgress = true, + ), + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 1, + currentStory = Stories.RevolutionaryWallet, + isScanInProgress = false, + ), + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 2, + currentStory = Stories.UltraSecureBackup, + isScanInProgress = false, + ), + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 3, + currentStory = Stories.Currencies, + isScanInProgress = false, + ), + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 4, + currentStory = Stories.Web3, + isScanInProgress = false, + ), + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 5, + currentStory = Stories.WalletForEveryone, + isScanInProgress = false, + ), + ), +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt new file mode 100644 index 0000000000..6b620da6dc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt @@ -0,0 +1,124 @@ +package com.tangem.tap.features.home.compose.views + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags +import com.tangem.wallet.R + +@Composable +internal fun HomeButtonsV2( + btnScanStateInProgress: Boolean, + onScanButtonClick: () -> Unit, + onCreateNewWalletButtonClick: () -> Unit, + onAddExistingWalletButtonClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + CreateNewWalletButton( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.STORIES_SCREEN_CREATE_NEW_WALLET_BUTTON), + onClick = onCreateNewWalletButtonClick, + ) + AddExistingWalletButton( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.STORIES_SCREEN_ADD_EXISTING_WALLET_BUTTON), + onClick = onAddExistingWalletButtonClick, + ) + ScanCardButton( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.STORIES_SCREEN_SCAN_BUTTON), + showProgress = btnScanStateInProgress, + onClick = onScanButtonClick, + ) + } +} + +@Composable +private fun CreateNewWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + StoriesButton( + modifier = modifier, + text = stringResourceSafe(id = R.string.home_button_create_new_wallet), + useDarkerColors = false, + onClick = onClick, + ) +} + +@Composable +private fun AddExistingWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + StoriesButton( + modifier = modifier, + text = stringResourceSafe(id = R.string.home_button_add_existing_wallet), + useDarkerColors = true, + onClick = onClick, + ) +} + +@Composable +private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + StoriesButton( + modifier = modifier, + text = stringResourceSafe(id = R.string.home_button_scan), + useDarkerColors = true, + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), + onClick = onClick, + showProgress = showProgress, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun HomeButtonsV2Preview(@PreviewParameter(HomeButtonsV2ParameterProvider::class) state: HomeButtonsV2State) { + TangemThemePreview { + Box( + modifier = Modifier.background(Color.Black), + ) { + HomeButtonsV2( + btnScanStateInProgress = state.btnScanStateInProgress, + onCreateNewWalletButtonClick = {}, + onAddExistingWalletButtonClick = {}, + onScanButtonClick = {}, + modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), + ) + } + } +} + +private class HomeButtonsV2ParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + HomeButtonsV2State( + btnScanStateInProgress = false, + ), + HomeButtonsV2State( + btnScanStateInProgress = true, + ), + ), +) + +private data class HomeButtonsV2State( + val btnScanStateInProgress: Boolean, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index b1d45779ba..d36ea27e67 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -7,6 +7,7 @@ import org.rekotlin.StateType // todo refactor [REDACTED_TASK_KEY] data class HomeState( val scanInProgress: Boolean = false, + val isV2StoriesEnabled: Boolean = false, val stories: ImmutableList = getRestrictedStories().toImmutableList(), ) : StateType { diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 27c5144b7f..92d6363206 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -383,6 +383,8 @@ Unlimited Order Tangem Scan Tangem + Create New Wallet + Add Existing Wallet to %s On %s network This information was generated with AI.\nTap here, if you find any errors. diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt index d7de6de18e..feca4f686c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt @@ -4,6 +4,8 @@ object TestTags { const val STORIES_SCREEN = "STORIES_SCREEN_CONTAINER" const val STORIES_SCREEN_SCAN_BUTTON = "STORIES_SCREEN_SCAN_BUTTON" const val STORIES_SCREEN_ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON" + const val STORIES_SCREEN_CREATE_NEW_WALLET_BUTTON = "STORIES_SCREEN_CREATE_NEW_WALLET_BUTTON" + const val STORIES_SCREEN_ADD_EXISTING_WALLET_BUTTON = "STORIES_SCREEN_ADD_EXISTING_WALLET_BUTTON" const val MAIN_SCREEN = "MAIN_SCREEN_CONTAINER" const val MAIN_SCREEN_MORE_BUTTON = "MAIN_SCREEN_MORE_BUTTON" From 0d63a74f752e6a10b795e368e4c2421297025f90 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Jun 2025 15:08:55 +0500 Subject: [PATCH 31/99] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 6 +- .../CreateCryptoCurrencyUseCase.kt | 65 +++++++++++++++++++ .../managetokens/CreateCurrencyUseCase.kt | 26 -------- .../utils/CustomCurrencyValidator.kt | 6 +- 4 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt delete mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index a10bcc05e1..38eae7aec7 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -36,8 +36,10 @@ internal object ManageTokensDomainModule { @Provides @Singleton - fun provideCreateCurrencyUseCase(customTokensRepository: CustomTokensRepository): CreateCurrencyUseCase { - return CreateCurrencyUseCase(customTokensRepository) + fun provideCreateCryptoCurrencyUseCase( + customTokensRepository: CustomTokensRepository, + ): CreateCryptoCurrencyUseCase { + return CreateCryptoCurrencyUseCase(customTokensRepository) } @Provides diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt new file mode 100644 index 0000000000..7cdb98ba3d --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt @@ -0,0 +1,65 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Creates cryptocurrency from manage token + */ +class CreateCryptoCurrencyUseCase( + private val customTokensRepository: CustomTokensRepository, +) { + + /** + * Creates currency from default crypto currency + * + * @param token supported crypto currency info + * @param userWalletId selected user wallet id + */ + suspend operator fun invoke( + token: ManagedCryptoCurrency.Token, + userWalletId: UserWalletId, + ): Either> = Either.catch { + token.availableNetworks + .map { sourceNetwork -> + when (sourceNetwork) { + is ManagedCryptoCurrency.SourceNetwork.Default -> customTokensRepository.createToken( + managedCryptoCurrency = token, + sourceNetwork = sourceNetwork, + rawId = CryptoCurrency.RawID(token.id.value), + ) + is ManagedCryptoCurrency.SourceNetwork.Main -> customTokensRepository.createCoin( + userWalletId = userWalletId, + networkId = sourceNetwork.id, + derivationPath = sourceNetwork.network.derivationPath, + ) + } + } + } + + /** + * Creates currency from custom crypto currency + * + * @param userWalletId selected user wallet id + * @param networkId currency network id + * @param derivationPath custom derivation path + * @param formValues token info (e.i. name, contract address, symbol, decimals) + */ + suspend operator fun invoke( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + formValues: AddCustomTokenForm.Validated.All?, + ): Either = Either.catch { + if (formValues == null) { + customTokensRepository.createCoin(userWalletId, networkId, derivationPath) + } else { + customTokensRepository.createCustomToken(userWalletId, networkId, derivationPath, formValues) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt deleted file mode 100644 index 5e8438b839..0000000000 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCurrencyUseCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.domain.managetokens - -import arrow.core.Either -import com.tangem.domain.managetokens.model.AddCustomTokenForm -import com.tangem.domain.managetokens.repository.CustomTokensRepository -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.wallets.models.UserWalletId - -class CreateCurrencyUseCase( - private val repository: CustomTokensRepository, -) { - - suspend operator fun invoke( - userWalletId: UserWalletId, - networkId: Network.ID, - derivationPath: Network.DerivationPath, - formValues: AddCustomTokenForm.Validated.All?, - ): Either = Either.catch { - if (formValues == null) { - repository.createCoin(userWalletId, networkId, derivationPath) - } else { - repository.createCustomToken(userWalletId, networkId, derivationPath, formValues) - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt index 1bdbfd739b..01b8c9cb51 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt @@ -3,7 +3,7 @@ package com.tangem.features.managetokens.utils import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase -import com.tangem.domain.managetokens.CreateCurrencyUseCase +import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase import com.tangem.domain.managetokens.FindTokenUseCase import com.tangem.domain.managetokens.ValidateTokenFormUseCase import com.tangem.domain.managetokens.model.AddCustomTokenForm @@ -23,7 +23,7 @@ import javax.inject.Inject @ModelScoped internal class CustomCurrencyValidator @Inject constructor( private val validateTokenFormUseCase: ValidateTokenFormUseCase, - private val createCustomCurrencyUseCase: CreateCurrencyUseCase, + private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase, private val findTokenUseCase: FindTokenUseCase, private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase, ) { @@ -164,7 +164,7 @@ internal class CustomCurrencyValidator @Inject constructor( derivationPath: Network.DerivationPath, validatedForm: AddCustomTokenForm.Validated.All?, ) { - val currency = createCustomCurrencyUseCase( + val currency = createCryptoCurrencyUseCase( userWalletId = userWalletId, networkId = networkId, derivationPath = derivationPath, From f9feacf074c9caacf547185a111dc19dc246ef6f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Jun 2025 14:19:58 +0500 Subject: [PATCH 32/99] Updated on 2026-08-14 --- .../inputrow/InputRowEnterInfoAmount.kt | 2 +- .../src/main/res/drawable/ic_fee_new_24.xml | 36 +++++--- .../main/res/drawable/ic_token_info_24.xml | 13 +++ core/ui/src/main/res/drawable/ic_union_16.xml | 12 --- .../send/v2/api/FeeSelectorBlockComponent.kt | 9 ++ .../DefaultFeeSelectorBlockComponent.kt | 92 +++++++++++++++++++ .../di/FeeSelectorFeatureModule.kt | 6 ++ features/walletconnect/impl/build.gradle.kts | 1 + .../routing/DefaultWcRoutingComponent.kt | 5 + .../PreviewFeeSelectorBlockComponent.kt | 63 +++++++++++++ .../chain/WcAddNetworkContainerComponent.kt | 4 +- .../WcCommonTransactionComponentDelegate.kt | 6 ++ .../send/WcSendTransactionComponent.kt | 3 + .../WcSendTransactionContainerComponent.kt | 10 +- .../sign/WcSignTransactionComponent.kt | 3 + .../WcSignTransactionContainerComponent.kt | 10 +- .../transaction/ui/common/WcNetworkFeeItem.kt | 73 --------------- .../ui/common/WcSendTransactionItems.kt | 12 +-- .../send/WcSendTransactionModalBottomSheet.kt | 15 ++- ...cSignTransactionModalBottomSheetContent.kt | 23 ++++- 20 files changed, 278 insertions(+), 120 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_token_info_24.xml delete mode 100644 core/ui/src/main/res/drawable/ic_union_16.xml create mode 100644 features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt index 5a92d21899..63903ea854 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt @@ -141,7 +141,7 @@ fun InputRowEnterInfoAmountV2( ) Icon( modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.ic_union_16), + painter = painterResource(R.drawable.ic_token_info_24), tint = TangemTheme.colors.icon.informative, contentDescription = null, ) diff --git a/core/ui/src/main/res/drawable/ic_fee_new_24.xml b/core/ui/src/main/res/drawable/ic_fee_new_24.xml index a644c488cf..435ad051d4 100644 --- a/core/ui/src/main/res/drawable/ic_fee_new_24.xml +++ b/core/ui/src/main/res/drawable/ic_fee_new_24.xml @@ -1,21 +1,27 @@ + android:width="24dp" + android:height="24dp" + android:autoMirrored="true" + android:viewportWidth="24" + android:viewportHeight="24"> + + android:fillColor="#0099FF" + android:fillType="evenOdd" + android:pathData="M12,3.547C11.407,3.547 10.866,3.701 10.283,3.956C9.721,4.201 9.073,4.562 8.271,5.008L5.073,6.788C4.611,7.045 4.214,7.374 3.939,7.825C3.662,8.278 3.547,8.793 3.547,9.357L3.547,9.411C3.546,9.602 3.546,9.825 3.599,10.027C3.733,10.53 4.055,10.86 4.448,11.035C4.663,11.13 4.887,11.175 5.088,11.194V16.66C4.167,17.188 3.547,18.181 3.547,19.318C3.547,19.945 4.055,20.453 4.682,20.453H11.23C11.644,20.453 11.98,20.117 11.98,19.703C11.98,19.289 11.644,18.953 11.23,18.953H5.09C5.254,18.267 5.872,17.757 6.608,17.757H11.23C11.644,17.757 11.98,17.421 11.98,17.007C11.98,16.593 11.644,16.257 11.23,16.257H9.669V11.21H18.572C18.834,11.21 19.205,11.19 19.553,11.035C19.946,10.86 20.268,10.53 20.401,10.027C20.455,9.825 20.454,9.602 20.454,9.411L20.454,9.357C20.454,8.793 20.339,8.278 20.062,7.825C19.787,7.374 19.39,7.045 18.928,6.788L15.73,5.008C14.928,4.562 14.28,4.201 13.718,3.956C13.134,3.701 12.594,3.547 12,3.547ZM8.968,6.337C9.81,5.868 10.395,5.543 10.883,5.33C11.356,5.124 11.685,5.047 12,5.047C12.316,5.047 12.645,5.124 13.118,5.33C13.606,5.543 14.191,5.868 15.033,6.337L18.198,8.099C18.506,8.269 18.679,8.437 18.782,8.606C18.883,8.772 18.954,9 18.954,9.357C18.954,9.489 18.954,9.563 18.951,9.617C18.95,9.635 18.949,9.646 18.949,9.652C18.948,9.657 18.946,9.66 18.945,9.663L18.942,9.664C18.923,9.673 18.884,9.686 18.817,9.695C18.75,9.705 18.67,9.71 18.572,9.71H5.429C5.331,9.71 5.251,9.705 5.184,9.695C5.116,9.686 5.078,9.673 5.059,9.664L5.056,9.663C5.055,9.66 5.053,9.657 5.052,9.652C5.051,9.646 5.051,9.635 5.05,9.617C5.047,9.563 5.047,9.489 5.047,9.357C5.047,9 5.118,8.772 5.219,8.606C5.322,8.437 5.495,8.269 5.802,8.099L8.968,6.337ZM6.608,16.257L6.588,16.257V11.21H8.169V16.257H6.608Z" /> + + android:fillColor="#0099FF" + android:pathData="M13.396,18.788C13.103,19.081 13.103,19.556 13.396,19.849C13.689,20.142 14.164,20.142 14.457,19.849L19.849,14.456C20.142,14.163 20.142,13.689 19.849,13.396C19.556,13.103 19.081,13.103 18.788,13.396L13.396,18.788Z" /> + + android:fillColor="#0099FF" + android:fillType="evenOdd" + android:pathData="M13.176,14.503C13.176,13.77 13.77,13.176 14.504,13.176C15.237,13.176 15.832,13.77 15.832,14.503C15.832,15.237 15.237,15.831 14.504,15.831C13.77,15.831 13.176,15.237 13.176,14.503ZM14.504,14.676C14.599,14.676 14.676,14.599 14.676,14.503C14.676,14.408 14.599,14.331 14.504,14.331C14.409,14.331 14.332,14.408 14.332,14.503C14.332,14.599 14.409,14.676 14.504,14.676Z" /> + + android:fillColor="#0099FF" + android:fillType="evenOdd" + android:pathData="M18.741,17.412C18.008,17.412 17.413,18.007 17.413,18.74C17.413,19.473 18.008,20.068 18.741,20.068C19.474,20.068 20.069,19.473 20.069,18.74C20.069,18.007 19.474,17.412 18.741,17.412ZM18.913,18.74C18.913,18.835 18.836,18.912 18.741,18.912C18.646,18.912 18.569,18.835 18.569,18.74C18.569,18.645 18.646,18.568 18.741,18.568C18.836,18.568 18.913,18.645 18.913,18.74Z" /> + diff --git a/core/ui/src/main/res/drawable/ic_token_info_24.xml b/core/ui/src/main/res/drawable/ic_token_info_24.xml new file mode 100644 index 0000000000..87c467d679 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_token_info_24.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_union_16.xml b/core/ui/src/main/res/drawable/ic_union_16.xml deleted file mode 100644 index 3281f5e0ff..0000000000 --- a/core/ui/src/main/res/drawable/ic_union_16.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..bfb687f2eb --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt @@ -0,0 +1,9 @@ +package com.tangem.features.send.v2.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface FeeSelectorBlockComponent : ComposableContentComponent { + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..96c3c5fd9e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.features.send.v2.feeselector + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.impl.R +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Unit, +) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext { + + @Composable + override fun Content(modifier: Modifier) { + FeeSelectorBlockContent(modifier = modifier) + } + + @AssistedFactory + interface Factory : FeeSelectorBlockComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultFeeSelectorBlockComponent + } +} + +@Composable +private fun FeeSelectorBlockContent(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background(TangemTheme.colors.background.action) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_fee_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing6) + .size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_token_info_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "~ 0.25 \$", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier.size(width = 18.dp, height = 24.dp), + painter = painterResource(id = R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FeeSelectorBlockContent_Preview() { + TangemThemePreview { + FeeSelectorBlockContent(modifier = Modifier.fillMaxWidth()) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorFeatureModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorFeatureModule.kt index ac270168fe..1b6f3fe484 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorFeatureModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorFeatureModule.kt @@ -1,6 +1,8 @@ package com.tangem.features.send.v2.feeselector.di +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.FeeSelectorComponent +import com.tangem.features.send.v2.feeselector.DefaultFeeSelectorBlockComponent import com.tangem.features.send.v2.feeselector.DefaultFeeSelectorComponent import dagger.Binds import dagger.Module @@ -15,4 +17,8 @@ internal interface FeeSelectorFeatureModule { @Binds @Singleton fun bindComponentFactory(factory: DefaultFeeSelectorComponent.Factory): FeeSelectorComponent.Factory + + @Binds + @Singleton + fun bindBlockComponentFactory(factory: DefaultFeeSelectorBlockComponent.Factory): FeeSelectorBlockComponent.Factory } \ No newline at end of file diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 53a4e3c17c..625e3afceb 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { implementation(projects.features.wallet.api) implementation(projects.features.walletconnect.api) + implementation(projects.features.sendV2.api) /** Core */ implementation(projects.core.configToggles) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt index 4e0a019cb7..adbaa219c2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt @@ -14,6 +14,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.components.WcRoutingComponent import com.tangem.features.walletconnect.connections.components.AlertsComponent import com.tangem.features.walletconnect.connections.components.WcPairComponent @@ -29,6 +30,7 @@ import dagger.assisted.AssistedInject internal class DefaultWcRoutingComponent @AssistedInject constructor( @Assisted private val appComponentContext: AppComponentContext, @Assisted params: Unit, + private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, ) : AppComponentContext by appComponentContext, WcRoutingComponent { private val model: WcRoutingModel = getOrCreateModel() @@ -62,14 +64,17 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( is WcInnerRoute.SignMessage -> WcSignTransactionContainerComponent( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), + feeSelectorBlockComponentFactory = feeSelectorBlockComponentFactory, ) is WcInnerRoute.AddNetwork -> WcAddNetworkContainerComponent( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), + feeSelectorBlockComponentFactory = feeSelectorBlockComponentFactory, ) is WcInnerRoute.Send -> WcSendTransactionContainerComponent( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), + feeSelectorBlockComponentFactory = feeSelectorBlockComponentFactory, ) is WcInnerRoute.Pair -> WcPairComponent( appComponentContext = childContext, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..00c218f576 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt @@ -0,0 +1,63 @@ +package com.tangem.features.walletconnect.transaction.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.walletconnect.impl.R + +internal class PreviewFeeSelectorBlockComponent : FeeSelectorBlockComponent { + @Composable + override fun Content(modifier: Modifier) { + Row( + modifier = modifier + .background(TangemTheme.colors.background.action) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_fee_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing6) + .size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_token_info_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "~ 0.25 \$", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier.size(width = 18.dp, height = 24.dp), + painter = painterResource(id = R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt index 7c9b4b33fc..86d6e850a2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.components.common.WcCommonTransactionComponentDelegate import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.common.getWcCommonScreen @@ -14,7 +15,8 @@ import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal class WcAddNetworkContainerComponent( appComponentContext: AppComponentContext, params: WcTransactionModelParams, -) : WcCommonTransactionComponentDelegate(appComponentContext) { + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : WcCommonTransactionComponentDelegate(appComponentContext, feeSelectorBlockComponentFactory) { private val model: WcAddNetworkModel = getOrCreateModel(params = params) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt index 1be9296325..87ae3eec22 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt @@ -15,10 +15,12 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal abstract class WcCommonTransactionComponentDelegate( private val appComponentContext: AppComponentContext, + private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, ) : AppComponentContext by appComponentContext, ComposableContentComponent { private var stackNavigation: StackNavigation? = null @@ -29,6 +31,10 @@ internal abstract class WcCommonTransactionComponentDelegate( InnerRouter(stackNavigation = it, popCallback = { onChildBack() }) } } + protected val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create( + context = appComponentContext, + params = Unit, + ) protected fun init( navigation: StackNavigation, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt index 2953ccb838..a6b69fd2b0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt @@ -5,12 +5,14 @@ import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel import com.tangem.features.walletconnect.transaction.ui.send.WcSendTransactionModalBottomSheet internal class WcSendTransactionComponent( private val appComponentContext: AppComponentContext, private val model: WcSendTransactionModel, + private val feeSelectorBlockComponent: FeeSelectorBlockComponent, ) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { override fun dismiss() { @@ -24,6 +26,7 @@ internal class WcSendTransactionComponent( if (content != null) { WcSendTransactionModalBottomSheet( state = content!!.transaction, + feeSelectorBlockComponent = feeSelectorBlockComponent, onClickTransactionRequest = model::showTransactionRequest, onBack = router::pop, onDismiss = ::dismiss, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt index 420e419ce1..e80290de33 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.components.common.WcCommonTransactionComponentDelegate import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.common.getWcCommonScreen @@ -14,7 +15,8 @@ import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal class WcSendTransactionContainerComponent( appComponentContext: AppComponentContext, params: WcTransactionModelParams, -) : WcCommonTransactionComponentDelegate(appComponentContext) { + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : WcCommonTransactionComponentDelegate(appComponentContext, feeSelectorBlockComponentFactory) { private val model: WcSendTransactionModel = getOrCreateModel(params = params) @@ -40,6 +42,10 @@ internal class WcSendTransactionContainerComponent( } private fun createTransactionComponent(appComponentContext: AppComponentContext): WcSendTransactionComponent { - return WcSendTransactionComponent(appComponentContext = appComponentContext, model = model) + return WcSendTransactionComponent( + appComponentContext = appComponentContext, + model = model, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt index 92550465c1..4ca73d8c25 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt @@ -5,12 +5,14 @@ import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel import com.tangem.features.walletconnect.transaction.ui.sign.WcSignTransactionModalBottomSheetContent internal class WcSignTransactionComponent( private val appComponentContext: AppComponentContext, private val model: WcSignTransactionModel, + private val feeSelectorBlockComponent: FeeSelectorBlockComponent, ) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { override fun dismiss() { @@ -24,6 +26,7 @@ internal class WcSignTransactionComponent( if (content != null) { WcSignTransactionModalBottomSheetContent( state = content!!.transaction, + feeSelectorBlockComponent = feeSelectorBlockComponent, onClickTransactionRequest = model::showTransactionRequest, onBack = router::pop, onDismiss = ::dismiss, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt index cdfd63915b..c9d6c4bdba 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.components.common.WcCommonTransactionComponentDelegate import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.common.getWcCommonScreen @@ -14,7 +15,8 @@ import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal class WcSignTransactionContainerComponent( appComponentContext: AppComponentContext, params: WcTransactionModelParams, -) : WcCommonTransactionComponentDelegate(appComponentContext) { + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : WcCommonTransactionComponentDelegate(appComponentContext, feeSelectorBlockComponentFactory) { private val model: WcSignTransactionModel = getOrCreateModel(params = params) @@ -40,6 +42,10 @@ internal class WcSignTransactionContainerComponent( } private fun createTransactionComponent(appComponentContext: AppComponentContext): WcSignTransactionComponent { - return WcSignTransactionComponent(appComponentContext = appComponentContext, model = model) + return WcSignTransactionComponent( + appComponentContext = appComponentContext, + model = model, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt deleted file mode 100644 index df6080b3e4..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.features.walletconnect.transaction.ui.common - -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.walletconnect.impl.R - -@Composable -internal fun WcNetworkFeeItem(networkFeeText: String, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(R.drawable.ic_fee_new_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - - Text( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), - text = stringResourceSafe(R.string.common_network_fee_title), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - - Icon( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing6) - .size(TangemTheme.dimens.size16), - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - - Spacer(modifier = Modifier.weight(1f)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End, - ) { - AnimatedContent( - targetState = networkFeeText, - label = "Animate Network fee text", - ) { fee -> - Text( - text = fee, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - ) - } - Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing6)) - Icon( - modifier = Modifier - .clip(CircleShape) - .size(width = TangemTheme.dimens.size18, height = TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_select_18_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt index 0767feb930..80054def8d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt @@ -12,14 +12,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.divider.DividerWithPadding import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM @Composable internal fun WcSendTransactionItems( walletName: String, networkInfo: WcNetworkInfoUM, + feeSelectorBlockComponent: FeeSelectorBlockComponent, modifier: Modifier = Modifier, - networkFee: String? = null, address: String? = null, ) { Column( @@ -50,12 +51,7 @@ internal fun WcSendTransactionItems( addressText = address, ) } - if (!networkFee.isNullOrEmpty()) { - DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) - WcNetworkFeeItem( - modifier = itemsModifier, - networkFeeText = networkFee, - ) - } + DividerWithPadding(start = 40.dp, end = 12.dp) + feeSelectorBlockComponent.Content(Modifier) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index 70a4e5ec02..9e66c41bc8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -24,9 +24,11 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.components.PreviewFeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM @@ -40,9 +42,11 @@ import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequ import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem import kotlinx.collections.immutable.persistentListOf +@Suppress("LongParameterList") @Composable internal fun WcSendTransactionModalBottomSheet( state: WcSendTransactionItemUM, + feeSelectorBlockComponent: FeeSelectorBlockComponent, onClickTransactionRequest: () -> Unit, onBack: () -> Unit, onDismiss: () -> Unit, @@ -99,7 +103,7 @@ internal fun WcSendTransactionModalBottomSheet( WcSendTransactionItems( walletName = state.walletName, networkInfo = state.networkInfo, - networkFee = state.networkFee, + feeSelectorBlockComponent = feeSelectorBlockComponent, address = state.address, ) } @@ -141,7 +145,14 @@ private fun WcSendTransactionBottomSheetPreview( ) }, content = { - WcSendTransactionModalBottomSheet(state, {}, {}, {}, {}) + WcSendTransactionModalBottomSheet( + state = state, + feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(), + onClickTransactionRequest = {}, + onBack = {}, + onDismiss = {}, + onClickAllowToSpend = {}, + ) }, ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt index 3791a02ae4..bdf2adb43a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt @@ -25,9 +25,11 @@ import com.tangem.core.ui.components.divider.DividerWithPadding import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.components.PreviewFeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM @@ -36,6 +38,7 @@ import com.tangem.features.walletconnect.transaction.ui.common.* @Composable internal fun WcSignTransactionModalBottomSheetContent( state: WcSignTransactionItemUM, + feeSelectorBlockComponent: FeeSelectorBlockComponent, onClickTransactionRequest: () -> Unit, onBack: () -> Unit, onDismiss: () -> Unit, @@ -84,7 +87,7 @@ internal fun WcSignTransactionModalBottomSheetContent( ) } Column(modifier = Modifier.padding(top = 16.dp)) { - WcSignTransactionItems(state) + WcSignTransactionItems(state, feeSelectorBlockComponent) } } }, @@ -101,9 +104,13 @@ internal fun WcSignTransactionModalBottomSheetContent( } @Composable -private fun WcSignTransactionItems(state: WcSignTransactionItemUM) { +private fun WcSignTransactionItems( + state: WcSignTransactionItemUM, + feeSelectorBlockComponent: FeeSelectorBlockComponent, + modifier: Modifier = Modifier, +) { Column( - modifier = Modifier + modifier = modifier .clip(RoundedCornerShape(14.dp)) .background(color = TangemTheme.colors.background.action) .fillMaxWidth() @@ -130,6 +137,8 @@ private fun WcSignTransactionItems(state: WcSignTransactionItemUM) { addressText = state.addressText, ) } + DividerWithPadding(start = 40.dp, end = 12.dp) + feeSelectorBlockComponent.Content(modifier = Modifier) } } @@ -157,7 +166,13 @@ private fun WcSignTransactionBottomSheetPreview( ) }, content = { - WcSignTransactionModalBottomSheetContent(state, {}, {}, {}) + WcSignTransactionModalBottomSheetContent( + state = state, + feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(), + onClickTransactionRequest = {}, + onBack = {}, + onDismiss = {}, + ) }, ) } From 07f63b47e4d7f276dce1aa309cd05804dbd92f99 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Jun 2025 16:50:40 +0500 Subject: [PATCH 33/99] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 4 + core/res/src/main/res/values/strings.xml | 6 +- .../components/containers/FooterContainer.kt | 4 +- .../components/inputrow/InputRowRecipient.kt | 87 +++++++++++++++---- .../components/inputrow/inner/PasteButton.kt | 16 +++- .../core/ui/extensions/MarkdownExtension.kt | 36 ++++++++ core/ui/src/main/res/drawable/ic_scan_16.xml | 10 +++ .../send/v2/api/SendFeatureToggles.kt | 5 +- .../send/v2/DefaultSendFeatureToggles.kt | 7 +- .../features/send/v2/send/model/SendModel.kt | 3 + .../destination/model/SendDestinationModel.kt | 16 +++- .../SendDestinationInitialStateTransformer.kt | 2 + .../destination/ui/SendDestinationContent.kt | 66 +++++++++++++- .../destination/ui/TextFieldWithPaste.kt | 14 +++ .../destination/ui/state/DestinationUM.kt | 1 + 15 files changed, 245 insertions(+), 32 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_scan_16.xml diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 602bd04e20..a63c34bcdd 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -70,5 +70,9 @@ { "name": "SWAP_REDESIGN_ENABLED", "version": "undefined" + }, + { + "name": "SEND_REDESIGN_ENABLED", + "version": "undefined" } ] diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 92d6363206..41d8a84e62 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -856,8 +856,12 @@ Recipient Not a valid address Ensure the receiving wallet address is on the %s network to avoid losing your tokens + send to %s + Ensure you %s network address, as errors may result in lost transfers Send to A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds + Memo / Destination Tag is a code separating transactions to a shared recipient in a crypto network. + Caution: Missing a memo may lead to fund loss. My wallets A way of measuring Bitcoin transaction fees. It indicates the number of the smallest Bitcoin unit (Satoshi) for each virtual byte in a transaction. The higher the number, the faster the transaction will be processed by miners. Satoshi / vByte @@ -885,7 +889,7 @@ Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name - Join staking with this token + Put your token to work The amount to stake must be at least %s Staking amount will be rounded to %1$s TRX due to network rules. Unstaking amount will be rounded to %1$s TRX due to network rules. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt index bebd090e66..028be341c1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.res.TangemTheme /** @@ -32,7 +32,7 @@ fun FooterContainer( AnimatedVisibility(visible = footer != null) { val footerWrapped = remember(this) { requireNotNull(footer) } Text( - text = footerWrapped.resolveReference(), + text = footerWrapped.resolveAnnotatedReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier 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 index 8d5363d3cc..8991b68840 100644 --- 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 @@ -2,6 +2,10 @@ package com.tangem.core.ui.components.inputrow import android.content.res.Configuration import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -16,6 +20,7 @@ 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.buttons.small.TangemIconButton 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.CrossIcon @@ -25,24 +30,29 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION import kotlinx.coroutines.delay /** - * [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode=design&t=IQ5lBJEkFGU4WSvi-4) + * * [Design Reference](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 + * @param title The title text reference to display above the input field + * @param value The current recipient address value to display in the input field + * @param placeholder The placeholder text reference to show when the input is empty + * @param onValueChange Callback invoked when the input value changes, receives the new string value + * @param onPasteClick Callback invoked when the paste button is clicked, receives the pasted string + * @param onQrCodeClick Callback invoked when the QR code scan button is clicked + * @param modifier Optional modifier to apply to the composable + * @param singleLine Whether the text field should be constrained to a single line (default: false) + * @param error Optional error text reference to display when validation fails + * @param isError Whether the input is in an error state, affects styling and title color + * @param showDivider Whether to show a divider below the component (default: false) + * @param isLoading Whether to show a loading indicator instead of the identicon (default: false) + * @param isValuePasted Whether the current value was pasted, affects visual feedback (default: false) * - * @see InputRowRecipientDefault for readonly version + * @see InputRowRecipientDefault for a read-only version of this component */ +@Suppress("LongParameterList", "LongMethod") @Composable fun InputRowRecipient( title: TextReference, @@ -50,6 +60,8 @@ fun InputRowRecipient( placeholder: TextReference, onValueChange: (String) -> Unit, onPasteClick: (String) -> Unit, + onQrCodeClick: () -> Unit, + isRedesignEnabled: Boolean, modifier: Modifier = Modifier, singleLine: Boolean = false, error: TextReference? = null, @@ -109,18 +121,55 @@ fun InputRowRecipient( .padding(start = TangemTheme.dimens.spacing8), ) } - PasteButton( - isPasteButtonVisible = value.isBlank(), - onClick = onPasteClick, + Row( modifier = Modifier - .align(CenterEnd) - .padding(start = TangemTheme.dimens.spacing8), - ) + .align(CenterEnd), + ) { + if (isRedesignEnabled) { + QrButton( + visible = !singleLine, + onQrCodeClick = onQrCodeClick, + ) + } + PasteButton( + isPasteButtonVisible = value.isBlank(), + onClick = onPasteClick, + backgroundColorEnabled = if (isRedesignEnabled) { + TangemTheme.colors.button.secondary + } else { + TangemTheme.colors.button.primary + }, + textColor = if (isRedesignEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.primary2 + }, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8), + ) + } } } } } +@Composable +private fun QrButton(visible: Boolean, onQrCodeClick: () -> Unit, modifier: Modifier = Modifier) { + AnimatedVisibility( + visible = visible, + enter = fadeIn(), + exit = fadeOut(animationSpec = tween(DEFAULT_ANIMATION_DURATION)), + modifier = modifier, + ) { + TangemIconButton( + iconRes = R.drawable.ic_scan_16, + onClick = onQrCodeClick, + background = TangemTheme.colors.button.secondary, + iconTint = TangemTheme.colors.text.primary1, + ) + } +} + @Composable private fun RowScope.InputIcon(isLoading: Boolean, value: String) { var isLoadingProxy by remember { mutableStateOf(isLoading) } @@ -172,7 +221,9 @@ private fun InputRowRecipientPreview( showDivider = true, onValueChange = {}, onPasteClick = {}, + onQrCodeClick = {}, modifier = Modifier.background(TangemTheme.colors.background.primary), + isRedesignEnabled = false, ) } } 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 index ef23a4babc..d816b39eb6 100644 --- 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 @@ -14,6 +14,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalHapticFeedback @@ -34,14 +35,21 @@ import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION * @param modifier composable modifier */ @Composable -fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { +fun PasteButton( + isPasteButtonVisible: Boolean, + onClick: (String) -> Unit, + modifier: Modifier = Modifier, + backgroundColorEnabled: Color = TangemTheme.colors.button.primary, + backgroundColorDisabled: Color = TangemTheme.colors.button.secondary, + textColor: Color = TangemTheme.colors.text.primary2, +) { val clipboardManager = LocalClipboardManager.current val hapticFeedback = LocalHapticFeedback.current val isPasteEnabled = !clipboardManager.getText()?.text.isNullOrEmpty() val color = if (isPasteEnabled) { - TangemTheme.colors.button.primary + backgroundColorEnabled } else { - TangemTheme.colors.button.secondary + backgroundColorDisabled } AnimatedVisibility( visible = isPasteButtonVisible, @@ -53,7 +61,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi Text( text = stringResourceSafe(R.string.common_paste), style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary2, + color = textColor, modifier = Modifier .background( color = color, diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt index c510835242..f1e49e308f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt @@ -60,4 +60,40 @@ fun AnnotatedString.Builder.appendSpace() = append(" ") fun AnnotatedString.Builder.appendColored(text: String, color: Color) = withStyle(SpanStyle(color = color)) { append(text) +} + +/** + * Appends text from a template string to the AnnotatedString.Builder, replacing a placeholder (default "%s") + * with custom styled content provided by a lambda. The lambda allows you to insert styled or complex content + * (e.g., colored, bold, or annotated text) at the placeholder position. + * + * If the placeholder is not found in the template, the function does nothing. + * + * Example usage: + * builder.appendWithStyledPlaceholder( + * template = "Ensure you %s network address, as errors may result in lost transfers" + * ) { + * withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.Bold)) { + * append("Ethereum") + * } + * } + * + * @param template The template string containing the placeholder to be replaced. + * @param placeholder The placeholder string to be replaced by styled content. Default is "%s". + * @param styledContent Lambda to build the styled content to insert at the placeholder position. + */ +fun AnnotatedString.Builder.appendWithStyledPlaceholder( + template: String, + placeholder: String = "%s", + styledContent: AnnotatedString.Builder.() -> Unit, +) { + val index = template.indexOf(placeholder) + if (index < 0) return + // Append before placeholder + if (index > 0) append(template.substring(0, index)) + // Append styled content + styledContent() + // Append after placeholder + val afterIndex = index + placeholder.length + if (afterIndex < template.length) append(template.substring(afterIndex)) } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_scan_16.xml b/core/ui/src/main/res/drawable/ic_scan_16.xml new file mode 100644 index 0000000000..16a9aa2ffb --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_scan_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt index 4dbf452662..0e08965245 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt @@ -1,3 +1,6 @@ package com.tangem.features.send.v2.api -interface SendFeatureToggles \ No newline at end of file +interface SendFeatureToggles { + + val isSendRedesignEnabled: Boolean +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt index 326f72db7e..cce9c99562 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -4,5 +4,8 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.send.v2.api.SendFeatureToggles internal class DefaultSendFeatureToggles( - @Suppress("UnusedPrivateMember") private val featureToggles: FeatureTogglesManager, -) : SendFeatureToggles \ No newline at end of file + private val featureToggles: FeatureTogglesManager, +) : SendFeatureToggles { + override val isSendRedesignEnabled: Boolean + get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED") +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 9e82a35a76..9aa5fe4c78 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -37,6 +37,7 @@ import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.SendConfirmAlertFactory @@ -87,6 +88,7 @@ internal class SendModel @Inject constructor( private val createTransferTransactionUseCase: CreateTransferTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendAmountUpdateQRTrigger: SendAmountUpdateQRTrigger, + private val sendFeatureToggles: SendFeatureToggles, ) : Model(), SendComponentCallback { private val params: SendComponent.Params = paramsContainer.require() @@ -357,6 +359,7 @@ internal class SendModel @Inject constructor( amountUM = AmountState.Empty(), destinationUM = SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, + isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, ).transform(DestinationUM.Empty()), feeUM = FeeUM.Empty(), confirmUM = ConfirmUM.Empty, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 8ccf0bd907..6bd184f1fc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.isLocked import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource @@ -65,6 +66,7 @@ internal class SendDestinationModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val parseQrCodeUseCase: ParseQrCodeUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val sendFeatureToggles: SendFeatureToggles, ) : Model(), SendDestinationClickIntents { private val params: SendDestinationComponentParams = paramsContainer.require() @@ -90,6 +92,7 @@ internal class SendDestinationModel @Inject constructor( _uiState.update( SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, + isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, isInitialized = true, ), ) @@ -297,6 +300,7 @@ internal class SendDestinationModel @Inject constructor( flow2 = params.currentRoute, transform = { state, route -> state to route }, ).onEach { (state, route) -> + val isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled params.callback.onNavigationResult( NavigationUM.Content( title = params.title, @@ -319,8 +323,16 @@ internal class SendDestinationModel @Inject constructor( } params.onBackClick() }, - additionalIconRes = R.drawable.ic_qrcode_scan_24, - additionalIconClick = ::onQrCodeScanClick, + additionalIconRes = if (isRedesignEnabled) { + null + } else { + R.drawable.ic_qrcode_scan_24 + }, + additionalIconClick = if (isRedesignEnabled) { + null + } else { + ::onQrCodeScanClick + }, primaryButton = ButtonsUM.PrimaryButtonUM( text = if (route.isEditMode) { resourceReference(R.string.common_continue) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt index 76de538ac5..8ad0687969 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt @@ -13,6 +13,7 @@ import com.tangem.utils.transformer.Transformer internal class SendDestinationInitialStateTransformer( val cryptoCurrency: CryptoCurrency, + val isRedesignEnabled: Boolean, val isInitialized: Boolean = false, ) : Transformer { override fun transform(prevState: DestinationUM): DestinationUM { @@ -54,6 +55,7 @@ internal class SendDestinationInitialStateTransformer( recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT), networkName = cryptoCurrency.network.name, isValidating = false, + isRedesignEnabled = isRedesignEnabled, ) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt index 4caf6be2e2..1014976ae1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt @@ -16,6 +16,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowRecipient @@ -60,10 +64,13 @@ internal fun SendDestinationContent( isError = isError, isValidating = isValidating, onAddressChange = clickIntents::onRecipientAddressValueChange, + onQrCodeClick = clickIntents::onQrCodeScanClick, + isRedesignEnabled = state.isRedesignEnabled, ) memoField( memoField = memoField, onMemoChange = clickIntents::onRecipientMemoValueChange, + isRedesignEnabled = state.isRedesignEnabled, ) listHeaderItem( titleRes = R.string.send_recipient_wallets_title, @@ -100,16 +107,23 @@ internal fun SendDestinationContent( } } +@Suppress("LongParameterList") private fun LazyListScope.addressItem( address: DestinationTextFieldUM.RecipientAddress, networkName: String, isError: Boolean, isValidating: Boolean, onAddressChange: (String, EnterAddressSource) -> Unit, + onQrCodeClick: () -> Unit, + isRedesignEnabled: Boolean, ) { item(key = ADDRESS_FIELD_KEY) { FooterContainer( - footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName)), + footer = if (isRedesignEnabled) { + buildHighlightedFooterText(networkName) + } else { + resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName)) + }, ) { InputRowRecipient( value = address.value, @@ -117,6 +131,8 @@ private fun LazyListScope.addressItem( placeholder = address.placeholder, onValueChange = { onAddressChange(it, EnterAddressSource.InputField) }, onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) }, + onQrCodeClick = onQrCodeClick, + isRedesignEnabled = isRedesignEnabled, isError = isError, isLoading = isValidating, error = address.error, @@ -131,9 +147,37 @@ private fun LazyListScope.addressItem( } } +@Composable +private fun buildHighlightedFooterText(networkName: String): TextReference { + return annotatedReference( + buildAnnotatedString { + val styledText = stringResourceSafe( + R.string.send_recipient_address_footer_highlighted_part, + networkName, + ) + val styledColor = TangemTheme.colors.text.secondary + appendWithStyledPlaceholder( + template = stringResourceSafe( + R.string.send_recipient_address_footer_v2, + networkName, + ), + placeholder = networkName, + ) { + withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { + appendColored( + text = styledText, + color = styledColor, + ) + } + } + }, + ) +} + private fun LazyListScope.memoField( memoField: DestinationTextFieldUM.RecipientMemo?, onMemoChange: (String, Boolean) -> Unit, + isRedesignEnabled: Boolean, ) { if (memoField != null) { item(key = MEMO_FIELD_KEY) { @@ -142,7 +186,24 @@ private fun LazyListScope.memoField( value = memoField.value, label = memoField.label, placeholder = placeholder, - footer = resourceReference(R.string.send_recipient_memo_footer), + footer = if (isRedesignEnabled) { + annotatedReference( + buildAnnotatedString { + append(stringResourceSafe(R.string.send_recipient_memo_footer_v2)) + append("\n") + withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { + appendColored( + text = stringResourceSafe( + R.string.send_recipient_memo_footer_v2_highlighted, + ), + color = TangemTheme.colors.text.secondary, + ) + } + }, + ) + } else { + resourceReference(R.string.send_recipient_memo_footer) + }, onValueChange = { onMemoChange(it, false) }, onPasteClick = { onMemoChange(it, true) }, modifier = Modifier.padding(top = 20.dp), @@ -151,6 +212,7 @@ private fun LazyListScope.memoField( error = memoField.error, isReadOnly = !memoField.isEnabled, isValuePasted = memoField.isValuePasted, + isRedesignEnabled = isRedesignEnabled, ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt index 162ab6bf7b..2c2fe6de6a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.components.containers.FooterContainer +@Suppress("LongMethod", "LongParameterList") @Composable internal fun TextFieldWithPaste( value: String, @@ -27,6 +28,7 @@ internal fun TextFieldWithPaste( label: TextReference, onValueChange: (String) -> Unit, onPasteClick: (String) -> Unit, + isRedesignEnabled: Boolean, modifier: Modifier = Modifier, footer: TextReference? = null, labelStyle: TextStyle = TangemTheme.typography.body2, @@ -96,6 +98,18 @@ internal fun TextFieldWithPaste( PasteButton( isPasteButtonVisible = value.isBlank(), onClick = onPasteClick, + backgroundColorEnabled = if (isRedesignEnabled) { + TangemTheme.colors.button.secondary + } else { + TangemTheme.colors.button.primary + }, + textColor = if (isRedesignEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.primary2 + }, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8), ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt index 06935a8368..8a73b412f0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt @@ -17,6 +17,7 @@ internal sealed class DestinationUM { val networkName: String, val isValidating: Boolean = false, val isInitialized: Boolean = false, + val isRedesignEnabled: Boolean = false, ) : DestinationUM() data class Empty( From 5212c214549ad509fc12fcc03b064f3266224b8e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Jun 2025 17:29:12 +0500 Subject: [PATCH 34/99] Updated on 2026-08-14 --- .../ui/amountScreen/AmountScreenContent.kt | 36 +- .../converters/AmountStateConverter.kt | 15 +- .../ui/amountScreen/models/AmountState.kt | 7 +- .../preview/AmountStatePreviewData.kt | 15 +- .../common/ui/amountScreen/ui/AmountBlock.kt | 2 +- .../common/ui/amountScreen/ui/AmountField.kt | 53 +-- .../amountScreen/ui/AmountFieldContainer.kt | 152 +++++++- .../ui/amountScreen/ui/AmountFieldV2.kt | 329 ++++++++++++++++++ .../ui/components/fields/AmountTextField.kt | 5 +- .../AmountVisualTransformation.kt | 47 ++- .../inputrow/InputRowEnterAmount.kt | 1 + .../inputrow/InputRowEnterInfoAmount.kt | 3 + .../tangem/core/ui/extensions/ModifierExt.kt | 4 +- .../onramp/main/ui/OnrampAmountContent.kt | 6 + .../amount/model/SendAmountModel.kt | 3 + 15 files changed, 586 insertions(+), 92 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index d8a18ff233..bdb765bbbf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -12,6 +12,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.common.ui.amountScreen.ui.amountField +import com.tangem.common.ui.amountScreen.ui.amountFieldV2 import com.tangem.common.ui.amountScreen.ui.buttons import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -40,18 +41,28 @@ fun AmountScreenContent( bottom = TangemTheme.dimens.spacing16, ), ) { - amountField( - amountState = amountState, - isBalanceHidden = isBalanceHidden, - onValueChange = clickIntents::onAmountValueChange, - onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, - ) - buttons( - segmentedButtonConfig = amountState.segmentedButtonConfig, - clickIntents = clickIntents, - isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled, - selectedButton = amountState.selectedButton, - ) + if (amountState.isRedesignEnabled) { + amountFieldV2( + amountState = amountState, + onValueChange = clickIntents::onAmountValueChange, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, + onCurrencyChange = clickIntents::onCurrencyChangeClick, + onMaxAmountClick = clickIntents::onMaxValueClick, + ) + } else { + amountField( + amountState = amountState, + isBalanceHidden = isBalanceHidden, + onValueChange = clickIntents::onAmountValueChange, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, + ) + buttons( + segmentedButtonConfig = amountState.segmentedButtonConfig, + clickIntents = clickIntents, + isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled, + selectedButton = amountState.selectedButton, + ) + } } } @@ -75,6 +86,7 @@ private class SendAmountContentPreviewProvider : PreviewParameterProvider get() = sequenceOf( AmountStatePreviewData.amountState, + AmountStatePreviewData.amountStateV2, ) } // endregion \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index 94981c571b..9bef8f3470 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -58,10 +58,11 @@ class AmountStateConverter( return AmountState.Data( title = value.title, availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), + tokenName = stringReference(status.currency.name), tokenIconState = iconStateConverter.convert(status), amountTextField = amountFieldConverter.convert(value.value), isPrimaryButtonEnabled = false, - appCurrencyCode = appCurrency.code, + appCurrency = appCurrency, segmentedButtonConfig = persistentListOf( AmountSegmentedButtonsConfig( title = stringReference(status.currency.symbol), @@ -80,6 +81,7 @@ class AmountStateConverter( ), isSegmentedButtonsEnabled = !noFeeRate, selectedButton = 0, + isRedesignEnabled = false, ) } } @@ -99,6 +101,7 @@ class AmountStateConverterV2( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val maxEnterAmount: EnterAmountBoundary, private val iconStateConverter: CryptoCurrencyToIconStateConverter, + private val isRedesignEnabled: Boolean, ) : Converter { private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { @@ -116,11 +119,16 @@ class AmountStateConverterV2( return AmountState.Data( title = value.title, - availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), + availableBalance = if (isRedesignEnabled) { + resourceReference(R.string.common_balance, wrappedList(crypto)) + } else { + resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)) + }, + tokenName = stringReference(cryptoCurrencyStatus.currency.name), tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus), amountTextField = amountFieldConverter.convert(value.value), isPrimaryButtonEnabled = false, - appCurrencyCode = appCurrency.code, + appCurrency = appCurrency, segmentedButtonConfig = persistentListOf( AmountSegmentedButtonsConfig( title = stringReference(cryptoCurrencyStatus.currency.symbol), @@ -139,6 +147,7 @@ class AmountStateConverterV2( ), isSegmentedButtonsEnabled = !noFeeRate, selectedButton = 0, + isRedesignEnabled = isRedesignEnabled, ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index 9f714127da..b5eab4301d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -3,6 +3,7 @@ package com.tangem.common.ui.amountScreen.models import androidx.compose.runtime.Stable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency import kotlinx.collections.immutable.PersistentList import java.math.BigDecimal @@ -21,7 +22,7 @@ sealed class AmountState { * @param selectedButton selected currency index * @param isSegmentedButtonsEnabled indicates if currency switches is enabled * @param amountTextField amount field state - * @param appCurrencyCode app currency code + * @param appCurrency app currency * @param isEditingDisabled indicated whether amount is editable * @param reduceAmountBy reduces amount to be sent by specified value * @param isIgnoreReduce ignores reduce amount value @@ -30,15 +31,17 @@ sealed class AmountState { override val isPrimaryButtonEnabled: Boolean, val title: TextReference, val availableBalance: TextReference, + val tokenName: TextReference, val tokenIconState: CurrencyIconState, val segmentedButtonConfig: PersistentList, val selectedButton: Int, val isSegmentedButtonsEnabled: Boolean, val amountTextField: AmountFieldModel, - val appCurrencyCode: String, + val appCurrency: AppCurrency, val isEditingDisabled: Boolean = false, val reduceAmountBy: BigDecimal = BigDecimal.ZERO, val isIgnoreReduce: Boolean = false, + val isRedesignEnabled: Boolean, ) : AmountState() data class Empty( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index 63c4d0cd5d..ab6ead2229 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -2,12 +2,16 @@ package com.tangem.common.ui.amountScreen.preview import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.CurrencyIconState 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.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.AmountType import kotlinx.collections.immutable.persistentListOf @@ -15,6 +19,8 @@ import java.math.BigDecimal object AmountStatePreviewData { + val emptyState = AmountState.Empty() + val amountState = AmountState.Data( isPrimaryButtonEnabled = false, title = stringReference("Family Wallet"), @@ -31,7 +37,8 @@ object AmountStatePreviewData { isFiat = true, ), ), - appCurrencyCode = "usd", + appCurrency = AppCurrency.Default, + tokenName = stringReference("Tether"), amountTextField = AmountFieldModel( value = "", onValueChange = {}, @@ -60,6 +67,7 @@ object AmountStatePreviewData { ), isSegmentedButtonsEnabled = true, selectedButton = 0, + isRedesignEnabled = false, ) val amountWithValueState = amountState.copy( @@ -75,6 +83,11 @@ object AmountStatePreviewData { ), ) + val amountStateV2 = amountState.copy( + isRedesignEnabled = true, + availableBalance = resourceReference(R.string.common_balance, wrappedList("2 130,88 USDT")), + ) + val amountWithValueFiatState = amountWithValueState.copy( amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false), ) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index 3fd2bd8ad8..e4f345ca11 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -36,7 +36,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis val fiatAmount = amount.fiatAmount.value.format { fiat( fiatCurrencySymbol = amount.fiatAmount.currencySymbol, - fiatCurrencyCode = amountState.appCurrencyCode, + fiatCurrencyCode = amountState.appCurrency.code, ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index 70d51c2503..35d49a5387 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -31,56 +31,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.rememberDecimalFormat import kotlinx.coroutines.delay -@Deprecated("Use AmountField with clicks") -@Composable -internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) { - val decimalFormat = rememberDecimalFormat() - val isFiatValue = amountField.isFiatValue - val currencyCode = if (isFiatValue) appCurrencyCode else null - val (primaryAmount, primaryValue) = if (isFiatValue) { - amountField.fiatAmount to amountField.fiatValue - } else { - amountField.cryptoAmount to amountField.value - } - val requester = remember { FocusRequester() } - - AmountTextField( - value = primaryValue, - decimals = primaryAmount.decimals, - visualTransformation = AmountVisualTransformation( - decimals = primaryAmount.decimals, - symbol = primaryAmount.currencySymbol, - currencyCode = currencyCode, - decimalFormat = decimalFormat, - ), - onValueChange = amountField.onValueChange, - keyboardOptions = amountField.keyboardOptions, - keyboardActions = amountField.keyboardActions, - textStyle = TangemTheme.typography.h2.copy( - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ), - isAutoResize = true, - isValuePasted = amountField.isValuePasted, - onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss, - modifier = Modifier - .focusRequester(requester) - .padding( - top = TangemTheme.dimens.spacing24, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ) - .requiredHeightIn(min = TangemTheme.dimens.size32), - ) - - LaunchedEffect(key1 = Unit) { - delay(timeMillis = 200) - requester.requestFocus() - } - - AmountSecondary(amountField, appCurrencyCode) -} - @Composable internal fun AmountField( amountField: AmountFieldModel, @@ -97,7 +47,7 @@ internal fun AmountField( amountField.cryptoAmount to amountField.value } val requester = remember { FocusRequester() } - + val symbolColor = if (primaryValue.isBlank()) TangemTheme.colors.text.disabled else TangemTheme.colors.text.primary1 AmountTextField( value = primaryValue, decimals = primaryAmount.decimals, @@ -106,6 +56,7 @@ internal fun AmountField( symbol = primaryAmount.currencySymbol, currencyCode = currencyCode, decimalFormat = decimalFormat, + symbolColor = symbolColor, ), onValueChange = onValueChange, keyboardOptions = amountField.keyboardOptions, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 104f2f60ae..7b4dfe4e57 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -2,20 +2,32 @@ package com.tangem.common.ui.amountScreen.ui import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme private const val AMOUNT_FIELD_KEY = "amountFieldKey" @@ -64,10 +76,142 @@ internal fun LazyListScope.amountField( ) AmountField( amountField = amountState.amountTextField, - appCurrencyCode = amountState.appCurrencyCode, + appCurrencyCode = amountState.appCurrency.code, onValueChange = onValueChange, onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, ) } } +} + +internal fun LazyListScope.amountFieldV2( + amountState: AmountState, + modifier: Modifier = Modifier, + onValueChange: (String) -> Unit, + onValuePastedTriggerDismiss: () -> Unit, + onCurrencyChange: (Boolean) -> Unit, + onMaxAmountClick: () -> Unit, +) { + item(key = AMOUNT_FIELD_KEY) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.action), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .padding(top = 48.dp, bottom = 28.dp), + ) { + if (amountState !is AmountState.Data) { + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(60.dp), + ) + } else { + Text( + text = amountState.title.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + AmountFieldV2( + amountUM = amountState, + onValueChange = onValueChange, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, + onCurrencyChange = onCurrencyChange, + modifier = Modifier, + ) + } + HorizontalDivider( + modifier = Modifier.padding(horizontal = 12.dp), + thickness = 0.5.dp, + color = TangemTheme.colors.stroke.primary, + ) + AmountInfo( + amountUM = amountState, + onMaxAmountClick = onMaxAmountClick, + ) + } + } +} + +@Composable +private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modifier: Modifier = Modifier) { + val tokenIconState = (amountUM as? AmountState.Data)?.tokenIconState ?: CurrencyIconState.Loading + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier, + ) { + CurrencyIcon( + state = tokenIconState, + shouldDisplayNetwork = true, + modifier = Modifier.padding( + start = 16.dp, + top = 16.dp, + bottom = 16.dp, + ), + ) + AmountInfoMain(amountUM = amountUM) + SpacerWMax() + Text( + text = stringResourceSafe(R.string.send_max_amount), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(end = 16.dp) + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.secondary) + .padding(horizontal = 12.dp, vertical = 4.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + onClick = onMaxAmountClick, + ), + ) + } +} + +@Composable +private fun AmountInfoMain(amountUM: AmountState, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = amountUM !is AmountState.Data, + modifier = modifier, + ) { isContent -> + if (isContent) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + TextShimmer( + style = TangemTheme.typography.subtitle2, + modifier = Modifier.width(56.dp), + ) + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(72.dp), + ) + } + } else { + val amountUM = amountUM as AmountState.Data + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = amountUM.tokenName.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + ) + EllipsisText( + text = amountUM.availableBalance.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ellipsis = TextEllipsis.OffsetEnd(amountUM.amountTextField.cryptoAmount.currencySymbol.length), + ) + } + } + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt new file mode 100644 index 0000000000..184f7ed46e --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt @@ -0,0 +1,329 @@ +package com.tangem.common.ui.amountScreen.ui + +import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.BottomCenter +import androidx.compose.ui.Alignment.Companion.TopCenter +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.currency.fiaticon.FiatIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.rememberDecimalFormat +import kotlinx.coroutines.delay + +private const val ROTATED_DEGREE = 180f +private const val INITIAL_DEGREE = 0f + +@Composable +fun AmountFieldV2( + amountUM: AmountState, + onValueChange: (String) -> Unit, + onValuePastedTriggerDismiss: () -> Unit, + onCurrencyChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val decimalFormat = rememberDecimalFormat() + + val requester = remember { FocusRequester() } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + ) { + if (amountUM is AmountState.Empty) { + TextShimmer( + style = TangemTheme.typography.head, + modifier = Modifier.width(150.dp), + ) + } else if (amountUM is AmountState.Data) { + AnimatedContent( + targetState = amountUM.amountTextField.isFiatValue, + transitionSpec = { primaryFieldCurrencyAnimation() }, + label = "Primary field change animation", + ) { isFiatValue -> + val currencyCode = if (isFiatValue) amountUM.appCurrency.code else null + val (primaryAmount, primaryValue) = if (isFiatValue) { + amountUM.amountTextField.fiatAmount to amountUM.amountTextField.fiatValue + } else { + amountUM.amountTextField.cryptoAmount to amountUM.amountTextField.value + } + AmountTextField( + value = primaryValue, + decimals = primaryAmount.decimals, + visualTransformation = AmountVisualTransformation( + decimals = primaryAmount.decimals, + symbol = primaryAmount.currencySymbol, + currencyCode = currencyCode, + decimalFormat = decimalFormat, + symbolColor = TangemTheme.colors.text.disabled, + ), + onValueChange = onValueChange, + keyboardOptions = amountUM.amountTextField.keyboardOptions, + keyboardActions = amountUM.amountTextField.keyboardActions, + textStyle = TangemTheme.typography.head.copy( + textAlign = TextAlign.Center, + ), + isAutoResize = true, + isValuePasted = amountUM.amountTextField.isValuePasted, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, + modifier = Modifier + .focusRequester(requester) + .requiredHeightIn(min = 44.dp), + ) + LaunchedEffect(key1 = Unit) { + delay(timeMillis = 200) + requester.requestFocus() + } + } + } + AmountSecondary( + amountUM = amountUM, + onCurrencyChange = onCurrencyChange, + ) + } +} + +@Composable +private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .animateContentSize() + .padding(top = 8.dp), + ) { + if (amountUM is AmountState.Empty) { + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier + .width(90.dp) + .padding(bottom = 20.dp) + .align(TopCenter), + ) + } else if (amountUM is AmountState.Data) { + AmountFieldCurrencyInfo( + amountUM = amountUM, + onCurrencyChange = onCurrencyChange, + ) + AmountFieldError( + isError = amountUM.amountTextField.isError, + isWarning = amountUM.amountTextField.isWarning, + error = amountUM.amountTextField.error, + modifier = Modifier + .align(BottomCenter) + .padding(top = 20.dp), + ) + } + } +} + +@Composable +private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurrencyChange: (Boolean) -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .align(TopCenter) + .padding(bottom = 20.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onCurrencyChange(!amountUM.amountTextField.isFiatValue) }, + ), + ) { + val iconRotateState by animateFloatAsState( + targetValue = if (amountUM.amountTextField.isFiatValue) ROTATED_DEGREE else INITIAL_DEGREE, + animationSpec = tween( + durationMillis = 400, + easing = FastOutSlowInEasing, + ), + label = "Currency change icon animation", + ) + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_exchange_vertical_24), + ), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + modifier = Modifier + .size(16.dp) + .graphicsLayer { + rotationY = iconRotateState + }, + ) + AmountFieldCurrencyIcon( + amountUM = amountUM, + ) + } +} + +@Composable +private fun AmountFieldCurrencyIcon(amountUM: AmountState.Data) { + AnimatedContent( + targetState = amountUM.amountTextField.isFiatValue, + transitionSpec = { secondaryFieldCurrencyAnimation() }, + label = "Secondary field change animation", + ) { isFiatValue -> + val secondaryAmount = + if (isFiatValue) amountUM.amountTextField.cryptoAmount else amountUM.amountTextField.fiatAmount + val text = secondaryAmount.value.format { + if (isFiatValue) { + crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) + } else { + fiat( + fiatCurrencySymbol = amountUM.appCurrency.symbol, + fiatCurrencyCode = amountUM.appCurrency.code, + ) + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = text, + style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + if (isFiatValue) { + CurrencyIcon( + state = amountUM.tokenIconState, + shouldDisplayNetwork = false, + modifier = Modifier.size(12.dp), + ) + } else { + FiatIcon( + url = amountUM.appCurrency.iconSmallUrl, + size = 12.dp, + isGrayscale = false, + modifier = Modifier.size(12.dp), + ) + } + } + } +} + +@Composable +private fun AmountFieldError( + isError: Boolean, + isWarning: Boolean, + error: TextReference, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = isError || isWarning, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier, + label = "Error field appearance animation", + ) { + val errorText = remember(this, error) { error } + val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention + Text( + text = errorText.resolveReference(), + style = TangemTheme.typography.caption2, + color = color, + textAlign = TextAlign.Center, + ) + } +} + +private fun AnimatedContentTransitionScope.secondaryFieldCurrencyAnimation(): ContentTransform { + return ( + fadeIn( + animationSpec = tween(durationMillis = 300), + ) + scaleIn( + animationSpec = tween(durationMillis = 300), + initialScale = 0.9f, + ) + ).togetherWith( + fadeOut( + animationSpec = tween(durationMillis = 300), + ) + scaleOut( + animationSpec = tween(durationMillis = 300), + targetScale = 0.9f, + ), + ) +} + +private fun AnimatedContentTransitionScope.primaryFieldCurrencyAnimation(): ContentTransform { + return ( + fadeIn( + animationSpec = tween(durationMillis = 300), + ) + scaleIn( + animationSpec = tween(durationMillis = 300), + initialScale = 0.9f, + ) + slideInVertically(initialOffsetY = { it / 2 }) + ) + .togetherWith( + fadeOut( + animationSpec = tween(durationMillis = 300), + ) + scaleOut( + animationSpec = tween(durationMillis = 300), + targetScale = 0.9f, + ) + slideOutVertically(targetOffsetY = { it / 2 }), + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun AmountFieldV2_Preview(@PreviewParameter(AmountFieldV2PreviewProvider::class) params: AmountState) { + TangemThemePreview { + AmountFieldV2( + amountUM = params, + onValueChange = {}, + onValuePastedTriggerDismiss = { }, + onCurrencyChange = {}, + modifier = Modifier.background(TangemTheme.colors.background.action), + ) + } +} + +private class AmountFieldV2PreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + AmountStatePreviewData.emptyState, + AmountStatePreviewData.amountState, + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index 017523fbbc..be5c0a9f2c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -60,7 +60,10 @@ fun AmountTextField( modifier: Modifier = Modifier, color: Color = TangemTheme.colors.text.primary1, backgroundColor: Color = TangemTheme.colors.background.action, - visualTransformation: VisualTransformation = AmountVisualTransformation(decimals), + visualTransformation: VisualTransformation = AmountVisualTransformation( + decimals = decimals, + symbolColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color, + ), keyboardOptions: KeyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index 17521b19b9..c16ed2e9f5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt @@ -1,9 +1,12 @@ package com.tangem.core.ui.components.fields.visualtransformations +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation +import com.tangem.core.ui.extensions.appendColored import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode @@ -19,6 +22,7 @@ class AmountVisualTransformation( private val symbol: String? = null, private val currencyCode: String? = null, private val decimalFormat: DecimalFormat = DecimalFormat(), + private val symbolColor: Color, ) : VisualTransformation { override fun filter(text: AnnotatedString): TransformedText { @@ -28,17 +32,22 @@ class AmountVisualTransformation( ) formattedAmount = formattedAmount.ifEmpty { decimalFormat.defaultFormat() } val formattedText = if (formattedAmount.isNotEmpty() && symbol != null) { - AnnotatedString( + buildAnnotatedString { if (currencyCode != null) { - formatFiatEditableAmount( - fiatAmount = formattedAmount, - fiatCurrencyCode = currencyCode, - fiatCurrencySymbol = symbol, + append( + formatFiatEditableAmount( + fiatAmount = formattedAmount, + fiatCurrencyCode = currencyCode, + fiatCurrencySymbol = symbol, + fiatCurrencySymbolColor = symbolColor, + ), ) } else { - formatWithSymbol(formattedAmount, symbol) - }, - ) + append(formattedAmount) + append(CURRENCY_SPACE) + appendColored(symbol, symbolColor) + } + } } else { AnnotatedString(decimalFormat.defaultFormat()) } @@ -54,9 +63,10 @@ class AmountVisualTransformation( fiatAmount: String?, fiatCurrencyCode: String, fiatCurrencySymbol: String, + fiatCurrencySymbolColor: Color, locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN + ): AnnotatedString { + if (fiatAmount == null) return AnnotatedString(BigDecimalFormatConstants.EMPTY_BALANCE_SIGN) val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply { @@ -64,14 +74,21 @@ class AmountVisualTransformation( } val formatter = requireNotNull(numberFormatter as? DecimalFormat) { Timber.e("NumberFormat is null") - return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN + return AnnotatedString(BigDecimalFormatConstants.EMPTY_BALANCE_SIGN) + } + return buildAnnotatedString { + appendColored( + formatter.positivePrefix.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol), + fiatCurrencySymbolColor, + ) + append(fiatAmount) + appendColored( + formatter.positiveSuffix.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol), + fiatCurrencySymbolColor, + ) } - return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}" - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } - private fun formatWithSymbol(amount: String, symbol: String) = "$amount$CURRENCY_SPACE$symbol" - private class OffsetMappingImpl( private val text: String, private val formattedText: AnnotatedString, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt index 1e15b0b4dd..a3fda95160 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt @@ -82,6 +82,7 @@ fun InputRowEnterAmount( decimals = decimals, symbol = symbol, decimalFormat = rememberDecimalFormat(), + symbolColor = textColor, ), onValueChange = onValueChange, color = textColor, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt index 63903ea854..5878b03d62 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt @@ -82,6 +82,7 @@ fun InputRowEnterInfoAmount( decimals = decimals, symbol = symbol, decimalFormat = rememberDecimalFormat(), + symbolColor = textColor, ), onValueChange = onValueChange, color = textColor, @@ -120,6 +121,7 @@ fun InputRowEnterInfoAmountV2( titleColor: Color = TangemTheme.colors.text.secondary, textColor: Color = TangemTheme.colors.text.primary1, infoColor: Color = TangemTheme.colors.text.tertiary, + symbolColor: Color = TangemTheme.colors.text.primary1, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, showDivider: Boolean = false, @@ -154,6 +156,7 @@ fun InputRowEnterInfoAmountV2( decimals = decimals, symbol = symbol, decimalFormat = rememberDecimalFormat(), + symbolColor = symbolColor, ), onValueChange = onValueChange, color = textColor, diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt index fc74ccf197..ef94c6b681 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt @@ -82,7 +82,7 @@ fun Modifier.selectedBorder( color = color, shape = RoundedCornerShape(radius), ) - .padding(2.5.dp) + .padding(width) .border( width = 1.dp, color = TangemTheme.colors.text.accent, @@ -91,7 +91,7 @@ fun Modifier.selectedBorder( .clip(RoundedCornerShape(radius - 2.dp)) }, otherModifier = { - padding(2.dp) + padding(width) .clip(RoundedCornerShape(radius - 2.dp)) }, ) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt index 608587c822..07d5b17e3c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -51,6 +51,11 @@ internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier private fun OnrampAmountField(amountField: AmountFieldModel) { val decimalFormat = rememberDecimalFormat() val requester = remember { FocusRequester() } + val symbolColor = if (amountField.fiatValue.isBlank()) { + TangemTheme.colors.text.disabled + } else { + TangemTheme.colors.text.primary1 + } AmountTextField( value = amountField.fiatValue, decimals = amountField.fiatAmount.decimals, @@ -59,6 +64,7 @@ private fun OnrampAmountField(amountField: AmountFieldModel) { symbol = amountField.fiatAmount.currencySymbol, currencyCode = amountField.fiatAmount.currencySymbol, decimalFormat = decimalFormat, + symbolColor = symbolColor, ), onValueChange = amountField.onValueChange, keyboardOptions = amountField.keyboardOptions, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 691bd074ca..d60c1b6b81 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase +import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.impl.R @@ -47,6 +48,7 @@ internal class SendAmountModel @Inject constructor( private val feeReloadTrigger: SendFeeReloadTrigger, private val sendAmountUpdateQRListener: SendAmountUpdateQRListener, private val analyticsEventHandler: AnalyticsEventHandler, + private val sendFeatureToggles: SendFeatureToggles, ) : Model(), AmountScreenClickIntents { private val params: SendAmountComponentParams = paramsContainer.require() @@ -95,6 +97,7 @@ internal class SendAmountModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, maxEnterAmount = maxAmountBoundary, iconStateConverter = CryptoCurrencyToIconStateConverter(), + isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, ).convert( AmountParameters( title = stringReference(userWallet.name), From 22fb31ee53b74e4f27900d48a86484e64526cca3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Jun 2025 13:34:03 +0000 Subject: [PATCH 35/99] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 8cd2d641c9..14daca0245 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.25.0-1083" +tangemBlockchainSdk = "develop-1081" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.25.0-483" +tangemCardSdk = "develop-482" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 2f0b9c7a5c999bf7b2c83b7040dfa6839325b4a0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Jun 2025 18:49:50 +0300 Subject: [PATCH 36/99] Updated on 2026-08-14 --- .../walletmanager/utils/SdkRequirementsConditionConverter.kt | 1 + plugins/configuration/build.gradle.kts | 4 ++-- .../configuration/configurations/KotlinConfigurations.kt | 2 +- .../configurations/extension/BaseExtensionConfigurations.kt | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt index 92b365f3f3..1e93352c42 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt @@ -8,6 +8,7 @@ internal class SdkRequirementsConditionConverter : Converter AssetRequirementsCondition.PaidTransaction + is SdkRequirementsCondition.RequiredTrustline -> TODO("added in [REDACTED_TASK_KEY]") is SdkRequirementsCondition.PaidTransactionWithFee -> AssetRequirementsCondition.PaidTransactionWithFee( feeAmount = requireNotNull(value.feeAmount.value), feeCurrencySymbol = value.feeAmount.currencySymbol, diff --git a/plugins/configuration/build.gradle.kts b/plugins/configuration/build.gradle.kts index a8bc7da8a0..989cdb50ec 100644 --- a/plugins/configuration/build.gradle.kts +++ b/plugins/configuration/build.gradle.kts @@ -9,8 +9,8 @@ repositories { } configure { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } dependencies { diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt index 1ff0e4258f..1a78d7190f 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt @@ -12,7 +12,7 @@ internal fun Project.configureKotlinCompilerOptions() { } project.tasks.withType { kotlinOptions { - jvmTarget = "17" + jvmTarget = "21" allWarningsAsErrors = false // this is required to produce a unique META-INF/*.kotlin_module files moduleName = project.path.removePrefix(":").replace(':', '-') diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index c19607fae8..a3b2e0ddab 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -15,8 +15,8 @@ internal fun BaseExtension.configureCompileSdk() { internal fun BaseExtension.configureCompilerOptions() { compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } } From be42c19641e023d1b56180d0f4463c1d23b913e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Jun 2025 18:51:19 +0300 Subject: [PATCH 37/99] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 33 +++++++++-- core/res/src/main/res/values-es/strings.xml | 56 ++++++++++++++++++ core/res/src/main/res/values-fr/strings.xml | 42 +++++++++++++ core/res/src/main/res/values-ja/strings.xml | 25 ++++++-- core/res/src/main/res/values-ru/strings.xml | 24 ++++++-- .../src/main/res/values-uk-rUA/strings.xml | 59 +++++++++++++++++++ core/res/src/main/res/values/strings.xml | 20 ++++--- 7 files changed, 234 insertions(+), 25 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 7dccd4ed94..24c3cc5827 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -289,6 +289,7 @@ Kann nicht gegen %s ausgetauscht werden Bereitgestellt von Status + Anbieter auswählen Anbieter erleichtern Transaktionen Anbieter Es ist ein Fehler aufgetreten. Code: %s @@ -349,6 +350,8 @@ Diese Transaktion ausblenden Sobald der Transaktionsstatus ausgeblendet ist, kann er nicht mehr angezeigt werden. Stattdessen kannst Du ihn durch einfaches Wischen schließen. Diese Transaktion ausblenden + Dieses Token wird nicht unterstützt. Bitte wähle zum Tauschen ein anderes Token. + %s wird nicht unterstützt Tauschen mit Keine Token gefunden. Bitte versuche eine andere Anfrage ID: %s @@ -377,6 +380,8 @@ Um fortzufahren, erteile %1s Smart Contracts die Berechtigung, dein zu %2s verwenden. Erlaubnis erteilen Unbegrenzt + Vorhandene Wallet hinzufügen + Neues Wallet erstellen Karte oder Ring bestellen Karte oder Ring scannen An %s @@ -568,6 +573,8 @@ Du hast dieses Netzwerk noch nicht hinzugefügt. Um NFTs zu erhalten, füge es Deinem Portfolio hinzu. Netzwerk nicht hinzugefügt Nicht hinzugefügt + Nicht unterstützte NFT-Arten + cNFTs und pNFTs werden noch nicht unterstützt NFT senden Eigenschaften Unbetitelte Sammlung @@ -823,6 +830,9 @@ Gebühr bis zu Ungültiges Memo Abdeckung der Netzgebühren + Nonce + Eindeutige Nummer für jede Transaktion. Verwende diese Nummer, um eine ausstehende Transaktion erneut zu senden oder abzubrechen. + Nonce eingeben… Unzureichende Mittel für die Überweisung, da die Summe aus Gebühr und Überweisungsbetrag das bestehende Guthaben übersteigt Gesamtbetrag übersteigt den Saldo Ein Guthaben von mindestens %s ist erforderlich, um dein Konto in der Blockchain aktiv zu halten und Sicherheitsrisiken zu vermeiden. Dieser Betrag verbleibt auf deinem Guthaben und kann nicht abgehoben werden. @@ -850,8 +860,12 @@ Empfänger Keine gültige Adresse Vergewisser dich, dass die empfangende Wallet-Adresse im %s Netzwerk ist, damit du deine Token nicht verlierst. + senden an %s + Stelle sicher, dass Du %s Netzwerkadresse hast, da Fehler zu verlorenen Übertragungen führen können Senden an Ein Memo/Destination Tag ist eine eindeutige ID zur Unterscheidung von Transaktionen, die an denselben Empfänger im selben Netzwerk gesendet werden. Vorsicht! Das Weglassen eines Vermerks kann dazu führen, dass Gelder für immer verloren sind! + Memo / Destination Tag ist ein Code, der Transaktionen an einen gemeinsamen Empfänger im Kryptonetzwerk voneinander trennt. + Achtung: Das Fehlen eines Memos kann zum Verlust von Geldern führen. Meine Wallet Eine Möglichkeit, Bitcoin-Transaktionsgebühren zu messen. Du gibst die Anzahl der kleinsten Bitcoin-Einheiten (Satoshi) für jedes virtuelle Byte in einer Transaktion an. Je höher die Zahl, desto schneller wird die Transaktion von den Minern verarbeitet. Satoshi / vByte @@ -872,11 +886,14 @@ Ungültiger Betrag Die Gebühr geht über die Bilanz hinaus Der Gesamtbetrag geht über die Bilanz hinaus + Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht – nahtlos. + Senden mit Swap Transaktion gesendet Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. Entferne diese Wallet Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden. Name + Lass deinen Token für dich arbeiten Die Anzahl der zu stakenden Krypros muss mindesten %s betragen Der Stakingbetrag wird aufgrund der Netzwerkregeln auf %1$s TRX aufgerundet. Der Betrag der unstaked wird, wird aufgrund der Netzwerkregeln auf %1$s TRX gerundet. @@ -1011,6 +1028,7 @@ Web 3.0-kompatibel Zum Fortfahren ist eine eingehende Transaktion von mindestens %1$s erforderlich Unzureichende Mittel + Fester Zinssatz Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. Neuer Swap-Anbieter verfügbar! @@ -1260,7 +1278,7 @@ Deine Bewertung motiviert uns, die Tangem Wallet noch besser zu machen. Gefällt dir Tangem? Du musst deinen Token zuordnen, bevor du Token erhalten kannst - Du musst die trustline öffnen, bevor Du Token erhältst + Du musst die Trustline für Deinen Token öffnen, bevor Du diesen erhalten kannst. Netzmietgebühr erforderlich Handlungsbedarf Hast Du innerhalb von 7 Tagen nach der Erstellung Ihres Wallets über die App Kontakt zum Support aufgenommen? Wenn ja oder wenn Du Dir nicht sicher bist, klicke auf „Ja“ und folge den Anweisungen. @@ -1276,13 +1294,13 @@ Wischen Sie nach unten, um zu aktualisieren, oder versuchen Sie es später erneut. Einige Netzwerke sind nicht erreichbar Einige Token-Guthaben konnten nicht aktualisiert werden - Erforderliche Mindest-XLM-Reserve %1$s - Trustline öffnen - Trustline-Untermenü öffnen - Trustline-Titel öffnen Dies ist eine Testnet-Karte. Sie kann keine Transaktionen verarbeiten und sollte nur zu Test- und Entwicklungszwecken verwendet werden. Nur für Testzwecke Der Kontostand ist möglicherweise veraltet. Aktualisiere bittre die Seite. + Nicht genug %1$s. Lade Dein XLM-Konto auf, um dieses Token zu verknüpfen + Trustline aktivieren + Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s. + Trustline erforderlich Unbekannte Domäne Trotzdem verbinden Zeitüberschreitungsfehler. Bitte versuche es später erneut. @@ -1290,7 +1308,9 @@ Diese Domäne kann nicht verifiziert werden. Überprüfe die Anfrage sorgfältig und bestätige diese dann. Bitte kehre zu Deinem Browser zurück und stellen die Verbindung über WalletConnect erneut her. WalletConnect-Sitzung wurde getrennt + Trotzdem unterschreiben Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. + Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. Fehlercode: 8 005. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. @@ -1303,8 +1323,10 @@ Erlaubnis auszugeben Vertragsadresse Verbinden + Laden Netzwerk Netzwerke + Unbegrenzt Wallet Verbundene Netzwerke Anzeige des Kontostands und der Aktivitäten in Deiner Wallet @@ -1321,6 +1343,7 @@ Text über die Trennung aller dApps Alle dApps trennen Geschätzte Wallet-Änderungen + Die Transaktion konnte nicht simuliert werden. Bitte sei vorsichtig. Böswillige/ gefährliche Transaktion Füge Deinem Profil für dieses Wallet das Netzwerk %s hinzu Die Wallet verfügt über keine erforderlichen Netzwerke diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 0421d01d9e..bc9fd8a0e8 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -162,6 +162,7 @@ Tarifa de la red La cantidad enviada se reducirá en %1$s (%2$s) para cubrir el nivel de tarifa seleccionado Siguiente + NFT No Ninguna dirección Ahora @@ -184,6 +185,7 @@ Buscar Buscar tokens sec + Ver todo Seed phrase Seleccione una acción Vender @@ -514,6 +516,56 @@ Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado Agregar tokens NFC no está disponible en su dispositivo + Acerca de NFT + Activo NFT + + %d elemento + %d elementos + + Los NFTs enviados a la dirección de su billetera aparecerán aquí. + No hay colecciones todavía + Recibir NFT + Colecciones de NFT + Es posible que algunos datos no se carguen + Problemas temporales de carga + Información de base + Cadena + Dirección del contrato + Cadena es la blockchain en la que está el NFT. + La dirección del contrato es un identificador único para el contrato inteligente que gobierna los tokens en la blockchain + Una etiqueta que describe lo raro que es el NFT. Cuanto menor sea el valor, más único será el NFT. + La posición de un NFT en el ranking de rareza entre otros tokens. Cuanto más alto sea el ranking, más raro y valioso será el NFT. + La dirección del contrato es un identificador único para el contrato inteligente que gobierna los tokens en la blockchain + El ID del token es un identificador único asignado a cada token, que lo distingue de otros en la colección. + El estándar de token define qué tipo de token es y cómo funciona con diferentes billeteras y plataformas + Último precio de venta + Etiqueta de rareza + Ranking de rareza + Dirección del token + Token ID + Estándar de token + Rasgos + Sin resultados. Por favor, intente otra solicitud. + Sin colección + Disponible + Elegir red + Recibir NFT + Todavía no ha agregado esta red. Para recibir NFTs, añádala a su portafolio. + Red no agregada + No aregada + Tipos de NFT no compatibles + cNFT y pNFT aún no son compatibles. Por favor, no los envíes a tu billetera. + Enviar NFT + Rasgos + Colección sin título + %1$d NFT en %2$d colecciones + + + %1$d NFTs en %2$d colecciones + + Pulse aquí para recibir el primer NFT + Colecciones de NFT + No se pueden cargar los datos Para utilizar la red %1$s, debe pagar la reserva de cuenta (%2$s%3$s), que bloquea y oculta ese monto indefinidamente La cuenta de destino no está activa. Envíe %s o más para activar la cuenta. Para crear una cuenta, envíe fondos a esta dirección @@ -652,6 +704,7 @@ Acceso a la cámara denegado No se requiere nota %1$s (%2$s) en la red %3$s + %1$s en la red %2$s El envío de cualquier otra moneda supondrá una pérdida irreversible. Envíe solo %s a esta dirección. Enviar cualquier otra moneda resultará en su pérdida irreversible. Envíe solo %1$s en la red %2$s @@ -779,6 +832,8 @@ Asegúrese de que la dirección de la billetera receptora esté en la red %s para evitar perder sus tokens Destinatario Un Memo/ID de destino es un ID único para diferenciar transacciones enviadas al mismo destinatario en la misma red. Precaución: Omitir un memo puede llevar a la pérdida de fondos + Memo / Destination Tag es un código que separa las transacciones dirigidas a un destinatario compartido en una red de criptomonedas. + Atención: si falta el Memo, можно потерять средства. Mis billeteras Una forma de medir las tarifas de transacción de Bitcoin. Indique el número de la unidad más pequeña de Bitcoin (Satoshi) por cada byte virtual en una transacción. Cuanto más alto sea el número, más rápido procesarán los mineros la transacción. Satoshi / vByte @@ -804,6 +859,7 @@ Olvidar la billetera Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo. Nombre + Haz que tu token trabaje por ti El monto del staking debe ser al menos %s El monto del staking se redondeará a %1$s TRX debido a las reglas de la red. El monto de cancelación del staking se redondeará a %1$s TRX debido a las reglas de la red. diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 6e8c8d6142..5bd4e39551 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -162,6 +162,7 @@ Commissions du réseau Le montant envoyé sera réduit de %1$s(%2$s) pour couvrir le niveau de frais sélectionné Suivant + NFT Non Aucune adresse Maintenant @@ -184,6 +185,7 @@ Rechercher Rechercher des jetons sec + Voir tout Seed phrase Sélectionnez une action Vendre @@ -515,12 +517,48 @@ Tirez vers le haut ou appuyez sur la barre de recherche pour ajouter des jetons directement depuis le marché Ajouter des jetons NFC n\'est pas disponible sur votre appareil + À propos des NFT + Actif NFT + + %d article + %d articles + Les NFT envoyés à l\'adresse de votre portefeuille s\'afficheront ici. Aucune collection pour le moment Recevoir des NFT Collections NFT Certaines données peuvent ne pas se charger Problèmes de chargement temporaires + Informations de base + Chaîne + Adresse du contrat + Chain est la blockchain où existe le NFT. + L\'adresse du contrat est un identifiant unique pour le contrat intelligent qui régit les jetons sur la blockchain + Une étiquette décrivant la rareté du NFT. Plus la valeur est faible, plus le NFT est unique. + Position d\'un NFT dans le classement de rareté des autres jetons. Plus le rang est élevé, plus le NFT est rare et précieux. + L\'adresse du jeton est un identifiant unique pour le jeton sur la blockchain, permettant le suivi des transactions et de la propriété + L\'ID de jeton est un identifiant unique attribué à chaque jeton, le distinguant des autres dans la collection + La norme de jeton définit de quel type de jeton il s\'agit et comment il fonctionne avec différents portefeuilles et plateformes + Dernier prix de vente + Étiquette de rareté + Rang de rareté + Adresse du jeton + ID du jeton + Norme de jeton + Caractéristiques + Aucun résultat. Veuillez réessayer. + Aucune collection + Disponible + Choisir le réseau + Recevoir des NFT + Vous n\'avez pas encore ajouté ce réseau. Pour recevoir des NFT, ajoutez-le à votre portefeuille. + Réseau non ajouté + Non ajouté + Types de NFT non pris en charge + Les cNFT et les pNFT ne sont pas encore pris en charge + Envoyer un NFT + Caractéristiques + Collection sans titre %1$d NFT dans la collection %2$d @@ -667,6 +705,7 @@ Accès à la caméra refusé Aucun mémo requis %1$s (%2$s) sur le réseau %3$s + %1$s sur le réseau %2$s L’envoi de toute autre devise entraînera sa perte irréversible. Envoyez uniquement %s à cette adresse. L\'envoi de toute autre devise entraînera sa perte irréversible. Envoyez uniquement %1$s sur le réseau %2$s @@ -794,6 +833,8 @@ Assurez-vous que l\'adresse du portefeuille de réception est sur le réseau %s pour éviter de perdre vos jetons Envoyer à Un Memo/Destination Tag est un identifiant unique permettant de différencier les transactions envoyées au même destinataire sur le même réseau. Attention : L\'omission d\'un mémo peut entraîner des fonds mal placés + Le Memo / Destination Tag est un code qui distingue les transactions envoyées à un destinataire partagé sur un réseau crypto. + Attention : l\'absence de Memo peut entraîner une perte de fonds. Mes portefeuilles Un moyen de mesurer les frais de transaction Bitcoin. Il indique le nombre de la plus petite unité de Bitcoin (Satoshi) pour chaque octet virtuel dans une transaction. Plus le nombre est élevé, plus la transaction sera traitée rapidement par les mineurs. Satoshi / vByte @@ -819,6 +860,7 @@ Oublier le portefeuille Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. Nom + Faites travailler votre jeton pour vous Le montant à staker doit être au moins %s Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau. Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau. diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index f3c8df4619..722f628412 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -285,6 +285,7 @@ %sとの交換はできません 提供元 ステータス + プロバイダーを選択 プロバイダーが取引を促進します プロバイダー エラーが発生しました。コード: %s @@ -375,6 +376,7 @@ 続行するには、%1sスマートコントラクトに%2sを使用する権限を付与してください 許可を与える 無制限 + 既存のウォレットを追加 Tangemを注文 Tangemをスキャン %sへ @@ -562,6 +564,8 @@ このネットワークはまだ追加されていません。NFTを受け取るには、ポートフォリオに追加してください。 ネットワークが追加されていません 未追加 + サポートされていないNFTタイプ + cNFTとpNFTはまだサポートされていません NFTを送信する 特徴 無題のコレクション @@ -812,6 +816,9 @@ 最大手数料 無効なメモ ネットワーク手数料のカバー + ナンス + 各取引に固有の番号です。保留中の取引を再送信またはキャンセルするために使用します。 + ナンスを入力してください... 手数料と送金額の合計が残高を超えているため、送金に必要な資金が不足しています。 合計が残高を超えています セキュリティリスクを防ぐため、ブロックチェーン上にアカウントを維持するには、少なくとも%s の残高が必要です。この金額は残高に残り、引き出すことはできません。 @@ -839,8 +846,12 @@ 受取人 有効なアドレスではありません トークンを失わないように、受信ウォレットアドレスが%sネットワーク上にあることを確認してください。 + %s に送信 + %sネットワークアドレスを確認してください。エラーが発生すると転送が失われる可能性があります。 送金先 メモ/宛先タグは、同じネットワーク上の同じ受信者に送信された取引を区別するためのユニークなIDです。注意:メモを省略すると、資金の行き違いにつながる可能性があります。 + メモ / 宛先タグは、暗号資産ネットワーク内の共有受信者への取引を分離するコードです。 + 注意:メモを忘れると資金が失われる可能性があります。 マイウォレット Bitcoinの取引手数料を測定する方法。取引における各仮想バイトの最小Bitcoin単位(Satoshi)の数を示します。数値が高いほど、マイナーによる取引の処理速度が速くなります。 Satoshi / vByte @@ -868,7 +879,7 @@ ウォレット削除 これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。 名前 - このトークンのステーキングに参加 + トークンを活用しよう ステーキング金額は %s 以上である必要があります ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。 ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。 @@ -1247,7 +1258,7 @@ あなたのレビューは、Tangemウォレットをさらに良くするためのモチベーションになります Tangemを楽しんでいますか? トークンを受け取る前に、トークンを関連付ける必要があります。 - トークンを受け取る前に、トラストラインを開く必要があります + トークンを受け取る前に、トラストラインを開く必要があります ネットワーク使用料が必要です 対応が必要です ウォレットの作成後、7日以内にアプリ経由でサポートに連絡しましたか?連絡した場合、あるいは連絡したかわからない場合は、「はい」をクリックして指示に従ってください。 @@ -1263,13 +1274,13 @@ 下にスワイプして更新するか、後でもう一度お試しください。 一部のネットワークにアクセスできません 一部のトークン残高を更新できませんでした - %1$sが足りません。このトークンを関連付けるには、XLMアカウントにチャージしてください。 - トラストラインを有効にする - ネットワークルールに従い、このトークンを入金する前にトラストラインを開設する必要があります。これにより、ウォレットが資産を受け入れ、保管できるようになります。 - このトークンのトラストラインを有効にする これはテストネットカードです。取引処理はできませんので、テストおよび開発目的でのみご利用ください。 テスト目的のみ 残高が古い可能性があります。ページを更新してください。 + %1$sが足りません。このトークンを関連付けるには、XLMアカウントにチャージしてください。 + トラストラインを有効にする + このトークンを受け取るには、トラストラインを有効にする必要があります。ネットワークには%1$s %2$s予備金が必要です。 + トラストラインが必要 不明なドメイン とにかく接続する タイムアウトエラーが発生しました。しばらくしてからもう一度お試しください。 @@ -1277,7 +1288,9 @@ このドメインは検証できません。承認前にリクエスト内容をよく確認してください。 ブラウザに戻り、WalletConnect経由で再接続してください。 WalletConnectセッションが接続解除されました + とにかくサインする エラーコード: %s 。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 + 問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 不明なエラーが発生しました エラーコード: 8 005。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 不明なエラーが発生しました diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 132232d93f..6be2e1fb65 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -370,6 +370,8 @@ Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s Дать разрешение Безлимитно + Добавить существующий кошелек + Создать новый кошелек Купить Сканировать в %s @@ -568,14 +570,17 @@ Вы ещё не добавили эту сеть. Чтобы получать NFT, добавьте её в ваш портфель. Сеть не добавлена Не добавлено + Неподдерживаемый тип NFT + cNFT и pNFT не поддерживаются. Пожалуйста, не отправляйте их на свой кошелёк. Отправить NFT Черты + Безымянная коллекция %1$d NFTs в %2$d коллекциях - - + %1$d NFT в %2$d коллекции + %1$d NFT в %2$d коллекциях - %1$d NFTs в %2$d коллекциях + %1$d NFT в %2$d коллекциях Нажмите здесь, чтобы получить первую NFT NFT коллекции @@ -724,6 +729,7 @@ Доступ к камере запрещен Memo не требуется %1$s (%2$s) в сети %3$s + %1$s в %2$s сети Использование другой сети может привести к утрате средств. Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. Отправляйте только %1$s в сети %2$s @@ -826,6 +832,9 @@ Комиссия не превысит Недопустимый Memo Покрытие сетевой комиссии + Nonce + Уникальный номер транзакции. Измените, чтобы ускорить отправку или отменить зависшую операцию. + Введите nonce… Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств Для сохранения вашего аккаунта в блокчейне и защиты от возможных рисков необходим баланс не менее %s. Эта сумма останется на вашем счете и не может быть снята. @@ -855,6 +864,8 @@ Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов. Отправить Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. + Memo / Destination Tag — это код, разделяющий транзакции, отправляемые одному и тому же получателю в криптовалютной сети. + Внимание: отсутствие Memo может привести к потере средств. Мои кошельки Способ измерения комиссии за биткоин-транзакцию. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый виртуальный байт в транзакции. Чем выше число, тем быстрее будет обработана транзакция майнерами. Сатоши / вбайт @@ -879,6 +890,7 @@ Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя + Заставьте ваш токен работать Сумма для стейкинга должна быть не менее %s Согласно правилам сети, сумма стейкинга будет округлена до %1$sTRX. Сумма для вывода из стейкинга будет округлена до %1$s TRX ввиду особенностей сети. @@ -1247,12 +1259,12 @@ Свайпните вниз для обновления или попробуйте позже. Некоторые сети недоступны Некоторые балансы токенов не удалось обновить - Открыть Trustline - Согласно правилам сети, чтобы пополнить этот токен, необходимо сначала открыть Trustline — это позволит вашему кошельку принимать и хранить этот актив. - Откройте Trustline Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. Только для целей тестирования Возможно, баланс устарел. Обновите страницу. + Открыть Trustline + Согласно правилам сети, чтобы пополнить этот токен, необходимо сначала открыть Trustline — это позволит вашему кошельку принимать и хранить этот актив. + Откройте Trustline Пожалуйста, вернитесь в браузер и выполните повторное подключение через WalletConnect. Выбрана не верная карта или кольцо Адрес diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index a6ec71a7c8..314b79ecef 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -168,6 +168,7 @@ Комісія мережі Сума відправлення буде зменшена на %1$s (%2$s) для покриття обраного рівня комісії Далі + NFT Ні Немає адреси Зараз @@ -190,6 +191,7 @@ Пошук Пошук токенів сек + Переглянути все Seed-фраза Оберіть дію Продати @@ -527,6 +529,59 @@ Потягніть вгору або торкніться панелі пошуку, щоб додати токени безпосередньо з маркету Додати токени Функція NFC недоступна на вашому пристрої + Про NFT + NFT + + %d NFT + %d NFTs + %d NFTs + %d NFTs + + Тут відображатимуться NFT, отримані на ваш гаманець + У вас ще немає колекцій + Отримати NFT + NFT колекції + Частина даних може не відобразитися + Не вдалося завантажити дані + Базова інформація + Мережа + Адреса контракту + Мережа — це блокчейн, на якому існує NFT. + Адреса контракту – це унікальний код, який ідентифікує смарт-контракт, керуючий токенами в блокчейні + Мітка, яка описує, наскільки рідкісним є NFT. Чим нижче значення, тим унікальніше + Позиція NFT у рейтингу рідкісності серед інших токенів. Чим вище ранг, тим рідкісніше і цінніше NFT. + Адреса токена — це унікальний ідентифікатор токена в блокчейні, що дозволяє відстежувати транзакції та володіння. + ID токена – це унікальний ідентифікатор, який присвоюється кожному токену, що відрізняє його від інших у колекції + Стандарт токена визначає, який це тип токена і як він працює з різними гаманцями та платформами + Остання ціна продажу + Мітка унікальності + Ранг рідкості + Адреса токену + ID токену + Стандарт токена + Риси + Немає результатів. Будь ласка, спробуйте інший запит. + Без колекції + Доступно + Оберіть мережу + Отримати NFT + Ви ще не додали цю мережу. Щоб отримувати NFT, додайте її до свого портфоліо. + Мережу не додано + Не додано + Непідтримувані типи NFT + cNFT і pNFT наразі не підтримуються. Будь ласка, не надсилайте їх на свій гаманець. + Надіслати NFT + Риси + NFTs в %2$d колекціях%1$d + + + + + %1$d NFTs в %2$d колекціях + + Натисніть тут, щоб отримати першу NFT + NFT колекції + Неможливо завантажити дані Щоб користуватися мережею %1$s, ви маєте оплатити резерв акаунта (%2$s %3$s), який блокується і не використовується у вашому балансі Обліковий запис одержувача не активовано. Надішліть %s або більше, щоб активувати обліковий запис. Для створення акаунту надішліть кошти на цю адресу @@ -671,6 +726,7 @@ Доступ до камери заборонено Memo не вимагається %1$s (%2$s) у мережі %3$s + %1$s у мережі %2$s Використання іншої мережі може призвести до втрати коштів. Надсилайте лише %s на цю адресу. Надсилання будь-якої іншої валюти призведе до її незворотної втрати. Надсилайте лише %1$s у мережі %2$s @@ -802,6 +858,8 @@ Переконайтеся, що адреса гаманця одержувача знаходиться в мережі %s, щоб уникнути втрати ваших токенів Надіслати до Memo/ Тег призначення — це унікальний ідентифікатор для розрізнення транзакцій, надісланих тому самому одержувачу в тій самій мережі. Застереження: відсутність тегу може призвести до втрати коштів. + Мемо / Destination Tag — це код, що розділяє транзакції до спільного одержувача в криптовалютній мережі. + Увага: відсутність Memo може призвести до втрати коштів. Мої гаманці Спосіб виміру комісії за біткоїн-транзакцію. Він свідчить про кількість найменшої одиниці біткоїна (сатоші) за кожен віртуальний байт у транзакції. Чим вище число, тим швидше буде оброблено транзакцію майнерами. Сатоші / вбайт @@ -827,6 +885,7 @@ Забути гаманець Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову. Ім\'я + Змусьте свій токен працювати Сума для стейкінгу має бути не менше %s Сума стейкінгу буде округлена до %1$s TRX відповідно до правил мережі. Сума зняття зі стейкінгу буде округлена до %1$s TRX через мережеві правила. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4b28892f04..59973b51ac 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -381,10 +381,10 @@ To continue, grant %1s smart contracts permission to use your %2s Give Permission Unlimited + Add Existing Wallet + Create New Wallet Order Tangem Scan Tangem - Create New Wallet - Add Existing Wallet to %s On %s network This information was generated with AI.\nTap here, if you find any errors. @@ -575,7 +575,7 @@ Network not added Not Added Unsupported NFT types - cNFTs and pNFTs are not supported yet. Please don’t send them to your wallet. + cNFTs and pNFTs are not supported yet. Please don\'t send them to your wallet. Send NFT Traits Untitled collection @@ -831,6 +831,9 @@ Fee up to Invalid Memo Network fee coverage + Nonce + Unique number for each transaction. Use it to resend or cancel a pending transaction. + Enter nonce… Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance A balance of at least %s is required to keep your account on the blockchain to prevent security risks. This amount will remain in your balance and cannot be withdrawn. @@ -1320,7 +1323,7 @@ Your review keeps us motivated to make Tangem Wallet even better Enjoying Tangem? You must associate your token before receiving tokens - You must open trustline for your token before receiving it + You must open trustline for your token before receiving it Network rent fee required Action required Did you contact support via the app within 7 days of creating your wallet? If yes, or if you\'re unsure, click \'Yes\' and follow the instructions. @@ -1336,13 +1339,13 @@ Swipe down to refresh or try again later. Some networks are unreachable Some token balances could not be updated - Not enough %1$s. Top up your XLM account to associate this token - Enable Trustline - According to network rules, you need to open a Trustline before depositing this token. It allows your wallet to accept and hold the asset. - Enable Trustline for this token This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. For testing purposes only Balance may be outdated. Refresh the page. + Not enough %1$s. Top up your XLM account to associate this token + Enable Trustline + A Trustline must be enabled to receive this token. The network requires a %1$s %2$s reserve. + Trustline Required Unknown domain Connect anyway Timeout error. Please, try again later. @@ -1352,6 +1355,7 @@ WalletConnect session was disconnected Sign anyway Error code: %s. If the problem persists — feel free to contact our support. + If the problem persists — feel free to contact our support. We\'ve encountered unknown error Error code: 8 005. If the problem persists — feel free to contact our support. We\'ve encountered unknown error From 6f67cee7a9bf45ae80d15e5b95e17a93c8e00594 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Jun 2025 18:57:45 +0300 Subject: [PATCH 38/99] Updated on 2026-08-14 --- plugins/configuration/build.gradle.kts | 4 ++-- .../configuration/configurations/KotlinConfigurations.kt | 2 +- .../configurations/extension/BaseExtensionConfigurations.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/configuration/build.gradle.kts b/plugins/configuration/build.gradle.kts index 989cdb50ec..a8bc7da8a0 100644 --- a/plugins/configuration/build.gradle.kts +++ b/plugins/configuration/build.gradle.kts @@ -9,8 +9,8 @@ repositories { } configure { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } dependencies { diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt index 1a78d7190f..1ff0e4258f 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt @@ -12,7 +12,7 @@ internal fun Project.configureKotlinCompilerOptions() { } project.tasks.withType { kotlinOptions { - jvmTarget = "21" + jvmTarget = "17" allWarningsAsErrors = false // this is required to produce a unique META-INF/*.kotlin_module files moduleName = project.path.removePrefix(":").replace(':', '-') diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index a3b2e0ddab..c19607fae8 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -15,8 +15,8 @@ internal fun BaseExtension.configureCompileSdk() { internal fun BaseExtension.configureCompilerOptions() { compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } From b80083992e0018eb5084a42c9c6b042c4bc1d473 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Jun 2025 19:53:13 +0300 Subject: [PATCH 39/99] Updated on 2026-08-14 --- plugins/configuration/build.gradle.kts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/configuration/build.gradle.kts b/plugins/configuration/build.gradle.kts index a8bc7da8a0..d72acfbdcd 100644 --- a/plugins/configuration/build.gradle.kts +++ b/plugins/configuration/build.gradle.kts @@ -13,6 +13,16 @@ configure { targetCompatibility = JavaVersion.VERSION_17 } +kotlin { + jvmToolchain(17) +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + dependencies { implementation(deps.gradle.kotlin) implementation(deps.gradle.android) From 3b8e33bbb9c832e43e5726ee828b75f97cdb1d7f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Jun 2025 20:25:16 +0300 Subject: [PATCH 40/99] Updated on 2026-08-14 --- plugins/configuration/build.gradle.kts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/plugins/configuration/build.gradle.kts b/plugins/configuration/build.gradle.kts index d72acfbdcd..a8bc7da8a0 100644 --- a/plugins/configuration/build.gradle.kts +++ b/plugins/configuration/build.gradle.kts @@ -13,16 +13,6 @@ configure { targetCompatibility = JavaVersion.VERSION_17 } -kotlin { - jvmToolchain(17) -} - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } -} - dependencies { implementation(deps.gradle.kotlin) implementation(deps.gradle.android) From bd859548f832508b4a1d7991487ecb5692eb1e88 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Jun 2025 15:08:55 +0500 Subject: [PATCH 41/99] Updated on 2026-08-14 --- .../com/tangem/domain/managetokens/CreateCoinCurrencyUseCase.kt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCoinCurrencyUseCase.kt diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCoinCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCoinCurrencyUseCase.kt new file mode 100644 index 0000000000..e69de29bb2 From 12e53a3aa3947814ab23ed8f021e3c4b5b5a8c8e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 23 Jun 2025 11:56:00 +0300 Subject: [PATCH 42/99] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 9 +++- .../tap/di/RootAppComponentContextModule.kt | 4 +- ci_resources/Dockerfile.android | 54 ------------------- ci_resources/Dockerfile.lightweight | 26 --------- ....properties => tests_ci_gradle.properties} | 2 +- .../version/DefaultVersionProvider.kt | 4 +- core/decompose/build.gradle.kts | 8 ++- .../decompose/context/AppComponentContext.kt | 4 +- .../AppComponentContextChildrenFactory.kt | 4 +- .../context/DefaultAppComponentContext.kt | 2 + .../core/decompose/utils/ActivityHolder.kt | 16 ++++++ fastlane/Fastfile | 4 +- features/kyc/api/.gitignore | 1 + features/kyc/api/build.gradle.kts | 22 ++++++++ .../com/tangem/features/kyc/KycComponent.kt | 12 +++++ features/kyc/impl/.gitignore | 1 + features/kyc/impl/build.gradle.kts | 54 +++++++++++++++++++ .../features/kyc/DefaultKycComponent.kt | 39 ++++++++++++++ .../tangem/features/kyc/di/FeatureModule.kt | 16 ++++++ gradle/dependencies.toml | 6 ++- .../plugin/configuration/model/AppConfig.kt | 4 +- settings.gradle.kts | 4 ++ tangem-android-tools | 2 +- 24 files changed, 205 insertions(+), 95 deletions(-) delete mode 100644 ci_resources/Dockerfile.android delete mode 100644 ci_resources/Dockerfile.lightweight rename ci_resources/{tests_8gb_ram_ci_gradle.properties => tests_ci_gradle.properties} (86%) create mode 100644 core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ActivityHolder.kt create mode 100644 features/kyc/api/.gitignore create mode 100644 features/kyc/api/build.gradle.kts create mode 100644 features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt create mode 100644 features/kyc/impl/.gitignore create mode 100644 features/kyc/impl/build.gradle.kts create mode 100644 features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt create mode 100644 features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d592ad2b98..794229daee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -217,6 +217,8 @@ dependencies { implementation(projects.features.usedesk.impl) implementation(projects.features.hotWallet.api) implementation(projects.features.hotWallet.impl) + implementation(projects.features.kyc.api) + implementation(projects.features.kyc.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ac76057c04..a2c1f4e5fe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,7 +45,14 @@ android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" tools:ignore="GoogleAppIndexingWarning" - tools:replace="android:allowBackup, android:fullBackupContent, android:label"> + tools:replace="android:allowBackup, android:fullBackupContent, android:label, android:largeHeap"> + + + /etc/apt/sources.list.d/github-cli.list && \ - apt-get update && apt-get install -y gh && \ - apt-get clean - -RUN mkdir -p $ANDROID_HOME/cmdline-tools/latest && \ - wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O /tmp/cmdline-tools.zip && \ - unzip /tmp/cmdline-tools.zip -d $ANDROID_HOME/cmdline-tools/latest && \ - mv $ANDROID_HOME/cmdline-tools/latest/cmdline-tools/* $ANDROID_HOME/cmdline-tools/latest/ && \ - rm -rf $ANDROID_HOME/cmdline-tools/latest/cmdline-tools && \ - rm -f /tmp/cmdline-tools.zip && \ - yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses && \ - $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-31" "platforms;android-34" "build-tools;34.0.0" - -RUN wget https://github.com/lzhiyong/android-sdk-tools/releases/download/34.0.3/android-sdk-tools-static-aarch64.zip -O /tmp/android-sdk-tools-static-aarch64.zip && \ - unzip /tmp/android-sdk-tools-static-aarch64.zip -d /tmp/android-sdk-tools-static-arm && \ - cp -r /tmp/android-sdk-tools-static-arm/build-tools/* $ANDROID_HOME/build-tools/34.0.0/ && \ - rm -rf /tmp/android-sdk-tools-static-arm /tmp/android-sdk-tools-static-aarch64.zip - -RUN gem install bundler:2.5.23 -RUN gem install fastlane -v 2.225.0 -N -V -RUN gem install fastlane-plugin-firebase_app_distribution -v 0.9.1 -N -V - -COPY ../Gemfile Gemfile.lock ./ -RUN bundle install --jobs=4 --retry=3 --verbose - -RUN bundle exec fastlane -v - -CMD ["bash"] diff --git a/ci_resources/Dockerfile.lightweight b/ci_resources/Dockerfile.lightweight deleted file mode 100644 index c2b4b399b2..0000000000 --- a/ci_resources/Dockerfile.lightweight +++ /dev/null @@ -1,26 +0,0 @@ -FROM ubuntu:24.04 - -ENV LC_ALL=en_US.UTF-8 -ENV LANG=en_US.UTF-8 -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y \ - locales \ - curl \ - git \ - && locale-gen en_US.UTF-8 \ - && update-locale LANG=en_US.UTF-8 \ - && apt-get clean - -RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \ - chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && \ - echo "deb [signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list && \ - apt-get update && apt-get install -y gh && \ - apt-get clean - -RUN curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \ - apt-get install -y git-lfs && \ - git lfs install && \ - apt-get clean - -CMD ["bash"] diff --git a/ci_resources/tests_8gb_ram_ci_gradle.properties b/ci_resources/tests_ci_gradle.properties similarity index 86% rename from ci_resources/tests_8gb_ram_ci_gradle.properties rename to ci_resources/tests_ci_gradle.properties index 78577a0975..860c64d2b5 100644 --- a/ci_resources/tests_8gb_ram_ci_gradle.properties +++ b/ci_resources/tests_ci_gradle.properties @@ -8,6 +8,6 @@ org.gradle.caching=false android.useAndroidX=true android.nonTransitiveRClass=false -android.aapt2FromMavenOverride=/opt/android-sdk/build-tools/34.0.0/aapt2 +android.aapt2FromMavenOverride=/opt/android-sdk/build-tools/35.0.0/aapt2 kotlin.incremental=false diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/DefaultVersionProvider.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/DefaultVersionProvider.kt index b55e9637c3..105ca52d15 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/DefaultVersionProvider.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/DefaultVersionProvider.kt @@ -28,9 +28,9 @@ internal class DefaultVersionProvider @Inject constructor( context.packageName, PackageManager.PackageInfoFlags.of(0), ) - .versionName + .versionName!! } else { - context.packageManager.getPackageInfo(context.packageName, 0).versionName + context.packageManager.getPackageInfo(context.packageName, 0).versionName!! } } diff --git a/core/decompose/build.gradle.kts b/core/decompose/build.gradle.kts index aa1b6e3c82..792dbe9a1c 100644 --- a/core/decompose/build.gradle.kts +++ b/core/decompose/build.gradle.kts @@ -1,13 +1,19 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) alias(deps.plugins.kotlin.kapt) id("configuration") } +android { + namespace = "com.tangem.core.decompose" +} + dependencies { api(projects.core.utils) api(deps.decompose) + api(deps.androidx.appCompat) implementation(deps.kotlin.coroutines) implementation(deps.hilt.core) diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContext.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContext.kt index 9426735800..8201bba2c4 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContext.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContext.kt @@ -4,6 +4,7 @@ import com.arkivanov.decompose.ComponentContext import com.tangem.core.decompose.di.HiltComponentBuilderOwner import com.tangem.core.decompose.navigation.NavigationOwner import com.tangem.core.decompose.ui.UiMessageSenderOwner +import com.tangem.core.decompose.utils.ActivityHolder import com.tangem.core.decompose.utils.ComponentScopeOwner import com.tangem.core.decompose.utils.DispatchersOwner import com.tangem.core.decompose.utils.TagsOwner @@ -20,4 +21,5 @@ interface AppComponentContext : DispatchersOwner, UiMessageSenderOwner, HiltComponentBuilderOwner, - TagsOwner \ No newline at end of file + TagsOwner, + ActivityHolder \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContextChildrenFactory.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContextChildrenFactory.kt index 146a7e2bb0..2ae9d71656 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContextChildrenFactory.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContextChildrenFactory.kt @@ -10,6 +10,7 @@ import com.tangem.core.decompose.ui.DefaultUiMessageSender import com.tangem.core.decompose.ui.UiMessageHandler import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.ui.UiMessageSenderOwner +import com.tangem.core.decompose.utils.ActivityHolder import com.tangem.core.decompose.utils.ComponentCoroutineScope import com.tangem.core.decompose.utils.DispatchersOwner import kotlinx.coroutines.CoroutineScope @@ -58,7 +59,8 @@ fun AppComponentContext.childByContext( NavigationOwner by this@childByContext, UiMessageSenderOwner by this@childByContext, DispatchersOwner by this@childByContext, - HiltComponentBuilderOwner by this@childByContext { + HiltComponentBuilderOwner by this@childByContext, + ActivityHolder by this@childByContext { override val tags: HashMap = HashMap() diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt index ea9afa04a5..a8a4fac2f7 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt @@ -1,5 +1,6 @@ package com.tangem.core.decompose.context +import androidx.appcompat.app.AppCompatActivity import com.arkivanov.decompose.ComponentContext import com.arkivanov.essenty.instancekeeper.getOrCreate import com.tangem.core.decompose.di.ModelComponent @@ -17,6 +18,7 @@ class DefaultAppComponentContext( override val dispatchers: CoroutineDispatcherProvider, override val hiltComponentBuilder: ModelComponent.Builder, override val messageSender: UiMessageSender, + override val activity: AppCompatActivity, private val replaceRouter: Router? = null, ) : AppComponentContext, ComponentContext by componentContext { diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ActivityHolder.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ActivityHolder.kt new file mode 100644 index 0000000000..5ef79e239d --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ActivityHolder.kt @@ -0,0 +1,16 @@ +package com.tangem.core.decompose.utils + +import androidx.appcompat.app.AppCompatActivity + +/** + * Interface for holding an [AppCompatActivity] instance. + * + * This interface is used to provide access to the activity in which the component is running. + */ +interface ActivityHolder { + + /** + * The [AppCompatActivity] instance associated with this context. + */ + val activity: AppCompatActivity +} \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 950482041e..24ec4eb861 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :android do desc "Run detekt" lane :detekt do - FileUtils.cp("../ci_resources/tests_8gb_ram_ci_gradle.properties", "../gradle.properties") + FileUtils.cp("../ci_resources/tests_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") gradle(task: "detekt") end @@ -27,7 +27,7 @@ platform :android do desc "Run tests" lane :test do FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") - FileUtils.cp("../ci_resources/tests_8gb_ram_ci_gradle.properties", "../gradle.properties") + FileUtils.cp("../ci_resources/tests_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") gradle(task: "testDebugUnitTest") end diff --git a/features/kyc/api/.gitignore b/features/kyc/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/kyc/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/kyc/api/build.gradle.kts b/features/kyc/api/build.gradle.kts new file mode 100644 index 0000000000..3133f2c7de --- /dev/null +++ b/features/kyc/api/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.kyc.api" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.models) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt new file mode 100644 index 0000000000..6a18888000 --- /dev/null +++ b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.kyc + +import com.tangem.core.decompose.context.AppComponentContext + +interface KycComponent { + + fun launch() + + interface Factory { + fun create(appComponentContext: AppComponentContext): KycComponent + } +} \ No newline at end of file diff --git a/features/kyc/impl/.gitignore b/features/kyc/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/kyc/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/kyc/impl/build.gradle.kts b/features/kyc/impl/build.gradle.kts new file mode 100644 index 0000000000..86e6e641fb --- /dev/null +++ b/features/kyc/impl/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.kyc.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.kyc.api) + + /** Core modules */ + implementation(projects.core.configToggles) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.res) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.datasource) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** Compose libraries */ + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.lottie.compose) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + + /** Other libraries */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.timber) + implementation(deps.firebase.crashlytics) + implementation(deps.sumsub.sdk) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt new file mode 100644 index 0000000000..d192cead2d --- /dev/null +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.kyc + +import com.sumsub.sns.core.SNSMobileSDK +import com.sumsub.sns.core.data.listener.TokenExpirationHandler +import com.tangem.core.decompose.context.AppComponentContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.Locale + +class DefaultKycComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, +) : KycComponent, AppComponentContext by appComponentContext { + + override fun launch() { + val accessToken = "123456" + + val tokenExpirationHandler = object : TokenExpirationHandler { + override fun onTokenExpired(): String? { + // Access token expired + // get a new one and pass it to the callback to re-initiate the SDK + val newToken = "..." // get a new token from your backend + return newToken + } + } + + val snsSdk = SNSMobileSDK.Builder(activity) + .withAccessToken(accessToken, onTokenExpiration = tokenExpirationHandler) + .withLocale(Locale("en")) + .build() + + snsSdk.launch() + } + + @AssistedFactory + interface Factory : KycComponent.Factory { + override fun create(appComponentContext: AppComponentContext): DefaultKycComponent + } +} \ No newline at end of file diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt new file mode 100644 index 0000000000..c72f282775 --- /dev/null +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt @@ -0,0 +1,16 @@ +package com.tangem.features.kyc.di + +import com.tangem.features.kyc.DefaultKycComponent +import com.tangem.features.kyc.KycComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface FeatureModule { + + @Binds + fun bindComponentFactory(impl: DefaultKycComponent.Factory): KycComponent.Factory +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 04c6f4b039..2473ea55b9 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -15,7 +15,7 @@ firebasePerf = "1.4.2" androidxActivityCompose = "1.8.0" androidxAppCompat = "1.5.1" androidxBrowser = "1.4.0" -androidxConstraintLayout = "2.1.4" +androidxConstraintLayout = "2.2.1" androidxKtx = "1.9.0" androidxSplashScreen = "1.0.1" androidxFragment = "1.8.5" @@ -32,7 +32,7 @@ compose-runtime = "1.7.4" compose-foundation = "1.7.4" compose-material = "1.7.4" compose-material3 = "1.3.1" -compose-constraint = "1.0.1" +compose-constraint = "1.1.1" compose-navigation = "2.7.7" compose-accompanist = "0.30.1" compose-paging = "3.2.1" @@ -90,6 +90,7 @@ room = "2.6.1" markdown = "0.7.2" markdownComposeView = "0.5.4" usedesk = "4.4.0" +sumsub = "1.36.0" # endregion Other libraries # region Tools @@ -277,4 +278,5 @@ markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" } markdown-composeview = { module = "com.github.jeziellago:compose-markdown", version.ref = "markdownComposeView" } usedesk-chat-sdk = { module = "com.github.Usedesk.Android_SDK:chat-sdk", version.ref = "usedesk" } usedesk-chat-gui = { module = "com.github.Usedesk.Android_SDK:chat-gui", version.ref = "usedesk" } +sumsub-sdk = { module = "com.sumsub.sns:idensic-mobile-sdk", version.ref = "sumsub" } # endregion Other diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt index 3db7478662..5ddd7ba12d 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt @@ -6,6 +6,6 @@ internal object AppConfig { const val versionName = "1.0.0-SNAPSHOT" // const val versionName = "100.0.0-SNAPSHOT" //TODO: [REDACTED_JIRA] const val minSdkVersion = 24 - const val targetSdkVersion = 34 - const val compileSdkVersion = 34 + const val targetSdkVersion = 35 + const val compileSdkVersion = 35 } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c827ecc8a0..eca5b3d022 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -118,6 +118,7 @@ dependencyResolutionManagement { } } maven("https://jitpack.io") + maven("https://maven.sumsub.com/repository/maven-public/") } versionCatalogs { @@ -247,6 +248,9 @@ include(":features:walletconnect:impl") include(":features:hot-wallet:api") include(":features:hot-wallet:impl") + +include(":features:kyc:api") +include(":features:kyc:impl") // endregion Feature modules // region Domain modules diff --git a/tangem-android-tools b/tangem-android-tools index 53e359bc2e..8d971eb9d9 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 53e359bc2ead37feeb9e70d7004c91cde07ccf9b +Subproject commit 8d971eb9d901358419da78408ecf7dc3f0c4331f From 315d7c9d6cd78469149e98baf3aa17e588f51738 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 23 Jun 2025 12:06:26 +0300 Subject: [PATCH 43/99] Updated on 2026-08-14 --- app/build.gradle.kts | 16 +- .../kotlin/com/tangem/common/BaseTestCase.kt | 71 +++-- .../com/tangem/common/extensions/KNode.kt | 8 + .../tangem/common/extensions/TestDataUtils.kt | 9 - .../com/tangem/scenarios/MainPageScenario.kt | 27 -- .../scenarios/OpenMainScreenScenario.kt | 44 +++ .../kotlin/com/tangem/screens/DetailsPage.kt | 16 -- .../com/tangem/screens/DetailsTestScreen.kt | 51 ++++ .../com/tangem/screens/DisclaimerPage.kt | 9 - .../tangem/screens/DisclaimerTestScreen.kt | 17 ++ .../kotlin/com/tangem/screens/MainPage.kt | 9 - .../com/tangem/screens/MainTestScreen.kt | 11 + .../kotlin/com/tangem/screens/StoriesPage.kt | 10 - .../com/tangem/screens/StoriesTestScreen.kt | 32 +++ .../kotlin/com/tangem/screens/TestTopBar.kt | 16 ++ .../kotlin/com/tangem/screens/TopBarPage.kt | 9 - .../com/tangem/screens/WalletSettingsPage.kt | 13 - .../screens/WalletSettingsTestScreen.kt | 31 +++ .../com/tangem/tests/DetailsScreenTest.kt | 259 ++++++++++-------- .../kotlin/com/tangem/tests/MainScreenTest.kt | 21 ++ .../kotlin/com/tangem/tests/ScanErrorTest.kt | 56 ++-- .../kotlin/com/tangem/tests/StoriesTest.kt | 49 ++-- .../main/java/com/tangem/tap/MainActivity.kt | 6 +- domain/legacy/build.gradle.kts | 3 +- gradle/dependencies.toml | 10 +- 25 files changed, 482 insertions(+), 321 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/common/extensions/TestDataUtils.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/MainPageScenario.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/screens/DetailsPage.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/DetailsTestScreen.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPage.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/DisclaimerTestScreen.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/screens/MainPage.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/MainTestScreen.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/screens/StoriesPage.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/StoriesTestScreen.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/TestTopBar.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/screens/TopBarPage.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPage.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsTestScreen.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 794229daee..18dfcd383d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,6 +71,10 @@ configurations.all { } } +configurations.androidTestImplementation { + exclude(module = "protobuf-lite") +} + dependencies { implementation(projects.domain.legacy) @@ -317,19 +321,13 @@ dependencies { testImplementation(deps.test.mockk) testImplementation(deps.test.truth) androidTestImplementation(deps.test.junit.android) - androidTestImplementation(deps.test.espresso){ - exclude(group = "com.google.protobuf", module = "protobuf-lite") //conflicting with firebasePerf - } + androidTestImplementation(deps.test.espresso) androidTestImplementation(deps.test.espresso.intents) - { - exclude(group = "com.google.protobuf", module = "protobuf-lite") //conflicting with firebasePerf - } + androidTestImplementation(deps.test.kaspresso) + androidTestImplementation(deps.test.kaspresso.compose) androidTestImplementation(deps.test.compose.junit) androidTestImplementation(deps.test.hamcrest) androidTestImplementation(deps.test.hilt) - androidTestImplementation(deps.test.ultron.android) - androidTestImplementation(deps.test.ultron.compose) - androidTestImplementation(deps.test.ultron.allure) kaptAndroidTest(deps.test.hilt.compiler) /** Chucker */ diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index b028458c9b..d404225116 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -1,66 +1,77 @@ package com.tangem.common import android.Manifest +import android.content.Context +import android.util.Log +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.datastore.dataStoreFile +import androidx.test.espresso.intent.Intents +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import com.atiurin.ultron.core.compose.config.UltronComposeConfig -import com.atiurin.ultron.core.compose.createUltronComposeRule -import com.atiurin.ultron.core.compose.listeners.ComposDebugListener -import com.atiurin.ultron.core.config.UltronCommonConfig -import com.atiurin.ultron.core.config.UltronConfig -import com.atiurin.ultron.core.test.UltronTest +import com.kaspersky.components.composesupport.config.withComposeSupport +import com.kaspersky.kaspresso.kaspresso.Kaspresso +import com.kaspersky.kaspresso.testcases.api.testcase.TestCase import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.MainActivity import dagger.hilt.android.testing.HiltAndroidRule import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking -import org.junit.BeforeClass import org.junit.Rule +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.RunWith import javax.inject.Inject -abstract class BaseTestCase : UltronTest() { +abstract class BaseTestCase : TestCase( + kaspressoBuilder = Kaspresso.Builder.withComposeSupport() +) { + @Inject lateinit var tangemSdkManager: TangemSdkManager @Inject lateinit var appPreferencesStore: AppPreferencesStore - @get:Rule(order = 0) - val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + private val hiltRule = HiltAndroidRule(this) + private val permissionRule = GrantPermissionRule.grant( Manifest.permission.POST_NOTIFICATIONS, - Manifest.permission.CAMERA + Manifest.permission.CAMERA, ) - @get:Rule(order = 1) - val hiltRule = HiltAndroidRule(this) + val composeTestRule = createAndroidComposeRule() - @get:Rule (order = 2) - val injectionRule = ApplicationInjectionExecutionRule() + @Rule + @JvmField + val ruleChain: TestRule = RuleChain + .outerRule(hiltRule) + .around(ApplicationInjectionExecutionRule()) + .around(permissionRule) + .around(composeTestRule) - @get:Rule(order = 3) - val composeRule = createUltronComposeRule() - - override val beforeTest: () -> Unit = { + protected fun setupHooks( + additionalBeforeSection: () -> Unit = {}, + additionalAfterSection: () -> Unit = {}, + ) = before { hiltRule.inject() + Intents.init() + additionalBeforeSection() + runBlocking { delay(INIT_DELAY) } - } - override val afterTest: () -> Unit = { + }.after { runBlocking { appPreferencesStore.editData { prefs -> prefs.clear() } } + + additionalAfterSection() + Intents.release() } companion object { - @BeforeClass - @JvmStatic - fun config() { - UltronConfig.applyRecommended() - UltronComposeConfig.applyRecommended() - UltronCommonConfig.addListener(ComposDebugListener()) - } - - private const val INIT_DELAY = 2000L + private const val INIT_DELAY = 1000L } + } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt new file mode 100644 index 0000000000..a7baa714a4 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt @@ -0,0 +1,8 @@ +package com.tangem.common.extensions + +import io.github.kakaocup.compose.node.element.KNode + +fun KNode.clickWithAssertion() { + assertIsDisplayed() + performClick() +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/TestDataUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/TestDataUtils.kt deleted file mode 100644 index 671c7e4c60..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/TestDataUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.common.extensions - -import androidx.test.platform.app.InstrumentationRegistry - -object TestDataUtils { - fun getResourceString(resourceId: Int): String { - return InstrumentationRegistry.getInstrumentation().targetContext.resources.getString(resourceId) - } -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/MainPageScenario.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/MainPageScenario.kt deleted file mode 100644 index 2513c160b0..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/MainPageScenario.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.scenarios - -import com.atiurin.ultron.allure.step.step -import com.atiurin.ultron.extensions.assertIsDisplayed -import com.atiurin.ultron.extensions.click -import com.tangem.domain.models.scan.ProductType -import com.tangem.screens.DisclaimerPage -import com.tangem.screens.MainPage -import com.tangem.screens.StoriesPage -import com.tangem.tap.domain.sdk.mocks.MockProvider - -object MainPageScenario { - fun open(productType: ProductType? = null) { - if (productType != null) { - MockProvider.setMocks(productType) - } - step("Click on \"Accept\" button") { - DisclaimerPage.acceptButton.click() - } - step("Click on \"Scan\" button emulating scan error") { - StoriesPage.scanButton.click() - } - step("Assert: main is displayed") { - MainPage.container.assertIsDisplayed() - } - } -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt new file mode 100644 index 0000000000..cd966f6a0f --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt @@ -0,0 +1,44 @@ +package com.tangem.scenarios + +import androidx.compose.ui.test.junit4.ComposeTestRule +import com.kaspersky.kaspresso.testcases.api.scenario.Scenario +import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.domain.models.scan.ProductType +import com.tangem.screens.DisclaimerTestScreen +import com.tangem.screens.MainTestScreen +import com.tangem.screens.StoriesTestScreen +import com.tangem.screens.TestTopBar +import com.tangem.tap.domain.sdk.mocks.MockProvider +import io.github.kakaocup.compose.node.element.ComposeScreen + +class OpenMainScreenScenario( + private val testRule: ComposeTestRule, + private val productType: ProductType? = null, +) : Scenario() { + override val steps: TestContext.() -> Unit = { + if (productType != null) { + MockProvider.setMocks(productType) + } + ComposeScreen.onComposeScreen(testRule) { + step("Click on \"Accept\" button") { + acceptButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(testRule) { + step("Click on \"Scan\" button") { + scanButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(testRule) { + step("Make sure wallet screen is visible") { + assertIsDisplayed() + } + } + ComposeScreen.onComposeScreen(testRule) { + step("Close Markets tooltip"){ + performClick() + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPage.kt b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPage.kt deleted file mode 100644 index 990f09205c..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPage.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.screens - -import androidx.compose.ui.test.hasText -import com.atiurin.ultron.page.Page -import com.tangem.common.extensions.TestDataUtils.getResourceString -import com.tangem.wallet.R - -object DetailsPage : Page() { - val walletConnectButton = hasText(getResourceString(R.string.wallet_connect_title)) - val walletNameButton = hasText(getResourceString(R.string.manage_tokens_network_selector_wallet)) - val scanCardButton = hasText(getResourceString(R.string.scan_card_settings_button)) - val buyTangemButton = hasText(getResourceString(R.string.details_buy_wallet)) - val appSettingsButton = hasText(getResourceString(R.string.app_settings_title)) - val contactSupportButton = hasText(getResourceString(R.string.details_row_title_contact_to_support)) - val toSButton = hasText(getResourceString(R.string.disclaimer_title)) -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DetailsTestScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/DetailsTestScreen.kt new file mode 100644 index 0000000000..528989a4e3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/DetailsTestScreen.kt @@ -0,0 +1,51 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class DetailsTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.DETAILS_SCREEN) } + ) { + + val walletConnectButton: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + hasText(getResourceString(R.string.wallet_connect_title)) + } + + private val walletBlock: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + } + + val walletNameButton: KNode = walletBlock.child { + hasClickAction() + hasPosition(0) + } + + val scanCardButton: KNode = walletBlock.child { + hasText(getResourceString(R.string.scan_card_settings_button)) + } + + val buyTangemButton: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + hasText(getResourceString(R.string.details_buy_wallet)) + } + + val appSettingsButton: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + hasText(getResourceString(R.string.app_settings_title)) + } + val contactSupportButton: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + hasText(getResourceString(R.string.details_row_title_contact_to_support)) + } + val toSButton: KNode = child { + hasTestTag(TestTags.DETAILS_SCREEN_ITEM) + hasText(getResourceString(R.string.disclaimer_title)) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPage.kt b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPage.kt deleted file mode 100644 index b8dbd41150..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPage.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.screens - -import androidx.compose.ui.test.hasTestTag -import com.atiurin.ultron.page.Page -import com.tangem.core.ui.test.TestTags - -object DisclaimerPage : Page() { - val acceptButton = hasTestTag(TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON) -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerTestScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerTestScreen.kt new file mode 100644 index 0000000000..4af7f33d51 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerTestScreen.kt @@ -0,0 +1,17 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode + +class DisclaimerTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.DISCLAIMER_SCREEN_CONTAINER) } + ) { + + val acceptButton: KNode = child { + hasTestTag(TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainPage.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainPage.kt deleted file mode 100644 index f9016b3f69..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainPage.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.screens - -import androidx.compose.ui.test.hasTestTag -import com.atiurin.ultron.page.Page -import com.tangem.core.ui.test.TestTags - -object MainPage : Page() { - val container = hasTestTag(TestTags.MAIN_SCREEN) -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainTestScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainTestScreen.kt new file mode 100644 index 0000000000..a342754478 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainTestScreen.kt @@ -0,0 +1,11 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import io.github.kakaocup.compose.node.element.ComposeScreen + +class MainTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.MAIN_SCREEN) } + ) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StoriesPage.kt b/app/src/androidTest/kotlin/com/tangem/screens/StoriesPage.kt deleted file mode 100644 index 51f472521a..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/screens/StoriesPage.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.screens - -import androidx.compose.ui.test.hasTestTag -import com.atiurin.ultron.page.Page -import com.tangem.core.ui.test.TestTags - -object StoriesPage : Page() { - val scanButton = hasTestTag(TestTags.STORIES_SCREEN_SCAN_BUTTON) - val orderButton = hasTestTag(TestTags.STORIES_SCREEN_ORDER_BUTTON) -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StoriesTestScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/StoriesTestScreen.kt new file mode 100644 index 0000000000..9dd1a415e9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StoriesTestScreen.kt @@ -0,0 +1,32 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.views.KView +import io.github.kakaocup.kakao.text.KButton + +class StoriesTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.STORIES_SCREEN) } + ) { + + val scanButton: KNode = child { + hasTestTag(TestTags.STORIES_SCREEN_SCAN_BUTTON) + } + + val orderButton: KNode = child { + hasTestTag(TestTags.STORIES_SCREEN_ORDER_BUTTON) + } + + val enableNFCAlert: KView = KView { + withId(R.id.alertTitle) + } + + val cancelButton: KButton = KButton { + withId(android.R.id.button2) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TestTopBar.kt b/app/src/androidTest/kotlin/com/tangem/screens/TestTopBar.kt new file mode 100644 index 0000000000..83d2e8a6a9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/TestTopBar.kt @@ -0,0 +1,16 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode + +class TestTopBar(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.MAIN_SCREEN_TOP_BAR) } + ) { + val moreButton: KNode = child { + hasTestTag(TestTags.MAIN_SCREEN_MORE_BUTTON) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TopBarPage.kt b/app/src/androidTest/kotlin/com/tangem/screens/TopBarPage.kt deleted file mode 100644 index 2505ffae8b..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/screens/TopBarPage.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.screens - -import androidx.compose.ui.test.hasTestTag -import com.atiurin.ultron.page.Page -import com.tangem.core.ui.test.TestTags - -object TopBarPage : Page() { - val moreButton = hasTestTag(TestTags.MAIN_SCREEN_MORE_BUTTON) -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPage.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPage.kt deleted file mode 100644 index a38559a523..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPage.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.screens - -import androidx.compose.ui.test.hasText -import com.atiurin.ultron.page.Page -import com.tangem.common.extensions.TestDataUtils.getResourceString -import com.tangem.wallet.R - -object WalletSettingsPage : Page() { - val linkMoreCardsButton = hasText(getResourceString(R.string.details_row_title_create_backup)) - val cardSettingsButton = hasText(getResourceString(R.string.card_settings_title)) - val referralProgramButton = hasText(getResourceString(R.string.details_referral_title)) - val forgetWalletButton = hasText(getResourceString(R.string.settings_forget_wallet)) -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsTestScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsTestScreen.kt new file mode 100644 index 0000000000..1b31bd509c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsTestScreen.kt @@ -0,0 +1,31 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class WalletSettingsTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.WALLET_SETTINGS_SCREEN) } + ) { + private val walletSettingsItem: KNode = child { + hasTestTag(TestTags.WALLET_SETTINGS_SCREEN_ITEM) + } + + val linkMoreCardsButton: KNode = walletSettingsItem.child { + hasText(getResourceString(R.string.details_row_title_create_backup)) + } + val cardSettingsButton: KNode = walletSettingsItem.child { + hasText(getResourceString(R.string.card_settings_title)) + } + val referralProgramButton: KNode = walletSettingsItem.child { + hasText(getResourceString(R.string.details_referral_title)) + } + val forgetWalletButton: KNode = walletSettingsItem.child { + hasText(getResourceString(R.string.settings_forget_wallet)) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt index 1951caf4d7..dd86a046cc 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt @@ -1,138 +1,157 @@ package com.tangem.tests -import com.atiurin.ultron.allure.step.step -import com.atiurin.ultron.extensions.assertIsDisplayed -import com.atiurin.ultron.extensions.assertIsNotDisplayed -import com.atiurin.ultron.extensions.click import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion import com.tangem.domain.models.scan.ProductType -import com.tangem.scenarios.MainPageScenario -import com.tangem.screens.DetailsPage -import com.tangem.screens.TopBarPage -import com.tangem.screens.WalletSettingsPage +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.screens.DetailsTestScreen +import com.tangem.screens.TestTopBar +import com.tangem.screens.WalletSettingsTestScreen import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.compose.node.element.ComposeScreen import org.junit.Test @HiltAndroidTest class DetailsScreenTest : BaseTestCase() { @Test - fun walletWithoutBackupDetails() { - MainPageScenario.open() - step("Open wallet details") { - TopBarPage.moreButton.click() + fun walletWithoutBackupDetails() = + setupHooks().run { + scenario(OpenMainScreenScenario(composeTestRule)) + ComposeScreen.onComposeScreen(composeTestRule) { + step("Open wallet details") { + moreButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert wallet connect button is visible") { + walletConnectButton.assertIsDisplayed() + } + step("Assert scan card button is visible") { + scanCardButton.assertIsDisplayed() + } + step("Assert buy Tangem card button is visible") { + buyTangemButton.assertIsDisplayed() + } + step("Assert app settings button is visible") { + appSettingsButton.assertIsDisplayed() + } + step("Assert contact support button is visible") { + contactSupportButton.assertIsDisplayed() + } + step("Assert terms or service button is visible") { + toSButton.assertIsDisplayed() + } + step("Open wallet settings screen") { + walletNameButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert Link more cards button is visible") { + linkMoreCardsButton.assertIsDisplayed() + } + step("Assert Card Settings button is visible") { + cardSettingsButton.assertIsDisplayed() + } + step("Assert Referral program button is visible") { + referralProgramButton.assertIsDisplayed() + } + step("Assert Forget wallet button is visible") { + forgetWalletButton.assertIsDisplayed() + } + } } - step("Assert wallet connect button is visible") { - DetailsPage.walletConnectButton.assertIsDisplayed() - } - step("Assert scan card button is visible") { - DetailsPage.scanCardButton.assertIsDisplayed() - } - step("Assert buy Tangem card button is visible") { - DetailsPage.buyTangemButton.assertIsDisplayed() - } - step("Assert app settings button is visible") { - DetailsPage.appSettingsButton.assertIsDisplayed() - } - step("Assert contact support button is visible") { - DetailsPage.contactSupportButton.assertIsDisplayed() - } - step("Assert terms or service button is visible") { - DetailsPage.toSButton.assertIsDisplayed() - } - step("Open wallet settings screen") { - DetailsPage.walletNameButton.click() - } - step("Assert Link more cards button is visible") { - WalletSettingsPage.linkMoreCardsButton.assertIsDisplayed() - } - step("Assert Card Settings button is visible") { - WalletSettingsPage.cardSettingsButton.assertIsDisplayed() - } - step("Assert Referral program button is visible") { - WalletSettingsPage.referralProgramButton.assertIsDisplayed() - } - step("Assert Forget wallet button is visible") { - WalletSettingsPage.forgetWalletButton.assertIsDisplayed() - } - } @Test - fun wallet2Details() { - MainPageScenario.open(ProductType.Wallet2) - step("Open wallet details") { - TopBarPage.moreButton.click() + fun wallet2Details() = + setupHooks().run { + scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2)) + ComposeScreen.onComposeScreen(composeTestRule) { + step("Open wallet details") { + moreButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert wallet connect button is visible") { + walletConnectButton.assertIsDisplayed() + } + step("Assert scan card button is visible") { + scanCardButton.assertIsDisplayed() + } + step("Assert buy Tangem card button is visible") { + buyTangemButton.assertIsDisplayed() + } + step("Assert app settings button is visible") { + appSettingsButton.assertIsDisplayed() + } + step("Assert contact support button is visible") { + contactSupportButton.assertIsDisplayed() + } + step("Assert terms or service button is visible") { + toSButton.assertIsDisplayed() + } + step("Open wallet settings screen") { + walletNameButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert Link more cards button does not exist") { + linkMoreCardsButton.assertIsNotDisplayed() + } + step("Assert Card Settings button is visible") { + cardSettingsButton.assertIsDisplayed() + } + step("Assert Referral program button is visible") { + referralProgramButton.assertIsDisplayed() + } + step("Assert Forget wallet button is visible") { + forgetWalletButton.assertIsDisplayed() + } + } } - step("Assert wallet connect button is visible") { - DetailsPage.walletConnectButton.assertIsDisplayed() - } - step("Assert scan card button is visible") { - DetailsPage.scanCardButton.assertIsDisplayed() - } - step("Assert buy Tangem card button is visible") { - DetailsPage.buyTangemButton.assertIsDisplayed() - } - step("Assert app settings button is visible") { - DetailsPage.appSettingsButton.assertIsDisplayed() - } - step("Assert contact support button is visible") { - DetailsPage.contactSupportButton.assertIsDisplayed() - } - step("Assert terms or service button is visible") { - DetailsPage.toSButton.assertIsDisplayed() - } - step("Open wallet settings screen") { - DetailsPage.walletNameButton.click() - } - step("Assert Link more cards button does not exist") { - WalletSettingsPage.linkMoreCardsButton.assertIsNotDisplayed() - } - step("Assert Card Settings button is visible") { - WalletSettingsPage.cardSettingsButton.assertIsDisplayed() - } - step("Assert Referral program button is visible") { - WalletSettingsPage.referralProgramButton.assertIsDisplayed() - } - step("Assert Forget wallet button is visible") { - WalletSettingsPage.forgetWalletButton.assertIsDisplayed() - } - } @Test - fun noteDetails() { - MainPageScenario.open(ProductType.Note) - step("Open wallet details") { - TopBarPage.moreButton.click() + fun noteDetails() = + setupHooks().run { + scenario(OpenMainScreenScenario(composeTestRule, ProductType.Note)) + ComposeScreen.onComposeScreen(composeTestRule) { + step("Open wallet details") { + moreButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert wallet connect button does not exist") { + walletConnectButton.assertIsNotDisplayed() + } + step("Assert scan card button is visible") { + scanCardButton.assertIsDisplayed() + } + step("Assert buy Tangem card button is visible") { + buyTangemButton.assertIsDisplayed() + } + step("Assert app settings button is visible") { + appSettingsButton.assertIsDisplayed() + } + step("Assert contact support button is visible") { + contactSupportButton.assertIsDisplayed() + } + step("Assert terms or service button is visible") { + toSButton.assertIsDisplayed() + } + step("Open wallet settings screen") { + walletNameButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Assert Card Settings button is visible") { + cardSettingsButton.assertIsDisplayed() + } + step("Assert Referral program button does not exist") { + referralProgramButton.assertIsNotDisplayed() + } + step("Assert Forget wallet button is visible") { + forgetWalletButton.assertIsDisplayed() + } + } } - step("Assert wallet connect button does not exist") { - DetailsPage.walletConnectButton.assertIsNotDisplayed() - } - step("Assert scan card button is visible") { - DetailsPage.scanCardButton.assertIsDisplayed() - } - step("Assert buy Tangem card button is visible") { - DetailsPage.buyTangemButton.assertIsDisplayed() - } - step("Assert app settings button is visible") { - DetailsPage.appSettingsButton.assertIsDisplayed() - } - step("Assert contact support button is visible") { - DetailsPage.contactSupportButton.assertIsDisplayed() - } - step("Assert terms or service button is visible") { - DetailsPage.toSButton.assertIsDisplayed() - } - step("Open wallet settings screen") { - DetailsPage.walletNameButton.click() - } - step("Assert Card Settings button is visible") { - WalletSettingsPage.cardSettingsButton.assertIsDisplayed() - } - step("Assert Referral program button does not exist") { - WalletSettingsPage.referralProgramButton.assertIsNotDisplayed() - } - step("Assert Forget wallet button is visible") { - WalletSettingsPage.forgetWalletButton.assertIsDisplayed() - } - } -} +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt new file mode 100644 index 0000000000..fd3a2df953 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt @@ -0,0 +1,21 @@ +package com.tangem.tests + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import com.tangem.common.BaseTestCase +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.tap.MainActivity +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Rule +import org.junit.Test + +@HiltAndroidTest +class MainScreenTest : BaseTestCase() { + + @Test + fun goToMain() { + setupHooks().run { + scenario(OpenMainScreenScenario(composeTestRule)) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt index de198088d3..da686950a7 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt @@ -1,42 +1,40 @@ package com.tangem.tests -import com.atiurin.ultron.allure.step.step -import com.atiurin.ultron.extensions.assertIsDisplayed -import com.atiurin.ultron.extensions.assertIsNotDisplayed -import com.atiurin.ultron.extensions.click import com.tangem.common.BaseTestCase -import com.tangem.screens.DisclaimerPage -import com.tangem.screens.MainPage -import com.tangem.screens.StoriesPage +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.screens.DisclaimerTestScreen +import com.tangem.screens.MainTestScreen +import com.tangem.screens.StoriesTestScreen import com.tangem.tap.domain.sdk.mocks.MockProvider import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.compose.node.element.ComposeScreen import org.junit.Test @HiltAndroidTest class ScanErrorTest : BaseTestCase() { @Test - fun goToMain() { - step("Click on \"Accept\" button") { - DisclaimerPage.acceptButton.click() + fun goToMain() = + setupHooks().run { + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Accept\" button") { + acceptButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Scan\" button emulating scan error") { + MockProvider.setEmulateError() + scanButton.clickWithAssertion() + } + step("Click on \"Scan\" button again without emulating error") { + MockProvider.resetEmulateError() + scanButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Make sure wallet screen is visible") { + assertIsDisplayed() + } + } } - step("Emulate scan error") { - MockProvider.setEmulateError() - } - step("Click on \"Scan\" button emulating scan error") { - StoriesPage.scanButton.click() - } - step("Assert: Error is displayed") { - MainPage.container.assertIsNotDisplayed() - } - step("Emulate success scan") { - MockProvider.resetEmulateError() - } - step("Click on \"Scan\" button") { - StoriesPage.scanButton.click() - } - step("Assert: wallet screen is displayed") { - MainPage.container.assertIsDisplayed() - } - } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt index 5adaaeada1..eab4389f6b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -1,36 +1,41 @@ package com.tangem.tests import android.content.Intent.ACTION_VIEW -import androidx.test.espresso.intent.Intents -import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction -import androidx.test.espresso.intent.matcher.IntentMatchers.hasData -import com.atiurin.ultron.allure.step.step -import com.atiurin.ultron.core.config.UltronConfig.UiAutomator.Companion.uiDevice -import com.atiurin.ultron.extensions.click +import androidx.test.espresso.intent.matcher.UriMatchers import com.tangem.common.BaseTestCase -import com.tangem.screens.DisclaimerPage -import com.tangem.screens.StoriesPage +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.screens.DisclaimerTestScreen +import com.tangem.screens.StoriesTestScreen import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import dagger.hilt.android.testing.HiltAndroidTest -import org.hamcrest.core.AllOf.allOf +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.kakao.intent.KIntent +import org.hamcrest.Matchers import org.junit.Test @HiltAndroidTest class StoriesTest : BaseTestCase() { @Test - fun checkOrderButton() { - Intents.init() - step("Click Accept on ToS") { - DisclaimerPage.acceptButton.click() + fun clickOnOrderButton() = + setupHooks().run { + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Accept\" button") { + acceptButton.clickWithAssertion() + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Order\" button") { + orderButton.clickWithAssertion() + } + step("Assert: browser opened") { + val expectedIntent = KIntent { + hasAction(ACTION_VIEW) + hasData { toString().startsWith(NEW_BUY_WALLET_URL) } + } + expectedIntent.intended() + device.uiDevice.pressBack() + } + } } - step("Click order button ") { - StoriesPage.orderButton.click() - } - step("Assert: browser is opened ") { - Intents.intended(allOf(hasAction(ACTION_VIEW), hasData(NEW_BUY_WALLET_URL))) - uiDevice.pressBack() - Intents.release() - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 557f69fdd0..3a4577ab02 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -77,6 +77,7 @@ import com.tangem.tap.routing.utils.DeepLinkFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.utils.extensions.uriValidate +import com.tangem.wallet.BuildConfig import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -353,7 +354,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { intentProcessor.removeAll() // workaround: kill process when activity destroy to avoid state when lock() wallets // and navigation to unlock screen was skipped because system kills activity but not process - android.os.Process.killProcess(android.os.Process.myPid()) + if (BuildConfig.BUILD_TYPE != MOCKED_BUILD_TYPE) { + android.os.Process.killProcess(android.os.Process.myPid()) + } super.onDestroy() } @@ -556,5 +559,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { companion object { private const val APP_THEME_LOAD_TIMEOUT = 2 + private const val MOCKED_BUILD_TYPE = "mocked" } } \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index e109af45b0..1326610ec7 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -52,6 +52,5 @@ dependencies { testImplementation(deps.test.mockk) testImplementation(deps.test.truth) testImplementation(projects.common.test) - androidTestImplementation(deps.test.junit.android) - androidTestImplementation(deps.test.espresso) + } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 2473ea55b9..0839acc10f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -107,9 +107,8 @@ junitAndroidExt = "1.1.5" mockk = "1.13.4" turbine = "1.2.0" truth = "1.1.3" -ultron-android = "2.5.4" -ultron-compose = "2.5.4" -ultron-allure ="2.5.4" +kaspresso = "1.5.4" +kaspresso-compose = "1.5.4" compose-junit = "1.6.2" hamcrest = "2.2" # endregion Testing @@ -207,9 +206,8 @@ test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitA test-truth = { module = "com.google.truth:truth", version.ref = "truth" } test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } test-turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } -test-ultron-android = { module = "com.atiurin:ultron-android", version.ref = "ultron-android" } -test-ultron-compose = { module = "com.atiurin:ultron-compose", version.ref = "ultron-compose" } -test-ultron-allure = { module = "com.atiurin:ultron-allure", version.ref = "ultron-allure" } +test-kaspresso = { module = "com.kaspersky.android-components:kaspresso", version.ref = "kaspresso" } +test-kaspresso-compose = { module = "com.kaspersky.android-components:kaspresso-compose-support", version.ref = "kaspresso-compose" } test-compose-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-junit" } test-hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } test-hilt = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } From 7f393a301c9f14e06146b7beac427dee8926634b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Jun 2025 15:27:29 +0500 Subject: [PATCH 44/99] Updated on 2026-08-14 --- .../message/MessageBottomSheetV2.kt | 2 +- .../managetokens/CreateCoinCurrencyUseCase.kt | 0 features/manage-tokens/impl/build.gradle.kts | 1 + .../DefaultChooseManagedTokensComponent.kt | 34 +- .../model/ChooseManagedTokensModel.kt | 7 +- .../SwapChooseTokenNetworkComponent.kt | 20 ++ .../SwapChooseTokenNetworkTrigger.kt | 11 + .../choosetoken/di/SwapChooseTokenModule.kt | 46 +++ .../DefaultSwapChooseTokenNetworkComponent.kt | 42 +++ .../DefaultSwapChooseTokenNetworkTrigger.kt | 28 ++ .../entity/SwapChooseTokenNetworkUM.kt | 40 +++ .../model/SwapChooseTokenFactory.kt | 26 ++ .../model/SwapChooseTokenNetworkModel.kt | 118 +++++++ .../SwapChooseContentStateTransformer.kt | 54 ++++ .../SwapChooseErrorStateTransformer.kt | 24 ++ .../ui/SwapChooseTokenNetworkContent.kt | 301 ++++++++++++++++++ .../feature/swap/ui/SwapSelectTokenScreen.kt | 2 +- 17 files changed, 741 insertions(+), 15 deletions(-) delete mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCoinCurrencyUseCase.kt create mode 100644 features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt create mode 100644 features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkComponent.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/entity/SwapChooseTokenNetworkUM.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseErrorStateTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt index 407a375ade..57f8d0f0d5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt @@ -62,7 +62,7 @@ fun MessageBottomSheetV2(state: MessageBottomSheetUMV2, onDismissRequest: () -> } @Composable -private fun MessageBottomSheetV2Content(state: MessageBottomSheetUMV2, modifier: Modifier = Modifier) { +fun MessageBottomSheetV2Content(state: MessageBottomSheetUMV2, modifier: Modifier = Modifier) { Column(modifier = modifier) { state.elements.fastForEach { when (it) { diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCoinCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCoinCurrencyUseCase.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 87ecb9bb7b..84a0eb5e9a 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /* Project - API */ implementation(projects.features.manageTokens.api) + implementation(projects.features.swapV2.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt index c42147407a..ecd9dd8f6d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt @@ -7,20 +7,27 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig import com.tangem.features.managetokens.choosetoken.model.ChooseManagedTokensModel import com.tangem.features.managetokens.choosetoken.ui.ChooseManagedTokenContent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkTrigger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch internal class DefaultChooseManagedTokensComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: ChooseManagedTokensComponent.Params, + private val swapChooseTokenNetworkFactory: SwapChooseTokenNetworkComponent.Factory, + private val swapChooseTokenNetworkTrigger: SwapChooseTokenNetworkTrigger, ) : ChooseManagedTokensComponent, AppComponentContext by context { private val model: ChooseManagedTokensModel = getOrCreateModel(params) @@ -48,18 +55,21 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor( config: ChooseManageTokensBottomSheetConfig, componentContext: ComponentContext, ): ComposableBottomSheetComponent = when (config) { - else -> getStubComponent() - } - - private fun getStubComponent() = StubComponent() - - class StubComponent : ComposableBottomSheetComponent { - override fun dismiss() {} - - @Composable - override fun BottomSheet() { - /* no-op */ - } + is ChooseManageTokensBottomSheetConfig.SwapTokensBottomSheetConfig -> swapChooseTokenNetworkFactory.create( + context = childByContext(componentContext), + params = SwapChooseTokenNetworkComponent.Params( + userWalletId = config.userWalletId, + initialCurrency = config.initialCurrency, + token = config.token, + onDismiss = model.bottomSheetNavigation::dismiss, + onResult = { + componentScope.launch { + swapChooseTokenNetworkTrigger.trigger(it) + } + router.pop() + }, + ), + ) } @AssistedFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index 65284e752d..ad0f6fe8f2 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -30,6 +30,7 @@ import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -142,7 +143,11 @@ internal class ChooseManagedTokensModel @Inject constructor( private fun updateItems(items: ImmutableList) { uiState.update { state -> state.copy( - readContent = state.readContent.copy(items = items), + readContent = state.readContent.copy( + items = items.filterNot { + it.id.value == params.initialCurrency.id.rawCurrencyId?.value + }.toPersistentList(), + ), ) } } diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt new file mode 100644 index 0000000000..b283dd585c --- /dev/null +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt @@ -0,0 +1,20 @@ +package com.tangem.features.swap.v2.api.choosetoken + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +interface SwapChooseTokenNetworkComponent : ComposableBottomSheetComponent { + + data class Params( + val userWalletId: UserWalletId, + val initialCurrency: CryptoCurrency, + val token: ManagedCryptoCurrency.Token, + val onDismiss: () -> Unit, + val onResult: (CryptoCurrency) -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt new file mode 100644 index 0000000000..5940a10981 --- /dev/null +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt @@ -0,0 +1,11 @@ +package com.tangem.features.swap.v2.api.choosetoken + +import com.tangem.domain.models.currency.CryptoCurrency + +/** + * Triggers swap token selection + */ +interface SwapChooseTokenNetworkTrigger { + + suspend fun trigger(cryptoCurrency: CryptoCurrency) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt new file mode 100644 index 0000000000..187db962f7 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt @@ -0,0 +1,46 @@ +package com.tangem.features.swap.v2.impl.choosetoken.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkTrigger +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.DefaultSwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.DefaultSwapChooseTokenNetworkTrigger +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.SwapChooseTokenNetworkListener +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenNetworkModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@InstallIn(ModelComponent::class) +@Module +internal interface SwapChooseTokenModule { + + @Binds + @IntoMap + @ClassKey(SwapChooseTokenNetworkModel::class) + fun provideSwapChooseTokenNetworkModel(impl: SwapChooseTokenNetworkModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface SwapChooseTokenModuleBinds { + + @Binds + @Singleton + fun provideSwapChooseTokenNetworksBottomSheetComponent( + impl: DefaultSwapChooseTokenNetworkComponent.Factory, + ): SwapChooseTokenNetworkComponent.Factory + + @Binds + @Singleton + fun bindSwapChooseTokenNetworkTrigger(impl: DefaultSwapChooseTokenNetworkTrigger): SwapChooseTokenNetworkTrigger + + @Binds + @Singleton + fun bindSwapChooseTokenNetworkListener(impl: DefaultSwapChooseTokenNetworkTrigger): SwapChooseTokenNetworkListener +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkComponent.kt new file mode 100644 index 0000000000..b073b75725 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkComponent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenNetworkModel +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.ui.SwapChooseTokenNetworkBottomSheet +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultSwapChooseTokenNetworkComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: SwapChooseTokenNetworkComponent.Params, +) : SwapChooseTokenNetworkComponent, AppComponentContext by context { + + private val model: SwapChooseTokenNetworkModel = getOrCreateModel(params) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.uiState.collectAsStateWithLifecycle() + + SwapChooseTokenNetworkBottomSheet( + config = state.bottomSheetConfig, + ) + } + + @AssistedFactory + interface Factory : SwapChooseTokenNetworkComponent.Factory { + override fun create( + context: AppComponentContext, + params: SwapChooseTokenNetworkComponent.Params, + ): DefaultSwapChooseTokenNetworkComponent + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt new file mode 100644 index 0000000000..887637781f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt @@ -0,0 +1,28 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkTrigger +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Listens to swap token selection + */ +interface SwapChooseTokenNetworkListener { + val swapChooseTokenNetworkResultFlow: SharedFlow +} + +@Singleton +internal class DefaultSwapChooseTokenNetworkTrigger @Inject constructor() : + SwapChooseTokenNetworkTrigger, + SwapChooseTokenNetworkListener { + + override val swapChooseTokenNetworkResultFlow: SharedFlow + field = MutableSharedFlow() + + override suspend fun trigger(result: CryptoCurrency) { + swapChooseTokenNetworkResultFlow.emit(result) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/entity/SwapChooseTokenNetworkUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/entity/SwapChooseTokenNetworkUM.kt new file mode 100644 index 0000000000..202ba8410f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/entity/SwapChooseTokenNetworkUM.kt @@ -0,0 +1,40 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +internal data class SwapChooseTokenNetworkUM( + val bottomSheetConfig: TangemBottomSheetConfig, +) + +@Immutable +internal sealed class SwapChooseTokenNetworkContentUM : TangemBottomSheetConfigContent { + + abstract val messageContent: MessageBottomSheetUMV2 + + data class Loading( + override val messageContent: MessageBottomSheetUMV2, + ) : SwapChooseTokenNetworkContentUM() + + data class Error( + override val messageContent: MessageBottomSheetUMV2, + ) : SwapChooseTokenNetworkContentUM() + + data class Content( + override val messageContent: MessageBottomSheetUMV2, + val swapNetworks: ImmutableList, + ) : SwapChooseTokenNetworkContentUM() +} + +internal data class SwapChooseNetworkUM( + val title: TextReference, + val subtitle: TextReference, + @DrawableRes val iconResId: Int, + val hasFixedRate: Boolean, + val onNetworkClick: () -> Unit, +) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt new file mode 100644 index 0000000000..b7e4427eb3 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt @@ -0,0 +1,26 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model + +import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.swap.v2.impl.R + +internal object SwapChooseTokenFactory { + + fun getErrorMessage(tokenName: String, onDismiss: () -> Unit): MessageBottomSheetUMV2 { + return messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_attention_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.express_swap_not_supported_title, wrappedList(tokenName)) + body = resourceReference(R.string.express_swap_not_supported_text) + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { onDismiss() } + } + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt new file mode 100644 index 0000000000..40bd268b58 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt @@ -0,0 +1,118 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model + +import arrow.core.getOrElse +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase +import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenFactory.getErrorMessage +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.transformers.SwapChooseContentStateTransformer +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.transformers.SwapChooseErrorStateTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.transformer.update +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@ModelScoped +internal class SwapChooseTokenNetworkModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, + private val getSwapSupportedPairsUseCase: GetSwapSupportedPairsUseCase, + private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val uiMessageSender: UiMessageSender, +) : Model() { + + private val params: SwapChooseTokenNetworkComponent.Params = paramsContainer.require() + + val uiState: StateFlow + field: MutableStateFlow = MutableStateFlow( + SwapChooseTokenNetworkUM( + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = params.onDismiss, + content = SwapChooseTokenNetworkContentUM.Loading( + messageContent = getErrorMessage( + tokenName = params.token.name, + onDismiss = params.onDismiss, + ), + ), + ), + ), + ) + + init { + initContent() + } + + private fun initContent() { + val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse { + Timber.e("Failed to get user wallet: $it") + getGenericErrorState() + return + } + + modelScope.launch { + val cryptoCurrencyList = createCryptoCurrencyUseCase( + token = params.token, + userWalletId = params.userWalletId, + ).getOrElse { + Timber.e("Failed to get crypto currency") + getGenericErrorState() + return@launch + } + val pairs = getSwapSupportedPairsUseCase.invoke( + userWallet = userWallet, + initialCurrency = params.initialCurrency, + cryptoCurrencyList = cryptoCurrencyList + params.initialCurrency, + ).getOrElse { + Timber.e(it.toString()) + uiState.update( + SwapChooseErrorStateTransformer( + tokenName = params.token.name, + onDismiss = params.onDismiss, + ), + ) + return@launch + } + delay(MINIMUM_LOADING_TIME) + uiState.update( + SwapChooseContentStateTransformer( + pairs = pairs, + onNetworkClick = params.onResult, + tokenName = params.token.name, + onDismiss = params.onDismiss, + ), + ) + } + } + + private fun getGenericErrorState() { + uiMessageSender.send( + DialogMessage( + title = resourceReference(id = R.string.common_error), + message = resourceReference(id = R.string.common_unknown_error), + onDismissRequest = params.onDismiss, + firstActionBuilder = { okAction() }, + ), + ) + } + + private companion object { + const val MINIMUM_LOADING_TIME = 100L + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt new file mode 100644 index 0000000000..f876199b86 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt @@ -0,0 +1,54 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.transformers + +import com.tangem.core.ui.extensions.iconResId +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseNetworkUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenFactory.getErrorMessage +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class SwapChooseContentStateTransformer( + private val pairs: SwapCurrencies, + private val tokenName: String, + private val onNetworkClick: (CryptoCurrency) -> Unit, + private val onDismiss: () -> Unit, +) : Transformer { + override fun transform(prevState: SwapChooseTokenNetworkUM): SwapChooseTokenNetworkUM { + val swapNetworks = pairs.fromGroup.available.map { + val network = it.currencyStatus.currency.network + SwapChooseNetworkUM( + title = stringReference(network.name), + subtitle = stringReference(network.standardType.name), + iconResId = network.iconResId, + hasFixedRate = it.providers.any { it.rateTypes.any { it == ExpressRateType.Fixed } }, + onNetworkClick = { onNetworkClick(it.currencyStatus.currency) }, + ) + }.toPersistentList() + + return prevState.copy( + bottomSheetConfig = prevState.bottomSheetConfig.copy( + content = if (swapNetworks.isNotEmpty()) { + SwapChooseTokenNetworkContentUM.Content( + swapNetworks = swapNetworks, + messageContent = getErrorMessage( + tokenName = tokenName, + onDismiss = onDismiss, + ), + ) + } else { + SwapChooseTokenNetworkContentUM.Error( + messageContent = getErrorMessage( + tokenName = tokenName, + onDismiss = onDismiss, + ), + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseErrorStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseErrorStateTransformer.kt new file mode 100644 index 0000000000..41ea5a2ea3 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseErrorStateTransformer.kt @@ -0,0 +1,24 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.transformers + +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenFactory.getErrorMessage +import com.tangem.utils.transformer.Transformer + +internal class SwapChooseErrorStateTransformer( + private val tokenName: String, + private val onDismiss: () -> Unit, +) : Transformer { + override fun transform(prevState: SwapChooseTokenNetworkUM): SwapChooseTokenNetworkUM { + return prevState.copy( + bottomSheetConfig = prevState.bottomSheetConfig.copy( + content = SwapChooseTokenNetworkContentUM.Error( + messageContent = getErrorMessage( + tokenName = tokenName, + onDismiss = onDismiss, + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt new file mode 100644 index 0000000000..aa9e2fc590 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt @@ -0,0 +1,301 @@ +package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +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 androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseNetworkUM +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +@Composable +internal fun SwapChooseTokenNetworkBottomSheet(config: TangemBottomSheetConfig) { + TangemModalBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + title = { + AnimatedContent( + targetState = config.content is SwapChooseTokenNetworkContentUM.Content, + ) { isContent -> + TangemModalBottomSheetTitle( + title = resourceReference(R.string.common_choose_network).takeIf { isContent }, + endIconRes = R.drawable.ic_close_24, + onEndClick = config.onDismissRequest, + ) + } + }, + content = { contentConfig -> + SwapChooseTokenNetworkContent(contentConfig) + }, + ) +} + +@Composable +internal fun SwapChooseTokenNetworkContent(state: SwapChooseTokenNetworkContentUM) { + AnimatedContent( + targetState = state, + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + ) { targetState -> + when (targetState) { + is SwapChooseTokenNetworkContentUM.Content -> SwapChooseTokenNetworkContentList(targetState.swapNetworks) + else -> { + Box { + MessageBottomSheetV2Content( + state = targetState.messageContent, + modifier = Modifier + .conditional(targetState is SwapChooseTokenNetworkContentUM.Loading) { + alpha(0f) + }, + ) + if (targetState is SwapChooseTokenNetworkContentUM.Loading) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.inactive, + modifier = Modifier + .align(Alignment.Center), + ) + } + } + } + } + } +} + +@Composable +private fun SwapChooseTokenNetworkContentList(swapNetworks: ImmutableList) { + Column { + swapNetworks.fastForEach { network -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + onClick = { network.onNetworkClick() }, + ) + .padding( + vertical = 12.dp, + horizontal = 14.dp, + ), + ) { + Image( + modifier = Modifier.size(36.dp), + painter = painterResource(id = network.iconResId), + contentDescription = null, + ) + Text( + text = network.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = network.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + if (network.hasFixedRate) { + SpacerWMax() + Text( + text = stringResourceSafe(R.string.swap_fixed_rate), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = RoundedCornerShape(6.dp), + ) + .padding( + vertical = 2.dp, + horizontal = 4.dp, + ), + ) + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun SwapChooseTokenNetworkContent_Preview( + @PreviewParameter(PreviewProvider::class) params: SwapChooseTokenNetworkContentUM, +) { + TangemThemePreview { + SwapChooseTokenNetworkContent(state = params) + } +} + +private class PreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SwapChooseTokenNetworkContentUM.Loading( + messageContent = messageBottomSheetUM { + infoBlock { + icon(R.drawable.ic_alert_triangle_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Unspecified + } + title = stringReference("Shiba Inu is not suppoerted") + body = stringReference("This token is not supported. Please choose a different token to swap.") + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { closeBs() } + } + }, + ), + SwapChooseTokenNetworkContentUM.Error( + messageContent = messageBottomSheetUM { + infoBlock { + icon(R.drawable.ic_alert_triangle_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = stringReference("Shiba Inu is not supported") + body = stringReference("This token is not supported. Please choose a different token to swap.") + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { closeBs() } + } + }, + ), + SwapChooseTokenNetworkContentUM.Content( + messageContent = messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_attention_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = stringReference("Shiba Inu is not suppoerted") + body = stringReference("This token is not supported. Please choose a different token to swap.") + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { closeBs() } + } + }, + swapNetworks = persistentListOf( + SwapChooseNetworkUM( + title = stringReference("Cardano"), + subtitle = stringReference("MAIN"), + iconResId = R.drawable.img_cardano_22, + hasFixedRate = false, + onNetworkClick = {}, + ), + ), + ), + SwapChooseTokenNetworkContentUM.Content( + messageContent = messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_attention_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = stringReference("Shiba Inu is not suppoerted") + body = stringReference("This token is not supported. Please choose a different token to swap.") + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { closeBs() } + } + }, + swapNetworks = persistentListOf( + SwapChooseNetworkUM( + title = stringReference("Cardano"), + subtitle = stringReference("MAIN"), + iconResId = R.drawable.img_cardano_22, + hasFixedRate = false, + onNetworkClick = {}, + ), + SwapChooseNetworkUM( + title = stringReference("BNB Smart Chain"), + subtitle = stringReference("BEP20"), + iconResId = R.drawable.img_bsc_22, + hasFixedRate = false, + onNetworkClick = {}, + ), + SwapChooseNetworkUM( + title = stringReference("BNB Smart Chain"), + subtitle = stringReference("BEP20"), + iconResId = R.drawable.img_bsc_22, + hasFixedRate = true, + onNetworkClick = {}, + ), + SwapChooseNetworkUM( + title = stringReference("BNB Smart Chain"), + subtitle = stringReference("BEP20"), + iconResId = R.drawable.img_bsc_22, + hasFixedRate = false, + onNetworkClick = {}, + ), + ), + ), + SwapChooseTokenNetworkContentUM.Content( + messageContent = messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_attention_20) { + type = MessageBottomSheetUMV2.Icon.Type.Attention + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = stringReference("Shiba Inu is not suppoerted") + body = stringReference("This token is not supported. Please choose a different token to swap.") + } + secondaryButton { + text = resourceReference(R.string.warning_button_ok) + onClick { closeBs() } + } + }, + swapNetworks = buildList { + repeat(10) { + add( + SwapChooseNetworkUM( + title = stringReference("Cardano"), + subtitle = stringReference("MAIN"), + iconResId = R.drawable.img_cardano_22, + hasFixedRate = false, + onNetworkClick = {}, + ), + ) + } + }.toPersistentList(), + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 6da65023c5..03ebfc66ca 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -60,7 +60,7 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) }, topBar = { ExpandableSearchView( - title = stringResourceSafe(R.string.swapping_token_list_title), + title = stringResourceSafe(R.string.common_choose_token), onBackClick = onBack, placeholderSearchText = stringResourceSafe(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered, From ab2f0f58bb1a670858159ca17c5beeeedbbc2ed2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 23 Jun 2025 13:58:27 +0400 Subject: [PATCH 45/99] Updated on 2026-08-14 --- .../connections/utils/WcAlertsFactory.kt | 43 ++++++++++++++++--- .../model/WcSendTransactionModel.kt | 43 +++++++++++++++++-- .../transaction/routes/WcTransactionRoutes.kt | 3 +- .../blockaid/WcEstimatedWalletChangesItem.kt | 8 +++- 4 files changed, 85 insertions(+), 12 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt index 723b7046bf..75e7d6143e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt @@ -10,11 +10,16 @@ import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal object WcAlertsFactory { fun createCommonTransactionAppInfoAlertUM(alertType: WcTransactionRoutes.Alert.Type) = when (alertType) { - is WcTransactionRoutes.Alert.Type.Verified -> createVerifiedDomainAlert(alertType.appName) - is WcTransactionRoutes.Alert.Type.UnknownDomain -> createUnknownDomainAlert() - is WcTransactionRoutes.Alert.Type.UnsafeDomain -> createUnsafeDomainAlert() - is WcTransactionRoutes.Alert.Type.MaliciousDApp -> + is WcTransactionRoutes.Alert.Type.Verified -> + createVerifiedDomainAlert(alertType.appName) + is WcTransactionRoutes.Alert.Type.UnknownDomain -> + createUnknownDomainAlert() + is WcTransactionRoutes.Alert.Type.UnsafeDomain -> + createUnsafeDomainAlert() + is WcTransactionRoutes.Alert.Type.MaliciousInfo -> createMaliciousDAppAlert(alertType.description, alertType.onClick) + is WcTransactionRoutes.Alert.Type.UnknownError -> + createUnknownErrorAlert(alertType.errorMessage, alertType.onDismiss) } fun createUnknownDomainAlert(activeButtonOnClick: (() -> Unit)? = null): MessageBottomSheetUMV2 { @@ -69,8 +74,32 @@ internal object WcAlertsFactory { } } + private fun createUnknownErrorAlert(errorMessage: String?, onDismiss: () -> Unit): MessageBottomSheetUMV2 { + return messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_attention_20) { + type = MessageBottomSheetUMV2.Icon.Type.Warning + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.wc_alert_unknown_error_title) + body = if (errorMessage.isNullOrEmpty()) { + resourceReference(R.string.wc_alert_unknown_error_description_no_error_code) + } else { + resourceReference( + R.string.wc_alert_unknown_error_description, + wrappedList(errorMessage), + ) + } + } + secondaryButton { + text = resourceReference(R.string.balance_hidden_got_it_button) + onClick { onDismiss() } + } + } + } + private fun createMaliciousDAppAlert( - description: String, + description: String?, activeButtonOnClick: (() -> Unit), ): MessageBottomSheetUMV2 { return messageBottomSheetUM { @@ -80,7 +109,9 @@ internal object WcAlertsFactory { backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint } title = resourceReference(R.string.security_alert_title) - body = TextReference.Str(description) + if (!description.isNullOrEmpty()) { + body = TextReference.Str(description) + } } primaryButton { text = resourceReference(R.string.common_cancel) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 4d8311c79c..3e745c13f9 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -1,8 +1,11 @@ package com.tangem.features.walletconnect.transaction.model import androidx.compose.runtime.Stable +import arrow.core.Either import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.pushNew +import com.domain.blockaid.models.transaction.ValidationResult import com.tangem.blockchain.common.TransactionData import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -48,6 +51,7 @@ internal class WcSendTransactionModel @Inject constructor( val stackNavigation = StackNavigation() private var wcApproval: WcApproval? = null + private var sign: () -> Unit = {} init { @Suppress("UnusedPrivateMember") @@ -66,7 +70,7 @@ internal class WcSendTransactionModel @Inject constructor( .distinctUntilChanged() .collectLatest { val (signState, securityCheck) = it - if (signingIsDone(signState)) return@collectLatest + if (signingIsDone(signState, useCase)) return@collectLatest val signModel: Any = signState.signModel val isMutableFee = useCase is WcMutableFee val dAppFee = if (isMutableFee) useCase.dAppFee() else null @@ -81,6 +85,7 @@ internal class WcSendTransactionModel @Inject constructor( var isApprovalMethod = isSecurityCheckContent && securityCheck.content is BlockAidTransactionCheck.Result.Approval wcApproval = useCase as? WcApproval + sign = { useCase.sign() } buildUiState(securityCheck, useCase, signState) } else -> unknownMethodRunnable() @@ -107,7 +112,7 @@ internal class WcSendTransactionModel @Inject constructor( actions = WcTransactionActionsUM( onShowVerifiedAlert = ::showVerifiedAlert, onDismiss = { cancel(useCase) }, - onSign = useCase::sign, + onSign = { onSign(securityCheck.getOrNull()) }, onCopy = { copyData(useCase.rawSdkRequest.request.params) }, ), ), @@ -127,6 +132,14 @@ internal class WcSendTransactionModel @Inject constructor( stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo) } + private fun onSign(securityCheck: BlockAidTransactionCheck.Result?) { + if (securityCheck?.result?.validation == ValidationResult.UNSAFE) { + showMaliciousAlert(securityCheck.result.description) + } else { + sign() + } + } + fun onClickDoneCustomAllowance(value: BigDecimal, isUnlimited: Boolean) { val maxValue = if (isUnlimited) Double.MAX_VALUE.toBigDecimal() else value wcApproval?.getAmount()?.let { currentAmount -> @@ -142,14 +155,36 @@ internal class WcSendTransactionModel @Inject constructor( stackNavigation.pushNew(WcTransactionRoutes.Alert(WcTransactionRoutes.Alert.Type.Verified(appName))) } - private fun signingIsDone(signState: WcSignState<*>): Boolean { + private fun showMaliciousAlert(description: String?) { + val type = WcTransactionRoutes.Alert.Type.MaliciousInfo(description = description, onClick = ::signFromAlert) + stackNavigation.pushNew(WcTransactionRoutes.Alert(type)) + } + + private fun signFromAlert() { + stackNavigation.pop() + sign() + } + + private fun signingIsDone(signState: WcSignState<*>, useCase: WcSignUseCase<*>): Boolean { (signState.domainStep as? WcSignStep.Result)?.result?.let { - router.pop() + handleSigningError(it, useCase) return true } return false } + private fun handleSigningError(result: Either, useCase: WcSignUseCase<*>) { + if (result.isLeft()) { + val error = WcTransactionRoutes.Alert.Type.UnknownError( + errorMessage = result.leftOrNull()?.message, + onDismiss = { cancel(useCase) }, + ) + stackNavigation.pushNew(WcTransactionRoutes.Alert(error)) + } else { + router.pop() + } + } + private fun cancel(useCase: WcSignUseCase<*>) { useCase.cancel() router.pop() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt index b08fafc300..066a19eebc 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt @@ -24,7 +24,8 @@ internal sealed class WcTransactionRoutes : TangemBottomSheetConfigContent, Rout data class Verified(val appName: String) : Type() data object UnknownDomain : Type() data object UnsafeDomain : Type() - data class MaliciousDApp(val description: String, val onClick: () -> Unit) : Type() + data class MaliciousInfo(val description: String?, val onClick: () -> Unit) : Type() + data class UnknownError(val errorMessage: String?, val onDismiss: () -> Unit) : Type() } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt index fb730d52bb..25e8b53eba 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt @@ -4,6 +4,7 @@ import android.content.res.Configuration import androidx.compose.animation.animateContentSize 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.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -68,9 +69,14 @@ internal fun WcEstimatedWalletChangesItem(item: WcEstimatedWalletChangesUM, modi } if (item.items.size > MAX_CHANGES_SIZE) { + val interactionSource = remember { MutableInteractionSource() } Row( modifier = Modifier - .clickable { isExpanded = !isExpanded } + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = { isExpanded = !isExpanded }, + ) .padding(bottom = 12.dp, start = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { From 22ad2ddc6739d137ccf16b6b7ee5fc0f617dd618 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 1 Jun 2025 15:24:59 +0700 Subject: [PATCH 46/99] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 12 +++++ .../common/ui/tokens/TokenActionsUtils.kt | 4 ++ core/res/src/main/res/values-de/strings.xml | 4 +- core/res/src/main/res/values-ja/strings.xml | 4 +- core/res/src/main/res/values-ru/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 4 +- .../DefaultCustomTokensRepository.kt | 1 + .../SdkRequirementsConditionConverter.kt | 6 ++- .../model/ScenarioUnavailabilityReason.kt | 2 + .../analytics/TokenScreenAnalyticsEvent.kt | 2 + .../model/warnings/CryptoCurrencyWarning.kt | 7 +++ .../tokens/GetCryptoCurrencyActionsUseCase.kt | 14 +++--- .../tokens/GetCurrencyWarningsUseCase.kt | 6 +++ .../models/AssetRequirementsCondition.kt | 9 ++++ .../transaction/error/OpenTrustlineError.kt | 10 +++++ .../usecase/OpenTrustlineUseCase.kt | 44 +++++++++++++++++++ ...okenDetailsNotificationsAnalyticsSender.kt | 1 + .../model/TokenDetailsClickIntents.kt | 2 + .../tokendetails/model/TokenDetailsModel.kt | 34 ++++++++++++++ .../components/TokenDetailsNotification.kt | 18 ++++++++ .../TokenDetailsNotificationConverter.kt | 12 +++++ .../state/factory/TokenDetailsStateFactory.kt | 5 +++ 22 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/error/OpenTrustlineError.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/OpenTrustlineUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index ea48365cb7..8be21bb8cf 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -79,6 +79,18 @@ internal object TransactionDomainModule { ) } + @Provides + @Singleton + fun provideOpenTrustlineUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + walletManagersFacade: WalletManagersFacade, + ): OpenTrustlineUseCase { + return OpenTrustlineUseCase( + cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade, + ) + } + @Provides @Singleton fun provideDismissIncompleteTransactionUseCase( diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt index 7337dccc5d..374cbde833 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +@Suppress("CyclomaticComplexMethod") fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { return when (val unavailabilityReason = this) { is ScenarioUnavailabilityReason.StakingUnavailable -> { @@ -46,6 +47,9 @@ fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( id = R.string.warning_receive_blocked_hedera_token_association_required_message, ) + ScenarioUnavailabilityReason.TrustlineRequired -> resourceReference( + id = R.string.warning_receive_blocked_token_trustline_required_message, + ) ScenarioUnavailabilityReason.UsedOutdatedData -> { resourceReference(id = R.string.token_button_unavailability_reason_out_of_date_balance) } diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 24c3cc5827..e90c8e89e2 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1299,8 +1299,8 @@ Der Kontostand ist möglicherweise veraltet. Aktualisiere bittre die Seite. Nicht genug %1$s. Lade Dein XLM-Konto auf, um dieses Token zu verknüpfen Trustline aktivieren - Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s. - Trustline erforderlich + Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s. + Trustline erforderlich Unbekannte Domäne Trotzdem verbinden Zeitüberschreitungsfehler. Bitte versuche es später erneut. diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 722f628412..11cb1fb068 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1279,8 +1279,8 @@ 残高が古い可能性があります。ページを更新してください。 %1$sが足りません。このトークンを関連付けるには、XLMアカウントにチャージしてください。 トラストラインを有効にする - このトークンを受け取るには、トラストラインを有効にする必要があります。ネットワークには%1$s %2$s予備金が必要です。 - トラストラインが必要 + このトークンを受け取るには、トラストラインを有効にする必要があります。ネットワークには%1$s %2$s予備金が必要です。 + トラストラインが必要 不明なドメイン とにかく接続する タイムアウトエラーが発生しました。しばらくしてからもう一度お試しください。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 6be2e1fb65..8ac257b580 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1263,8 +1263,8 @@ Только для целей тестирования Возможно, баланс устарел. Обновите страницу. Открыть Trustline - Согласно правилам сети, чтобы пополнить этот токен, необходимо сначала открыть Trustline — это позволит вашему кошельку принимать и хранить этот актив. - Откройте Trustline + Согласно правилам сети, чтобы пополнить этот токен, необходимо сначала открыть Trustline — это позволит вашему кошельку принимать и хранить этот актив. + Откройте Trustline Пожалуйста, вернитесь в браузер и выполните повторное подключение через WalletConnect. Выбрана не верная карта или кольцо Адрес diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 59973b51ac..46db541274 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1344,8 +1344,8 @@ Balance may be outdated. Refresh the page. Not enough %1$s. Top up your XLM account to associate this token Enable Trustline - A Trustline must be enabled to receive this token. The network requires a %1$s %2$s reserve. - Trustline Required + A Trustline must be enabled to receive this token. The network requires a %1$s %2$s reserve. + Trustline Required Unknown domain Connect anyway Timeout error. Please, try again later. diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index b426db7892..b3250351dd 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -57,6 +57,7 @@ internal class DefaultCustomTokensRepository( -> true Blockchain.Cardano, Blockchain.Sui, + Blockchain.Stellar, -> blockchain.validateContractAddress(contractAddress) else -> blockchain.validateAddress(contractAddress) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt index 1e93352c42..f2cb03313a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt @@ -8,7 +8,11 @@ internal class SdkRequirementsConditionConverter : Converter AssetRequirementsCondition.PaidTransaction - is SdkRequirementsCondition.RequiredTrustline -> TODO("added in [REDACTED_TASK_KEY]") + is SdkRequirementsCondition.RequiredTrustline -> AssetRequirementsCondition.RequiredTrustline( + requiredAmount = requireNotNull(value.amount.value), + currencySymbol = value.amount.currencySymbol, + decimals = value.amount.decimals, + ) is SdkRequirementsCondition.PaidTransactionWithFee -> AssetRequirementsCondition.PaidTransactionWithFee( feeAmount = requireNotNull(value.feeAmount.value), feeCurrencySymbol = value.feeAmount.currencySymbol, diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt index 02f95ce1e1..bf816df605 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -54,6 +54,8 @@ sealed class ScenarioUnavailabilityReason { data object UnassociatedAsset : ScenarioUnavailabilityReason() + data object TrustlineRequired : ScenarioUnavailabilityReason() + enum class WithdrawalScenario { SELL, SEND // TODO staking create&process STAKING } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index 844923dee0..54eac28f44 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -167,6 +167,7 @@ sealed class TokenScreenAnalyticsEvent( private const val SINGLE_WALLET = "Single Wallet" private const val LOADING = "Loading" private const val ASSET_REQUIREMENT = "AssetRequirement" + private const val TRUSTLINE_REQUIREMENT = "TrustlineRequirement" @Suppress("CyclomaticComplexMethod") fun ScenarioUnavailabilityReason.toReasonAnalyticsText(): String { @@ -177,6 +178,7 @@ sealed class TokenScreenAnalyticsEvent( is ScenarioUnavailabilityReason.StakingUnavailable, -> UNAVAILABLE ScenarioUnavailabilityReason.UnassociatedAsset -> ASSET_REQUIREMENT + ScenarioUnavailabilityReason.TrustlineRequired -> TRUSTLINE_REQUIREMENT is ScenarioUnavailabilityReason.EmptyBalance -> EMPTY is ScenarioUnavailabilityReason.PendingTransaction -> PENDING ScenarioUnavailabilityReason.UsedOutdatedData -> CACHING diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index f76e272a6b..083b0aba95 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -61,4 +61,11 @@ sealed class CryptoCurrencyWarning { ) : CryptoCurrencyWarning() data object UsedOutdatedDataWarning : CryptoCurrencyWarning() + + data class RequiredTrustline( + val currency: CryptoCurrency, + val currencySymbol: String, + val requiredAmount: BigDecimal, + val currencyDecimals: Int, + ) : CryptoCurrencyWarning() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 77f16153ff..1e1a09d0d2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -375,12 +375,14 @@ class GetCryptoCurrencyActionsUseCase( } private fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason { - return if (requirements is AssetRequirementsCondition.PaidTransaction || - requirements is AssetRequirementsCondition.PaidTransactionWithFee - ) { - ScenarioUnavailabilityReason.UnassociatedAsset - } else { - ScenarioUnavailabilityReason.None + return when (requirements) { + AssetRequirementsCondition.PaidTransaction, + is AssetRequirementsCondition.PaidTransactionWithFee, + -> ScenarioUnavailabilityReason.UnassociatedAsset + is AssetRequirementsCondition.IncompleteTransaction, + null, + -> ScenarioUnavailabilityReason.None + is AssetRequirementsCondition.RequiredTrustline -> ScenarioUnavailabilityReason.TrustlineRequired } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index fbc6e1efdc..b02e4b6dda 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -222,6 +222,12 @@ class GetCurrencyWarningsUseCase( ): CryptoCurrencyWarning? { return when (val requirements = walletManagersFacade.getAssetRequirements(userWalletId, currency)) { is AssetRequirementsCondition.PaidTransaction -> HederaWarnings.AssociateWarning(currency = currency) + is AssetRequirementsCondition.RequiredTrustline -> CryptoCurrencyWarning.RequiredTrustline( + currency = currency, + requiredAmount = requirements.requiredAmount, + currencyDecimals = requirements.decimals, + currencySymbol = requirements.currencySymbol, + ) is AssetRequirementsCondition.PaidTransactionWithFee -> { HederaWarnings.AssociateWarningWithFee( currency = currency, diff --git a/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt index 2bb5c1d8fb..5826a800ab 100644 --- a/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt +++ b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt @@ -9,6 +9,15 @@ sealed class AssetRequirementsCondition { */ data object PaidTransaction : AssetRequirementsCondition() + /** + * Trustlines are an explicit opt-in for an account to hold a particular asset. + */ + data class RequiredTrustline( + val requiredAmount: BigDecimal, + val currencySymbol: String, + val decimals: Int, + ) : AssetRequirementsCondition() + /** * The exact value of the fee for this type of condition is stored in `feeAmount`. */ diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/OpenTrustlineError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/OpenTrustlineError.kt new file mode 100644 index 0000000000..a71270bf29 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/OpenTrustlineError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.transaction.error + +import java.math.BigDecimal + +sealed interface OpenTrustlineError { + val message: String? + + data class SomeError(override val message: String?) : OpenTrustlineError + data class NotEnoughCoin(val amount: BigDecimal, override val message: String?) : OpenTrustlineError +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/OpenTrustlineUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/OpenTrustlineUseCase.kt new file mode 100644 index 0000000000..1e3a9655a9 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/OpenTrustlineUseCase.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.transaction.error.OpenTrustlineError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId + +class OpenTrustlineUseCase( + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val walletManagersFacade: WalletManagersFacade, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Either { + return either { + val signer = cardSdkConfigRepository.getCommonSigner( + cardId = null, + twinKey = null, // use null here because no assets support for Twin cards + ) + + catch( + block = { + when (val result = walletManagersFacade.fulfillRequirements(userWalletId, currency, signer)) { + is SimpleResult.Failure -> when (val error = result.error) { + is BlockchainSdkError.Stellar.MinReserveRequired -> + raise(OpenTrustlineError.NotEnoughCoin(error.amount, result.error.customMessage)) + else -> raise(OpenTrustlineError.SomeError(result.error.customMessage)) + } + SimpleResult.Success -> Unit + } + }, + catch = { error -> raise(OpenTrustlineError.SomeError(error.message)) }, + ) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index b28e14d171..dbe31e24b9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -51,6 +51,7 @@ internal class TokenDetailsNotificationsAnalyticsSender( is TokenDetailsNotification.RentInfo, is TokenDetailsNotification.NetworkShutdown, is TokenDetailsNotification.HederaAssociateWarning, + is TokenDetailsNotification.RequiredTrustlineWarning, is TokenDetailsNotification.KoinosMana, is TokenDetailsNotification.MigrationMaticToPol, is TokenDetailsNotification.UsedOutdatedData, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt index 6aab0064c7..e0e6cd1566 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt @@ -62,6 +62,8 @@ interface TokenDetailsClickIntents { fun onRetryIncompleteTransactionClick() + fun onOpenTrustlineClick() + fun onDismissIncompleteTransactionClick() fun onConfirmDismissIncompleteTransactionClick() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 976ace101c..d2d966d302 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -56,9 +56,11 @@ import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Compan import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.transaction.error.AssociateAssetError import com.tangem.domain.transaction.error.IncompleteTransactionError +import com.tangem.domain.transaction.error.OpenTrustlineError import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.AssociateAssetUseCase import com.tangem.domain.transaction.usecase.DismissIncompleteTransactionUseCase +import com.tangem.domain.transaction.usecase.OpenTrustlineUseCase import com.tangem.domain.transaction.usecase.RetryIncompleteTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase @@ -116,6 +118,7 @@ internal class TokenDetailsModel @Inject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val associateAssetUseCase: AssociateAssetUseCase, private val retryIncompleteTransactionUseCase: RetryIncompleteTransactionUseCase, + private val openTrustlineUseCase: OpenTrustlineUseCase, private val dismissIncompleteTransactionUseCase: DismissIncompleteTransactionUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, @@ -893,6 +896,37 @@ internal class TokenDetailsModel @Inject constructor( } } + override fun onOpenTrustlineClick() { + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.Associate( + tokenSymbol = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) + modelScope.launch(dispatchers.mainImmediate) { + openTrustlineUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + ).fold( + ifLeft = { e -> + Timber.e(e.message) + internalUiState.value = when (e) { + is OpenTrustlineError.SomeError -> + stateFactory.getStateWithErrorDialog(stringReference(e.message.orEmpty())) + is OpenTrustlineError.NotEnoughCoin -> { + val text = resourceReference( + id = R.string.warning_token_required_min_coin_reserve, + formatArgs = wrappedList(e.amount), + ) + stateFactory.getStateWithErrorDialog(text) + } + } + }, + ifRight = { internalUiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() }, + ) + } + } + override fun onDismissIncompleteTransactionClick() { analyticsEventsHandler.send( TokenScreenAnalyticsEvent.RevealCancel( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 74eaebd01d..297bc67dfc 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -193,6 +193,24 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) + data class RequiredTrustlineWarning( + private val currency: CryptoCurrency, + private val amount: String, + private val currencySymbol: String, + private val onOpenClick: () -> Unit, + ) : Warning( + title = resourceReference(id = R.string.warning_token_trustline_title), + subtitle = resourceReference( + id = R.string.warning_token_trustline_subtitle, + formatArgs = wrappedList(amount, currencySymbol), + ), + iconResId = currency.networkIconResId, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_token_trustline_button_title), + onClick = onOpenClick, + ), + ) + data class KaspaIncompleteTransactionWarning( private val currency: CryptoCurrency, private val amount: String, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index c6faec47ad..67215ebe35 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -42,6 +42,12 @@ internal class TokenDetailsNotificationConverter( return newNotifications.toImmutableList() } + fun removeRequiredTrustlineWarning(currentState: TokenDetailsState): ImmutableList { + val newNotifications = currentState.notifications.toMutableList() + newNotifications.removeBy { it is RequiredTrustlineWarning } + return newNotifications.toImmutableList() + } + fun removeKaspaIncompleteTransactionWarning( currentState: TokenDetailsState, ): ImmutableList { @@ -118,6 +124,12 @@ internal class TokenDetailsNotificationConverter( feeCurrencySymbol = warning.feeCurrencySymbol, onAssociateClick = clickIntents::onAssociateClick, ) + is CryptoCurrencyWarning.RequiredTrustline -> RequiredTrustlineWarning( + currency = warning.currency, + amount = warning.requiredAmount.format { crypto(symbol = "", warning.currencyDecimals) }, + currencySymbol = warning.currencySymbol, + onOpenClick = clickIntents::onOpenTrustlineClick, + ) is KaspaWarnings.IncompleteTransaction -> KaspaIncompleteTransactionWarning( currency = warning.currency, amount = warning.amount.format { crypto(symbol = "", decimals = warning.currencyDecimals) }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 222804e41b..4819797954 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -334,6 +334,11 @@ internal class TokenDetailsStateFactory( return state.copy(notifications = notificationConverter.removeHederaAssociateWarning(state)) } + fun getStateWithRemovedRequiredTrustlineNotification(): TokenDetailsState { + val state = currentStateProvider() + return state.copy(notifications = notificationConverter.removeRequiredTrustlineWarning(state)) + } + fun getStateWithRemovedKaspaIncompleteTransactionNotification(): TokenDetailsState { val state = currentStateProvider() return state.copy( From 281b5ec956ba95109d95b6126ea55f613d3de8ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Jun 2025 14:43:38 +0500 Subject: [PATCH 47/99] Updated on 2026-08-14 --- .../ui/amountScreen/ui/AmountBlockV2.kt | 180 ++++++++++++++++++ core/res/src/main/res/values/strings.xml | 4 + core/ui/src/main/res/drawable/ic_scan_16.xml | 13 +- .../send/v2/send/DefaultSendComponent.kt | 2 + .../v2/send/confirm/SendConfirmComponent.kt | 1 + .../v2/send/confirm/model/SendConfirmModel.kt | 7 +- .../v2/send/confirm/ui/SendConfirmContent.kt | 9 +- .../features/send/v2/send/model/SendModel.kt | 1 + .../features/send/v2/send/ui/state/SendUM.kt | 1 + .../amount/SendAmountBlockComponent.kt | 26 ++- .../amount/SendAmountComponentParams.kt | 3 + .../SendDestinationBlockComponent.kt | 1 + .../destination/ui/DestinationBlock.kt | 143 +++++++++++++- 13 files changed, 371 insertions(+), 20 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt new file mode 100644 index 0000000000..611e613b72 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt @@ -0,0 +1,180 @@ +package com.tangem.common.ui.amountScreen.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun AmountBlockV2( + amountState: AmountState, + currencyIconState: CurrencyIconState, + isClickDisabled: Boolean, + isEditingDisabled: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (amountState !is AmountState.Data) return + val amount = amountState.amountTextField + + val cryptoAmount = amount.cryptoAmount.value?.format { + crypto( + symbol = "", + decimals = amount.cryptoAmount.decimals, + ) + }.orEmpty() + + val fiatAmount = amount.fiatAmount.value.format { + fiat( + fiatCurrencySymbol = amount.fiatAmount.currencySymbol, + fiatCurrencyCode = amountState.appCurrency.code, + ) + } + + val (firstAmount, secondAmount) = if (amount.isFiatValue) { + fiatAmount to cryptoAmount + } else { + cryptoAmount to fiatAmount + } + + val title = TextReference.Str( + stringResourceSafe( + R.string.send_from_wallet_name, + amountState.title + .resolveReference(), + ), + ) + val currencyTitle = amount.cryptoAmount.currencySymbol + + AmountBlockV2( + title = title, + balance = amountState.availableBalance, + currencyTitle = currencyTitle, + currencyIconState = currencyIconState, + firstAmount = firstAmount, + secondAmount = secondAmount, + isClickDisabled = isClickDisabled, + isEditingDisabled = isEditingDisabled, + onClick = onClick, + modifier = modifier, + ) +} + +@Suppress("LongParameterList") +@Composable +internal fun AmountBlockV2( + title: TextReference, + balance: TextReference, + currencyTitle: String, + currencyIconState: CurrencyIconState, + firstAmount: String, + secondAmount: String, + isClickDisabled: Boolean, + isEditingDisabled: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) + .padding(TangemTheme.dimens.spacing16), + ) { + Row { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + Spacer(modifier = modifier.weight(1f)) + Text( + text = balance.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(top = 8.dp), + ) { + Text( + text = firstAmount, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = secondAmount, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(top = 6.dp), + ) { + CurrencyIcon( + state = currencyIconState, + modifier = Modifier.padding(horizontal = 14.dp), + ) + Text( + text = currencyTitle, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + } + } + } +} + +// region Preview +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AmountBlockPreview(@PreviewParameter(AmountBlockV2PreviewProvider::class) value: AmountState) { + TangemThemePreview { + AmountBlockV2( + amountState = value, + currencyIconState = CurrencyIconState.Empty(), + isClickDisabled = false, + isEditingDisabled = false, + onClick = {}, + ) + } +} + +private class AmountBlockV2PreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + AmountStatePreviewData.amountState, + ) +} +// endregion \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 46db541274..fc05ef9a38 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -822,6 +822,7 @@ Check your network connection Network fee info unreachable From + From %s Gas limit This is the maximum amount of gas that will be spent to complete a transaction or contract. A gas limit prevents unexpected or unlimited charges when executing a transaction. Gas price @@ -829,6 +830,7 @@ Max Maximum amount Fee up to + Memo: %s Invalid Memo Network fee coverage Nonce @@ -879,6 +881,7 @@ network fee will be covered by using %1$s energy network fee will be reduced by spending %1$s energy including a network fee of %1$s + To address Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account. Amount exceeds balance @@ -888,6 +891,7 @@ Fee exceeds balance Total amount exceeds balance Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. + Will be sent a recipient Send with swap Transaction sent Prepare to scan card or ring you want to set up. diff --git a/core/ui/src/main/res/drawable/ic_scan_16.xml b/core/ui/src/main/res/drawable/ic_scan_16.xml index 16a9aa2ffb..21e35b19c6 100644 --- a/core/ui/src/main/res/drawable/ic_scan_16.xml +++ b/core/ui/src/main/res/drawable/ic_scan_16.xml @@ -1,10 +1,5 @@ - - + + + + diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 36b57ddd7c..d38c5b42a8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -206,6 +206,7 @@ internal class DefaultSendComponent @AssistedInject constructor( childStack = childStack, ) }, + isRedesignEnabled = model.uiState.value.isRedesignEnabled, ), ) } else { @@ -225,6 +226,7 @@ internal class DefaultSendComponent @AssistedInject constructor( return if (sendAmount != null && destinationAddress != null && feeCryptoCurrencyStatus != null && cryptoCurrencyStatus != null ) { + // TODO Apply new component [REDACTED_TASK_KEY] SendFeeComponent( appComponentContext = factoryContext, params = SendFeeComponentParams.FeeParams( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index a7145d5d6e..9a34d59c65 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -65,6 +65,7 @@ internal class SendConfirmComponent( appCurrency = params.appCurrency, blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), predefinedValues = params.predefinedValues, + isRedesignEnabled = model.uiState.value.isRedesignEnabled, ), onResult = model::onAmountResult, onClick = model::showEditAmount, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index d5dcfadac4..a84b4a3fc7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -454,6 +454,7 @@ internal class SendConfirmModel @Inject constructor( } } + @Suppress("LongMethod") private fun configConfirmNavigation() { combine( flow = uiState, @@ -470,7 +471,11 @@ internal class SendConfirmModel @Inject constructor( id = R.string.send_summary_title, formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name), ), - subtitle = amountUM?.title, + subtitle = if (uiState.value.isRedesignEnabled) { + null + } else { + amountUM?.title + }, backIconRes = R.drawable.ic_close_24, backIconClick = { analyticsEventHandler.send( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index 4f13d834a7..a6162f90f2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -98,8 +98,13 @@ private fun LazyListScope.blocks( modifier = Modifier.padding(vertical = 12.dp), ) } - destinationBlockComponent.Content(modifier = Modifier) - amountBlockComponent.Content(modifier = Modifier) + if (uiState.isRedesignEnabled) { + amountBlockComponent.Content(modifier = Modifier) + destinationBlockComponent.Content(modifier = Modifier) + } else { + destinationBlockComponent.Content(modifier = Modifier) + amountBlockComponent.Content(modifier = Modifier) + } feeBlockComponent.Content(modifier = Modifier) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 9aa5fe4c78..798a2abda2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -364,5 +364,6 @@ internal class SendModel @Inject constructor( feeUM = FeeUM.Empty(), confirmUM = ConfirmUM.Empty, navigationUM = NavigationUM.Empty, + isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt index 8359df5247..a84a77e878 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt @@ -12,4 +12,5 @@ internal data class SendUM( val feeUM: FeeUM, val confirmUM: ConfirmUM, val navigationUM: NavigationUM, + val isRedesignEnabled: Boolean, ) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt index b05e56c031..7bf133c215 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.ui.AmountBlock +import com.tangem.common.ui.amountScreen.ui.AmountBlockV2 import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent @@ -37,11 +38,24 @@ internal class SendAmountBlockComponent( val state by model.uiState.collectAsStateWithLifecycle() val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle() - AmountBlock( - amountState = state, - isClickDisabled = !isClickEnabled, - isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink, - onClick = onClick, - ) + if (params.isRedesignEnabled) { + val amountState = state as? AmountState.Data ?: return + + AmountBlockV2( + amountState = state, + currencyIconState = amountState.tokenIconState, + isClickDisabled = !isClickEnabled, + isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink, + onClick = onClick, + modifier = modifier, + ) + } else { + AmountBlock( + amountState = state, + isClickDisabled = !isClickEnabled, + isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink, + onClick = onClick, + ) + } } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt index 7a59f9cfde..a13dcf3ef1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt @@ -18,6 +18,7 @@ internal sealed class SendAmountComponentParams { abstract val appCurrency: AppCurrency abstract val cryptoCurrencyStatus: CryptoCurrencyStatus abstract val predefinedValues: PredefinedValues + abstract val isRedesignEnabled: Boolean data class AmountParams( override val state: AmountState, @@ -26,6 +27,7 @@ internal sealed class SendAmountComponentParams { override val appCurrency: AppCurrency, override val cryptoCurrencyStatus: CryptoCurrencyStatus, override val predefinedValues: PredefinedValues, + override val isRedesignEnabled: Boolean, val callback: ModelCallback, val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, @@ -40,6 +42,7 @@ internal sealed class SendAmountComponentParams { override val appCurrency: AppCurrency, override val cryptoCurrencyStatus: CryptoCurrencyStatus, override val predefinedValues: PredefinedValues, + override val isRedesignEnabled: Boolean, val blockClickEnableFlow: StateFlow, ) : SendAmountComponentParams() } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationBlockComponent.kt index 5f500027fa..f95975b12b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationBlockComponent.kt @@ -40,6 +40,7 @@ internal class SendDestinationBlockComponent( destinationUM = state, isClickDisabled = !isClickEnabled, isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink, + isRedesignEnabled = (params.state as? DestinationUM.Content)?.isRedesignEnabled ?: false, onClick = onClick, ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt index 9fe2722f12..3debd43e2f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt @@ -1,26 +1,38 @@ package com.tangem.features.send.v2.subcomponents.destination.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.HorizontalDivider 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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import kotlinx.collections.immutable.toImmutableList @Composable internal fun DestinationBlock( destinationUM: DestinationUM, isClickDisabled: Boolean, isEditingDisabled: Boolean, + isRedesignEnabled: Boolean, onClick: () -> Unit, ) { if (destinationUM !is DestinationUM.Content) return @@ -33,8 +45,15 @@ internal fun DestinationBlock( .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing12), ) { - AddressBlock(destinationUM.addressTextField) - MemoBlock(destinationUM.memoTextField) + if (isRedesignEnabled) { + AddressWithMemoBlock( + address = destinationUM.addressTextField, + memo = destinationUM.memoTextField, + ) + } else { + AddressBlock(destinationUM.addressTextField) + MemoBlock(destinationUM.memoTextField) + } } } @@ -84,4 +103,124 @@ private fun MemoBlock(memo: DestinationTextFieldUM.RecipientMemo?) { modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) } +} + +@Composable +private fun AddressWithMemoBlock( + address: DestinationTextFieldUM.RecipientAddress, + memo: DestinationTextFieldUM.RecipientMemo?, +) { + Text( + text = stringResourceSafe(R.string.send_to_address), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) { + Text( + modifier = Modifier.weight(1f), + text = address.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + IdentIcon( + address = address.value, + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius18)) + .background(TangemTheme.colors.background.tertiary), + ) + } + if (memo != null && memo.value.isNotBlank()) { + Text( + text = stringResourceSafe(R.string.send_memo, memo.value), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DestinationBlockPreview( + @PreviewParameter(DestinationBlockPreviewProvider::class) state: DestinationUM.Content, +) { + TangemThemePreview { + DestinationBlock( + destinationUM = state, + isClickDisabled = false, + isEditingDisabled = false, + isRedesignEnabled = state.isRedesignEnabled, + onClick = {}, + ) + } +} + +private class DestinationBlockPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + DestinationUM.Content( + isPrimaryButtonEnabled = true, + addressTextField = DestinationTextFieldUM.RecipientAddress( + value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + keyboardOptions = KeyboardOptions.Default, + placeholder = TextReference.Str("Enter address"), + label = TextReference.Str("Recipient Address"), + isError = false, + error = null, + isValuePasted = false, + ), + memoTextField = DestinationTextFieldUM.RecipientMemo( + value = "Test memo for transaction", + keyboardOptions = KeyboardOptions.Default, + placeholder = TextReference.Str("Enter memo (optional)"), + label = TextReference.Str("Memo"), + isError = false, + error = null, + disabledText = TextReference.Str("Memo disabled"), + isEnabled = true, + isValuePasted = false, + ), + recent = emptyList().toImmutableList(), + wallets = emptyList().toImmutableList(), + networkName = "Ethereum", + isValidating = false, + isInitialized = true, + isRedesignEnabled = false, + ), + DestinationUM.Content( + isPrimaryButtonEnabled = true, + addressTextField = DestinationTextFieldUM.RecipientAddress( + value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + keyboardOptions = KeyboardOptions.Default, + placeholder = TextReference.Str("Enter address"), + label = TextReference.Str("Recipient Address"), + isError = false, + error = null, + isValuePasted = false, + ), + memoTextField = DestinationTextFieldUM.RecipientMemo( + value = "Test memo for transaction", + keyboardOptions = KeyboardOptions.Default, + placeholder = TextReference.Str("Enter memo (optional)"), + label = TextReference.Str("Memo"), + isError = false, + error = null, + disabledText = TextReference.Str("Memo disabled"), + isEnabled = true, + isValuePasted = false, + ), + recent = emptyList().toImmutableList(), + wallets = emptyList().toImmutableList(), + networkName = "Ethereum", + isValidating = false, + isInitialized = true, + isRedesignEnabled = true, + ), + ) } \ No newline at end of file From 37efabe359ba0f2ad6987e3f15fa96bd0b5c54d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Jun 2025 15:44:11 +0500 Subject: [PATCH 48/99] Updated on 2026-08-14 --- .../create-wallet-selection/api/.gitignore | 1 + .../api/build.gradle.kts | 22 ++ .../CreateWalletSelectionComponent.kt | 8 + .../create-wallet-selection/impl/.gitignore | 1 + .../impl/build.gradle.kts | 70 +++++ .../CreateWalletSelectionContent.kt | 240 ++++++++++++++++++ .../CreateWalletSelectionFeatureModule.kt | 30 +++ .../CreateWalletSelectionModel.kt | 37 +++ .../DefaultCreateWalletSelectionComponent.kt | 33 +++ .../entity/CreateWalletSelectionUM.kt | 9 + settings.gradle.kts | 3 + 11 files changed, 454 insertions(+) create mode 100644 features/create-wallet-selection/api/.gitignore create mode 100644 features/create-wallet-selection/api/build.gradle.kts create mode 100644 features/create-wallet-selection/api/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionComponent.kt create mode 100644 features/create-wallet-selection/impl/.gitignore create mode 100644 features/create-wallet-selection/impl/build.gradle.kts create mode 100644 features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionContent.kt create mode 100644 features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionFeatureModule.kt create mode 100644 features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt create mode 100644 features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/DefaultCreateWalletSelectionComponent.kt create mode 100644 features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt diff --git a/features/create-wallet-selection/api/.gitignore b/features/create-wallet-selection/api/.gitignore new file mode 100644 index 0000000000..d3b3de5967 --- /dev/null +++ b/features/create-wallet-selection/api/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/features/create-wallet-selection/api/build.gradle.kts b/features/create-wallet-selection/api/build.gradle.kts new file mode 100644 index 0000000000..2ffb19aeee --- /dev/null +++ b/features/create-wallet-selection/api/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.createwalletselection.api" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.models) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/create-wallet-selection/api/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionComponent.kt b/features/create-wallet-selection/api/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionComponent.kt new file mode 100644 index 0000000000..cadc5950f1 --- /dev/null +++ b/features/create-wallet-selection/api/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionComponent.kt @@ -0,0 +1,8 @@ +package com.tangem.features.createwalletselection + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface CreateWalletSelectionComponent : ComposableContentComponent { + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/create-wallet-selection/impl/.gitignore b/features/create-wallet-selection/impl/.gitignore new file mode 100644 index 0000000000..d3b3de5967 --- /dev/null +++ b/features/create-wallet-selection/impl/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts new file mode 100644 index 0000000000..828a0ccfac --- /dev/null +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -0,0 +1,70 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.createwalletselection.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.createWalletSelection.api) + + /** Hot Wallet Feature */ + implementation(projects.features.hotWallet.api) + + /** Core modules */ + implementation(projects.core.configToggles) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.res) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.datasource) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** Tangem libraries */ + implementation(projects.libs.tangemSdkApi) + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) { + exclude(module = "joda-time") + } + + /** AndroidX libraries */ + implementation(deps.androidx.core.ktx) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.material) + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.lottie.compose) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + implementation(deps.androidx.datastore) + + /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.timber) + implementation(deps.firebase.crashlytics) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionContent.kt new file mode 100644 index 0000000000..ad39c3191b --- /dev/null +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionContent.kt @@ -0,0 +1,240 @@ +package com.tangem.features.createwalletselection + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM +import com.tangem.features.createwalletselection.impl.R + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifier: Modifier = Modifier) { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .systemBarsPadding(), + ) { + TopAppBar( + modifier = modifier + .statusBarsPadding(), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TangemTheme.colors.background.primary, + ), + navigationIcon = { + IconButton(onClick = state.onBackClick) { + Icon( + painter = painterResource(R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } + }, + title = { }, + actions = { + Text( + modifier = Modifier + .padding(16.dp), + // TODO update and extract this string + text = "What to choose?", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + ) + Column( + modifier = Modifier + .weight(1f) + .padding( + start = 16.dp, + top = 24.dp, + end = 16.dp, + ), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + // TODO update and extract this string + text = "Choose how you want to create your wallet", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + WalletBlock( + modifier = Modifier + .padding(top = 24.dp), + // TODO update and extract this string + title = "Mobile Wallet", + // TODO update and extract this string + description = "A secure wallet is created on your phone in seconds.", + badge = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.field.focused, + shape = TangemTheme.shapes.roundedCorners8, + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + // TODO update and extract this string + text = "Free", + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + ) + } + }, + onClick = state.onMobileWalletClick, + ) + WalletBlock( + // TODO update and extract this string + title = "Hardware Wallet", + // TODO update and extract this string + description = "Buy Tangem Wallet — physical cards that securely store your crypto offline.", + badge = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), + shape = TangemTheme.shapes.roundedCorners8, + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + // TODO update and extract this string + text = "From $54.90", + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.accent, + ) + } + }, + onClick = state.onHardwareWalletClick, + ) + } + AlreadyHaveTangemWalletBlock( + onScanClick = state.onScanClick, + isScanInProgress = state.isScanInProgress, + ) + } +} + +@Composable +private fun WalletBlock( + modifier: Modifier = Modifier, + title: String, + description: String, + badge: @Composable () -> Unit, + onClick: () -> Unit, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(top = 8.dp) + .background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(16.dp) + .clickable(onClick = onClick), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier + .padding(end = 8.dp), + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + badge() + } + Text( + modifier = Modifier + .padding(top = 4.dp), + text = description, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun AlreadyHaveTangemWalletBlock( + onScanClick: () -> Unit, + isScanInProgress: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(16.dp) + .background( + color = TangemTheme.colors.field.primary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding( + horizontal = 20.dp, + vertical = 16.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier + .weight(1f) + .padding(end = 16.dp), + // TODO update and extract this string + text = "Do you already have Tangem Wallet?", + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + ) + TangemButton( + modifier = Modifier + .wrapContentWidth(), + // TODO update and extract this string + text = "Scan Now", + onClick = onScanClick, + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), + size = TangemButtonSize.RoundedAction, + showProgress = isScanInProgress, + colors = TangemButtonsDefaults.secondaryButtonColors, + textStyle = TangemTheme.typography.subtitle1, + enabled = true, + animateContentChange = true, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewCreateWalletContent() { + TangemThemePreview { + CreateWalletSelectionContent( + state = CreateWalletSelectionUM( + onBackClick = {}, + onMobileWalletClick = {}, + onHardwareWalletClick = {}, + onScanClick = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionFeatureModule.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionFeatureModule.kt new file mode 100644 index 0000000000..a810f1ba9f --- /dev/null +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionFeatureModule.kt @@ -0,0 +1,30 @@ +package com.tangem.features.createwalletselection + +import com.tangem.core.decompose.model.Model +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CreateWalletSelectionFeatureModule + +@Module +@InstallIn(SingletonComponent::class) +internal interface CreateWalletSelectionFeatureModuleBinds { + + @Binds + @Singleton + fun bindCreateWalletSelectionComponentFactory( + impl: DefaultCreateWalletSelectionComponent.Factory, + ): CreateWalletSelectionComponent.Factory + + @Binds + @IntoMap + @ClassKey(CreateWalletSelectionModel::class) + fun bindCreateWalletSelectionModel(model: CreateWalletSelectionModel): Model +} \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt new file mode 100644 index 0000000000..0299618954 --- /dev/null +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -0,0 +1,37 @@ +package com.tangem.features.createwalletselection + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class CreateWalletSelectionModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + internal val uiState: StateFlow + field = MutableStateFlow( + CreateWalletSelectionUM( + onBackClick = { /* [REDACTED_TODO_COMMENT] */ }, + onMobileWalletClick = ::onMobileWalletClick, + onHardwareWalletClick = ::onHardwareWalletClick, + onScanClick = ::onScanClick, + ), + ) + + private fun onMobileWalletClick() { + // TODO navigate to mobile wallet creation + } + + private fun onHardwareWalletClick() { + // TODO open card order web page + } + + private fun onScanClick() { + // TODO open card scanning + } +} \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/DefaultCreateWalletSelectionComponent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/DefaultCreateWalletSelectionComponent.kt new file mode 100644 index 0000000000..7eede00b99 --- /dev/null +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/DefaultCreateWalletSelectionComponent.kt @@ -0,0 +1,33 @@ +package com.tangem.features.createwalletselection + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultCreateWalletSelectionComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Unit, +) : CreateWalletSelectionComponent, AppComponentContext by context { + + private val model: CreateWalletSelectionModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + CreateWalletSelectionContent( + state = state, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : CreateWalletSelectionComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultCreateWalletSelectionComponent + } +} \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt new file mode 100644 index 0000000000..70c95a5b38 --- /dev/null +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.createwalletselection.entity + +internal data class CreateWalletSelectionUM( + val isScanInProgress: Boolean = false, + val onBackClick: () -> Unit, + val onMobileWalletClick: () -> Unit, + val onHardwareWalletClick: () -> Unit, + val onScanClick: () -> Unit, +) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index eca5b3d022..ba875e3bb9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -251,6 +251,9 @@ include(":features:hot-wallet:impl") include(":features:kyc:api") include(":features:kyc:impl") + +include(":features:create-wallet-selection:api") +include(":features:create-wallet-selection:impl") // endregion Feature modules // region Domain modules From 4fe300d8ccc4cdb970dffc732caa0611d4096a5d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Jun 2025 15:00:23 +0300 Subject: [PATCH 49/99] Updated on 2026-08-14 --- app/build.gradle.kts | 3 +++ gradle/dependencies.toml | 2 ++ .../configurations/extension/AppExtensionConfigurations.kt | 1 + 3 files changed, 6 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 18dfcd383d..1c4d33f627 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,6 +18,7 @@ android { namespace = "com.tangem.wallet" testOptions { animationsDisabled = true + execution = "ANDROIDX_TEST_ORCHESTRATOR" } packaging { jniLibs { @@ -345,6 +346,8 @@ dependencies { implementation(deps.listenableFuture) implementation(deps.mlKit.barcodeScanning) + androidTestUtil(deps.test.orchestrator) + /** Leakcanary */ debugImplementation(deps.leakcanary) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 0839acc10f..97949a7daf 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -111,6 +111,7 @@ kaspresso = "1.5.4" kaspresso-compose = "1.5.4" compose-junit = "1.6.2" hamcrest = "2.2" +orchestrator = "1.5.1" # endregion Testing [plugins] @@ -212,6 +213,7 @@ test-compose-junit = { module = "androidx.compose.ui:ui-test-junit4", version.re test-hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } test-hilt = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } test-hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orchestrator" } # endregion Test # region Other diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index 03d2b74d0c..6f03ce1a3a 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -38,6 +38,7 @@ private fun AppExtension.configureDefaultConfig(project: Project) { buildFeatures.buildConfig = true testInstrumentationRunner = "com.tangem.common.HiltTestRunner" + testInstrumentationRunnerArguments["clearPackageData"] = "true" } } From 15bfcf9ceaa16d577147a0ea297b791979db5ca3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Jun 2025 17:37:01 +0800 Subject: [PATCH 50/99] Updated on 2026-08-14 --- .../sessions/DefaultWcSessionsManager.kt | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index c6239ed16c..66211e5d83 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -7,9 +7,9 @@ import com.domain.blockaid.models.dapp.CheckDAppResult import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate +import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkSessionConverter -import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionDTO @@ -18,11 +18,9 @@ import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext import timber.log.Timber import kotlin.coroutines.resume @@ -37,6 +35,7 @@ internal class DefaultWcSessionsManager( private val onSessionDelete = Channel(capacity = Channel.BUFFERED) private val oneTimeMigration = MutableStateFlow(true) + private val oneTimeSessionExtend = MutableStateFlow(true) override val sessions: Flow>> get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } @@ -51,6 +50,10 @@ internal class DefaultWcSessionsManager( val associatedSessions: List = associate(inSdk, inStore, wallets) val someRemove = removeUnknownSessions(inStore, associatedSessions) if (someRemove) return@transform // ignore emit, wait next one + if (oneTimeSessionExtend.value) { + oneTimeSessionExtend.value = false + scope.launch { extendAliveSessions(associatedSessions) } + } emit(associatedSessions.groupBy { it.wallet }) } .distinctUntilChanged() @@ -58,6 +61,7 @@ internal class DefaultWcSessionsManager( override fun onWcSdkInit() { oneTimeMigration.value = true + oneTimeSessionExtend.value = true listenOnSessionDelete() } @@ -150,6 +154,11 @@ internal class DefaultWcSessionsManager( return haveSomeUnknown } + private suspend fun extendAliveSessions(sessions: List) = coroutineScope { + val jobs = sessions.map { launch { sdkSessionExtend(it.sdkModel.topic) } } + jobs.joinAll() + } + private suspend fun sdkDisconnectSession(topic: String): Either { return suspendCancellableCoroutine { continuation -> WalletKit.disconnectSession( @@ -160,6 +169,16 @@ internal class DefaultWcSessionsManager( } } + private suspend fun sdkSessionExtend(topic: String): Either { + return suspendCancellableCoroutine { continuation -> + WalletKit.extendSession( + params = Wallet.Params.SessionExtend(topic), + onSuccess = { continuation.resume(Unit.right()) }, + onError = { continuation.resume(it.throwable.left()) }, + ) + } + } + private fun listenOnSessionDelete() { onSessionDelete.receiveAsFlow() .filterIsInstance() From 58291942cffb0b3392c71736794b552142b8912b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Jun 2025 18:47:56 +0900 Subject: [PATCH 51/99] Updated on 2026-08-14 --- .../data/walletconnect/di/WalletConnectDataModule.kt | 3 ++- .../data/walletconnect/request/DefaultWcRequestService.kt | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 2eb8bb69d2..2d585f4c9b 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -111,9 +111,10 @@ internal object WalletConnectDataModule { @Provides @Singleton - fun defaultWcRequestService(diHelperBox: DiHelperBox): DefaultWcRequestService { + fun defaultWcRequestService(diHelperBox: DiHelperBox, respondService: WcRespondService): DefaultWcRequestService { return DefaultWcRequestService( requestConverters = diHelperBox.handlers, + respondService = respondService, ) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt index ddfb53cc8e..4c50b189fd 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -1,9 +1,10 @@ package com.tangem.data.walletconnect.request import com.reown.walletkit.client.Wallet +import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkSessionRequestConverter -import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.domain.walletconnect.WcRequestService import com.tangem.domain.walletconnect.model.WcMethodName import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest @@ -14,6 +15,7 @@ import timber.log.Timber internal class DefaultWcRequestService( private val requestConverters: Set, + private val respondService: WcRespondService, ) : WcSdkObserver, WcRequestService { private val _wcRequest: Channel> = Channel(Channel.BUFFERED) @@ -29,6 +31,9 @@ internal class DefaultWcRequestService( val name = requestConverters.firstNotNullOfOrNull { it.toWcMethodName(sr) } ?: WcMethodName.Unsupported(sr.request.method) Timber.tag(WC_TAG).i("handle request name $name") + if (name is WcMethodName.Unsupported) { + respondService.rejectRequestNonBlock(sr) + } _wcRequest.trySend(name to sr) } } \ No newline at end of file From 4d55089d5ed05c13a3c7a0c974e3e04d6fab4828 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Jun 2025 17:31:26 +0500 Subject: [PATCH 52/99] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 1 + .../ui/components/inputrow/InputRowEnter.kt | 2 + .../DefaultTransactionRepository.kt | 3 +- .../transaction/TransactionRepository.kt | 1 + .../CreateTransferTransactionUseCase.kt | 5 +++ .../analytics/CommonSendAnalyticEvents.kt | 15 +++++++ .../v2/feeselector/entity/FeeSelectorUM.kt | 4 ++ .../ui/FeeSelectorModalBottomSheet.kt | 45 +++++++++++++++++-- .../v2/send/analytics/SendAnalyticEvents.kt | 7 +++ .../v2/send/analytics/SendAnalyticHelper.kt | 2 + .../v2/send/confirm/model/SendConfirmModel.kt | 2 + .../fee/model/SendFeeClickIntents.kt | 2 + .../subcomponents/fee/model/SendFeeModel.kt | 17 +++++++ .../transformers/SendFeeLoadedTransformer.kt | 11 +++-- .../SendFeeNonceChangeTransformer.kt | 36 +++++++++++++++ .../v2/subcomponents/fee/ui/SendCustomFee.kt | 34 ++++++++++++++ .../v2/subcomponents/fee/ui/SendFeeContent.kt | 6 +++ .../fee/ui/state/FeeSelectorUM.kt | 2 + .../v2/subcomponents/fee/ui/state/FeeUM.kt | 3 ++ 19 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeNonceChangeTransformer.kt diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index fc00d6b5bc..be0ea9267f 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -219,5 +219,6 @@ sealed class AnalyticsParam { const val ACTION = "Action" const val COLLECTIONS = "Collections" const val NFT = "Nft" + const val NONCE = "Nonce" } } \ 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 index 6b8f360c17..f930945feb 100644 --- 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 @@ -50,6 +50,7 @@ fun InputRowEnter( text: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, + placeholder: TextReference? = null, titleColor: Color = TangemTheme.colors.text.secondary, textColor: Color = TangemTheme.colors.text.primary1, isSingleLine: Boolean = false, @@ -78,6 +79,7 @@ fun InputRowEnter( SimpleTextField( value = text, onValueChange = onValueChange, + placeholder = placeholder, color = textColor, singleLine = isSingleLine, visualTransformation = visualTransformation, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index d1de414bbc..30d55b6061 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -83,6 +83,7 @@ internal class DefaultTransactionRepository( destination: String, userWalletId: UserWalletId, network: Network, + nonce: BigInteger?, ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { val blockchain = network.toBlockchain() val walletManager = walletManagersFacade.getOrCreateWalletManager( @@ -101,7 +102,7 @@ internal class DefaultTransactionRepository( createTransactionDataExtras( callData = callData, network = network, - nonce = null, + nonce = nonce, gasLimit = null, ) } else { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 9825addc50..52a4d262b4 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -33,6 +33,7 @@ interface TransactionRepository { destination: String, userWalletId: UserWalletId, network: Network, + nonce: BigInteger? = null, ): TransactionData.Uncompiled @Suppress("LongParameterList") diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransferTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransferTransactionUseCase.kt index 221f703b9d..291f343eeb 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransferTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransferTransactionUseCase.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.models.network.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigInteger /** * Use case to create and get transfer transaction @@ -28,11 +29,13 @@ class CreateTransferTransactionUseCase( destination: String, userWalletId: UserWalletId, network: Network, + nonce: BigInteger? = null, ) = Either.catch { transactionRepository.createTransferTransaction( amount = amount, fee = fee, memo = memo, + nonce = nonce, destination = destination, userWalletId = userWalletId, network = network, @@ -49,11 +52,13 @@ class CreateTransferTransactionUseCase( destination: String, userWalletId: UserWalletId, network: Network, + nonce: BigInteger? = null, ) = Either.catch { transactionRepository.createTransferTransaction( amount = amount, memo = memo, fee = null, + nonce = nonce, destination = destination, userWalletId = userWalletId, network = network, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt index a392ca4da3..c40b00375d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.v2.common.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM @@ -95,6 +96,20 @@ internal sealed class CommonSendAnalyticEvents( params = mapOf(SOURCE to source.name), ) + /** Fee screen is closed with non empty nonce */ + data class NonceInserted( + val categoryName: String, + val token: String, + val blockchain: String, + ) : CommonSendAnalyticEvents( + category = categoryName, + event = "Nonce Inserted", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) + companion object { const val SEND_CATEGORY = "Token / Send" const val NFT_SEND_CATEGORY = "NFT" diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt index fd759f5d7a..f12dfd588a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt @@ -7,6 +7,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal +import java.math.BigInteger @Immutable internal sealed class FeeSelectorUM { @@ -19,6 +20,9 @@ internal sealed class FeeSelectorUM { val selectedFeeItem: FeeItem, val isFeeApproximate: Boolean, val feeFiatRateDataHolder: FeeFiatRateDataHolder?, + val displayNonceInput: Boolean, + val nonce: BigInteger?, + val onNonceChange: (String) -> Unit, ) : FeeSelectorUM() } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 2b79756637..e904bf6477 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview @@ -55,6 +56,7 @@ import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal +import java.math.BigInteger @Composable internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () -> Unit) { @@ -75,7 +77,10 @@ internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () -> ) }, content = { - FeeSelectorItems(state = state, modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp)) + FeeSelectorItems( + state = state, + modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp), + ) }, footer = { PrimaryButton( @@ -240,18 +245,25 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = isSelected = isSelected, iconBackgroundColor = iconBackgroundColor, iconTint = iconTint, + displayNonceInput = state.displayNonceInput, + nonce = state.nonce, + onNonceChange = state.onNonceChange, ) } } } } +@Suppress("LongParameterList") @Composable private fun CustomFeeBlock( customFee: FeeItem.Custom, isSelected: Boolean, iconBackgroundColor: Color, iconTint: Color, + displayNonceInput: Boolean, + nonce: BigInteger?, + onNonceChange: (String) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -281,7 +293,13 @@ private fun CustomFeeBlock( enter = expandVertically().plus(fadeIn()), exit = shrinkVertically().plus(fadeOut()), ) { - ExpandedCustomFeeItems(customFeeFields = customFee.customValues, onValueChange = { _, _ -> }) + ExpandedCustomFeeItems( + customFeeFields = customFee.customValues, + onValueChange = { _, _ -> }, + displayNonceInput = displayNonceInput, + nonce = nonce, + onNonceChange = onNonceChange, + ) } } } @@ -290,11 +308,14 @@ private fun CustomFeeBlock( private fun ExpandedCustomFeeItems( customFeeFields: ImmutableList, onValueChange: (Int, String) -> Unit, + displayNonceInput: Boolean, + nonce: BigInteger?, + onNonceChange: (String) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { customFeeFields.fastForEachIndexed { index, field -> - val showDivider = index != customFeeFields.size - 1 + val showDivider = index != customFeeFields.size - 1 || displayNonceInput if (field.label != null) { InputRowEnterInfoAmountV2( text = field.value, @@ -323,6 +344,21 @@ private fun ExpandedCustomFeeItems( ) } } + + if (displayNonceInput) { + // TODO implement v2 input without binding to amount + InputRowEnterInfoAmountV2( + text = nonce?.toString() ?: "", + decimals = 0, + title = resourceReference(R.string.send_nonce), + titleColor = TangemTheme.colors.text.tertiary, + symbol = null, + onValueChange = onNonceChange, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + keyboardActions = KeyboardActions(), + showDivider = false, + ) + } } } @@ -466,6 +502,9 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< rate = BigDecimal.TEN, appCurrency = AppCurrency.Default, ), + displayNonceInput = true, + onNonceChange = {}, + nonce = null, ), ), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index 90e33cdec8..30a9234904 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -2,8 +2,11 @@ package com.tangem.features.send.v2.send.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE +import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.core.ui.extensions.capitalize import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents /** @@ -18,11 +21,15 @@ internal sealed class SendAnalyticEvents( data class TransactionScreenOpened( val token: String, val feeType: AnalyticsParam.FeeType, + val blockchain: String, + val nonceNotEmpty: Boolean, ) : SendAnalyticEvents( event = "Transaction Sent Screen Opened", params = mapOf( TOKEN_PARAM to token, FEE_TYPE to feeType.value, + BLOCKCHAIN to blockchain, + NONCE to nonceNotEmpty.toString().capitalize(), ), ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index be2690fcc3..807a4f0542 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -26,6 +26,8 @@ internal class SendAnalyticHelper @Inject constructor( SendAnalyticEvents.TransactionScreenOpened( token = cryptoCurrency.symbol, feeType = feeType, + blockchain = cryptoCurrency.network.name, + nonceNotEmpty = feeSelectorUM.nonce != null, ), ) analyticsEventHandler.send( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index a84b4a3fc7..29a3e1e93d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -320,6 +320,7 @@ internal class SendConfirmModel @Inject constructor( val memo = destinationUM?.memoTextField?.value val fee = feeSelectorUM?.selectedFee val feeValue = fee?.amount?.value ?: return + val nonce = feeSelectorUM?.nonce val receivingAmount = checkAndCalculateSubtractedAmount( isAmountSubtractAvailable = isAmountSubtractAvailable, @@ -334,6 +335,7 @@ internal class SendConfirmModel @Inject constructor( amount = receivingAmount.convertToSdkAmount(cryptoCurrency), fee = fee, memo = memo, + nonce = nonce, destination = destination, userWalletId = userWallet.walletId, network = cryptoCurrency.network, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeClickIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeClickIntents.kt index 656699429a..bffe9c81a2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeClickIntents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeClickIntents.kt @@ -10,6 +10,8 @@ internal interface SendFeeClickIntents { fun onCustomFeeValueChange(index: Int, value: String) + fun onNonceChange(value: String) + fun onReadMoreClick() fun onNextClick() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt index 570cb197f7..42b2a1da02 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt @@ -9,6 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.NonceInserted import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.ButtonsUM @@ -129,6 +130,12 @@ internal class SendFeeModel @Inject constructor( updateFeeNotifications() } + override fun onNonceChange(value: String) { + _uiState.update( + SendFeeNonceChangeTransformer(value = value), + ) + } + override fun onReadMoreClick() { val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE val url = buildString { @@ -157,6 +164,16 @@ internal class SendFeeModel @Inject constructor( ), ) + if (feeSelectorUM.nonce != null) { + analyticsEventHandler.send( + NonceInserted( + categoryName = analyticsCategoryName, + token = cryptoCurrencyStatus.currency.symbol, + blockchain = cryptoCurrencyStatus.currency.network.name, + ), + ) + } + saveResult() } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt index b1a7fb7467..89a69d49f0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt @@ -1,13 +1,14 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter +import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM -import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents -import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter import com.tangem.utils.transformer.Transformer internal class SendFeeLoadedTransformer( @@ -41,6 +42,7 @@ internal class SendFeeLoadedTransformer( fees = fees, customValues = customFeeFieldConverter.convert(fees.normal), selectedFee = fees.normal, + nonce = prevState.nonce, ) } else { FeeSelectorUM.Content( @@ -53,12 +55,15 @@ internal class SendFeeLoadedTransformer( customValues = feeSelectorUM.customValues, ), ), + nonce = prevState.nonce, ) } return state.copy( feeSelectorUM = updatedFeeSelector, isFeeApproximate = isFeeApproximate, + displayNonceInput = fees.normal is Fee.Ethereum, + nonce = prevState.nonce, ) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeNonceChangeTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeNonceChangeTransformer.kt new file mode 100644 index 0000000000..8b453ed2d3 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeNonceChangeTransformer.kt @@ -0,0 +1,36 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.transformers + +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.utils.transformer.Transformer +import java.math.BigInteger + +internal class SendFeeNonceChangeTransformer( + private val value: String, +) : Transformer { + + override fun transform(prevState: FeeUM): FeeUM { + val state = prevState as? FeeUM.Content ?: return prevState + + if (value.isEmpty()) { + return state.copy(nonce = null) + } + + return try { + val nonce = BigInteger(value) + state.copy( + nonce = nonce, + feeSelectorUM = when (val feeSelectorUM = state.feeSelectorUM) { + is FeeSelectorUM.Loading, + is FeeSelectorUM.Error, + -> feeSelectorUM + is FeeSelectorUM.Content -> feeSelectorUM.copy( + nonce = nonce, + ) + }, + ) + } catch (e: NumberFormatException) { + state + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt index cf31789446..95173f58c6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt @@ -5,23 +5,31 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.components.inputrow.InputRowEnter import com.tangem.core.ui.components.inputrow.InputRowEnterAmount import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import kotlinx.collections.immutable.ImmutableList +@Suppress("LongParameterList") @Composable internal fun SendCustomFee( customValues: ImmutableList, selectedFee: FeeType, hasNotifications: Boolean, onValueChange: (Int, String) -> Unit, + onNonceChange: (String) -> Unit, + nonce: String?, modifier: Modifier = Modifier, ) { AnimatedVisibility( @@ -77,6 +85,32 @@ internal fun SendCustomFee( } } } + Nonce( + onNonceChange = onNonceChange, + nonce = nonce, + ) } } +} + +@Composable +private fun Nonce(onNonceChange: (String) -> Unit, nonce: String?) { + FooterContainer( + footer = resourceReference(R.string.send_nonce_footer), + modifier = Modifier.padding(bottom = 12.dp), + ) { + InputRowEnter( + text = nonce.orEmpty(), + title = resourceReference(R.string.send_nonce), + onValueChange = onNonceChange, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + placeholder = resourceReference(R.string.send_nonce_hint), + showDivider = false, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendFeeContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendFeeContent.kt index 728f537a37..c017356770 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendFeeContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendFeeContent.kt @@ -40,6 +40,8 @@ internal fun SendFeeContent(state: FeeUM, clickIntents: SendFeeClickIntents) { customFee( feeSelectorUM = feeSelectorUM, onValueChange = clickIntents::onCustomFeeValueChange, + onNonceChange = clickIntents::onNonceChange, + nonce = state.nonce?.toString().orEmpty(), hasNotifications = hasNotifications, ) } @@ -61,6 +63,8 @@ internal fun LazyListScope.customFee( feeSelectorUM: FeeSelectorUM.Content, hasNotifications: Boolean, onValueChange: (Int, String) -> Unit, + onNonceChange: (String) -> Unit, + nonce: String?, modifier: Modifier = Modifier, ) { item(key = FEE_CUSTOM_KEY) { @@ -69,6 +73,8 @@ internal fun LazyListScope.customFee( selectedFee = feeSelectorUM.selectedType, hasNotifications = hasNotifications, onValueChange = onValueChange, + onNonceChange = onNonceChange, + nonce = nonce, modifier = modifier .fillMaxWidth() .animateItem() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt index a9f5aae1b4..46bbd7c4d9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt @@ -7,6 +7,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.transaction.error.GetFeeError import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import java.math.BigInteger @Stable internal sealed class FeeSelectorUM { @@ -16,6 +17,7 @@ internal sealed class FeeSelectorUM { val selectedType: FeeType = FeeType.Market, val selectedFee: Fee?, val customValues: ImmutableList = persistentListOf(), + val nonce: BigInteger? = null, ) : FeeSelectorUM() data object Loading : FeeSelectorUM() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt index 6bf499e1c4..592570e0b5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt @@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal +import java.math.BigInteger @Stable internal sealed class FeeUM { @@ -22,6 +23,8 @@ internal sealed class FeeUM { val isCustomSelected: Boolean, val isTronToken: Boolean, val customValues: ImmutableList = persistentListOf(), + val displayNonceInput: Boolean = false, + val nonce: BigInteger? = null, val notifications: ImmutableList, val isEditingDisabled: Boolean = false, ) : FeeUM() From 64626bbcbc75856828d25e7e069a33a6ae2b3835 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Jun 2025 22:39:17 +0500 Subject: [PATCH 53/99] Updated on 2026-08-14 --- .../drawable/ic_create_mobile_wallet_56.xml | 43 ++++++ .../DefaultCreateWalletSelectionComponent.kt | 1 + .../{ => ui}/CreateWalletSelectionContent.kt | 22 +-- .../hotwallet/CreateMobileWalletComponent.kt | 8 + .../CreateMobileWalletModel.kt | 27 ++++ .../DefaultCreateMobileWalletComponent.kt | 34 +++++ .../entity/CreateMobileWalletUM.kt | 6 + .../ui/CreateMobileWalletContent.kt | 144 ++++++++++++++++++ 8 files changed, 274 insertions(+), 11 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_create_mobile_wallet_56.xml rename features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/{ => ui}/CreateWalletSelectionContent.kt (90%) create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt diff --git a/core/ui/src/main/res/drawable/ic_create_mobile_wallet_56.xml b/core/ui/src/main/res/drawable/ic_create_mobile_wallet_56.xml new file mode 100644 index 0000000000..c4efe75b71 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_create_mobile_wallet_56.xml @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/DefaultCreateWalletSelectionComponent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/DefaultCreateWalletSelectionComponent.kt index 7eede00b99..a76cd26754 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/DefaultCreateWalletSelectionComponent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/DefaultCreateWalletSelectionComponent.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.createwalletselection.ui.CreateWalletSelectionContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt similarity index 90% rename from features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionContent.kt rename to features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index ad39c3191b..4c66af5a94 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.createwalletselection +package com.tangem.features.createwalletselection.ui import android.content.res.Configuration import androidx.compose.foundation.background @@ -52,7 +52,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi Text( modifier = Modifier .padding(16.dp), - // TODO update and extract this string + // TODO [REDACTED_TASK_KEY] update and extract this string text = "What to choose?", style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, @@ -74,7 +74,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - // TODO update and extract this string + // TODO [REDACTED_TASK_KEY] update and extract this string text = "Choose how you want to create your wallet", style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, @@ -83,9 +83,9 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi WalletBlock( modifier = Modifier .padding(top = 24.dp), - // TODO update and extract this string + // TODO [REDACTED_TASK_KEY] update and extract this string title = "Mobile Wallet", - // TODO update and extract this string + // TODO [REDACTED_TASK_KEY] update and extract this string description = "A secure wallet is created on your phone in seconds.", badge = { Box( @@ -97,7 +97,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi .padding(horizontal = 8.dp, vertical = 4.dp), ) { Text( - // TODO update and extract this string + // TODO [REDACTED_TASK_KEY] update and extract this string text = "Free", style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.secondary, @@ -107,9 +107,9 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi onClick = state.onMobileWalletClick, ) WalletBlock( - // TODO update and extract this string + // TODO [REDACTED_TASK_KEY] update and extract this string title = "Hardware Wallet", - // TODO update and extract this string + // TODO [REDACTED_TASK_KEY] update and extract this string description = "Buy Tangem Wallet — physical cards that securely store your crypto offline.", badge = { Box( @@ -121,7 +121,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi .padding(horizontal = 8.dp, vertical = 4.dp), ) { Text( - // TODO update and extract this string + // TODO [REDACTED_TASK_KEY] update and extract this string text = "From $54.90", style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.accent, @@ -201,7 +201,7 @@ private fun AlreadyHaveTangemWalletBlock( modifier = Modifier .weight(1f) .padding(end = 16.dp), - // TODO update and extract this string + // TODO [REDACTED_TASK_KEY] update and extract this string text = "Do you already have Tangem Wallet?", style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, @@ -209,7 +209,7 @@ private fun AlreadyHaveTangemWalletBlock( TangemButton( modifier = Modifier .wrapContentWidth(), - // TODO update and extract this string + // TODO [REDACTED_TASK_KEY] update and extract this string text = "Scan Now", onClick = onScanClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt new file mode 100644 index 0000000000..e9bdbf64e5 --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt @@ -0,0 +1,8 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface CreateMobileWalletComponent : ComposableContentComponent { + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt new file mode 100644 index 0000000000..629e6c0a74 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -0,0 +1,27 @@ +package com.tangem.features.hotwallet.createmobilewallet + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class CreateMobileWalletModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + internal val uiState: StateFlow + field = MutableStateFlow( + CreateMobileWalletUM( + onBackClick = { /* [REDACTED_TODO_COMMENT] */ }, + onCreateClick = ::onCreateClick, + ), + ) + + private fun onCreateClick() { + // TODO create a wallet + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt new file mode 100644 index 0000000000..7a8181fb40 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt @@ -0,0 +1,34 @@ +package com.tangem.features.hotwallet.createmobilewallet + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.CreateMobileWalletComponent +import com.tangem.features.hotwallet.createmobilewallet.ui.CreateMobileWalletContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultCreateMobileWalletComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Unit, +) : CreateMobileWalletComponent, AppComponentContext by context { + private val model: CreateMobileWalletModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + CreateMobileWalletContent( + state = state, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : CreateMobileWalletComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultCreateMobileWalletComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt new file mode 100644 index 0000000000..918e811d85 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.hotwallet.createmobilewallet.entity + +internal data class CreateMobileWalletUM( + val onBackClick: () -> Unit, + val onCreateClick: () -> Unit, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt new file mode 100644 index 0000000000..6f3a77c019 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt @@ -0,0 +1,144 @@ +package com.tangem.features.hotwallet.createmobilewallet.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Modifier = Modifier) { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .systemBarsPadding(), + ) { + TangemTopAppBar( + modifier = modifier + .statusBarsPadding(), + startButton = TopAppBarButtonUM.Back(state.onBackClick), + title = TextReference.EMPTY, + ) + Column( + modifier = Modifier + .weight(1f) + .padding( + start = 16.dp, + top = 24.dp, + end = 16.dp, + ), + ) { + Icon( + modifier = Modifier + .fillMaxWidth(), + painter = painterResource(R.drawable.ic_create_mobile_wallet_56), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + top = 20.dp, + end = 16.dp, + ), + // TODO [REDACTED_TASK_KEY] update and extract this string + text = "Create Mobile Wallet", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 32.dp), + // TODO [REDACTED_TASK_KEY] update and extract this string + title = "Keys are stored in the app", + // TODO [REDACTED_TASK_KEY] update and extract this string + description = "Get notified of incoming transactions", + iconRes = R.drawable.ic_lock_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + // TODO [REDACTED_TASK_KEY] update and extract this string + title = "Seed phrase backup", + // TODO [REDACTED_TASK_KEY] update and extract this string + description = "Stay up to date with the latest features and news", + iconRes = R.drawable.ic_settings_24, + ) + } + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + text = "Create", + showProgress = false, + enabled = true, + onClick = state.onCreateClick, + ) + } +} + +@Composable +private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + ) { + Icon( + modifier = Modifier + .padding(horizontal = 12.dp), + painter = painterResource(iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + ) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .padding(top = 4.dp), + text = description, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewCreateWalletContent() { + TangemThemePreview { + CreateMobileWalletContent( + state = CreateMobileWalletUM( + onBackClick = {}, + onCreateClick = {}, + ), + ) + } +} \ No newline at end of file From 3e2de9fd6b6ebe2cd0c2187c64e1a8c0a30f7989 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Jun 2025 16:31:20 +0500 Subject: [PATCH 54/99] Updated on 2026-08-14 --- .../swap/DefaultSwapTransactionRepository.kt | 289 ++++++++++++++++++ .../swap/converter/SwapStatusConverter.kt | 24 ++ .../transaction/SavedSwapStatusConverter.kt | 38 +++ .../SavedSwapTransactionConverter.kt | 60 ++++ .../SavedSwapTransactionListConverter.kt | 83 +++++ .../com/tangem/data/swap/di/SwapDataModule.kt | 17 ++ .../models/LastSwappedCryptoCurrencyDTO.kt | 12 + .../tangem/data/swap/models/SwapStatusDTO.kt | 78 +++++ .../swap/models/SwapTransactionListDTO.kt | 40 +++ .../domain/swap/models/SwapStatusModel.kt | 62 ++++ .../swap/models/SwapTransactionListModel.kt | 29 ++ .../domain/swap/SwapTransactionRepository.kt | 80 +++++ 12 files changed, 812 insertions(+) create mode 100644 data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/converter/SwapStatusConverter.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/models/LastSwappedCryptoCurrencyDTO.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/models/SwapStatusDTO.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/models/SwapTransactionListDTO.kt create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapStatusModel.kt create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt create mode 100644 domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt new file mode 100644 index 0000000000..5329db524a --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -0,0 +1,289 @@ +package com.tangem.data.swap + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.swap.converter.transaction.SavedSwapStatusConverter +import com.tangem.data.swap.converter.transaction.SavedSwapTransactionConverter +import com.tangem.data.swap.converter.transaction.SavedSwapTransactionListConverter +import com.tangem.data.swap.models.LastSwappedCryptoCurrencyDTO +import com.tangem.data.swap.models.SwapStatusDTO +import com.tangem.data.swap.models.SwapTransactionDTO +import com.tangem.data.swap.models.SwapTransactionListDTO +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectList +import com.tangem.datasource.local.preferences.utils.getObjectListSync +import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.SwapTransactionRepository +import com.tangem.domain.swap.models.SwapStatusModel +import com.tangem.domain.swap.models.SwapTransactionListModel +import com.tangem.domain.swap.models.SwapTransactionModel +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.requireColdWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +internal class DefaultSwapTransactionRepository( + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, + excludedBlockchains: ExcludedBlockchains, +) : SwapTransactionRepository { + + private val listConverter by lazy(LazyThreadSafetyMode.NONE) { + SavedSwapTransactionListConverter(excludedBlockchains) + } + private val converter by lazy(LazyThreadSafetyMode.NONE) { + SavedSwapTransactionConverter(excludedBlockchains) + } + private val savedStatusConverter by lazy(LazyThreadSafetyMode.NONE) { + SavedSwapStatusConverter() + } + private val userTokensResponseFactory = UserTokensResponseFactory() + + override suspend fun storeTransaction( + userWalletId: UserWalletId, + fromCryptoCurrency: CryptoCurrency, + toCryptoCurrency: CryptoCurrency, + transaction: SwapTransactionModel, + ) { + transaction.status?.let { + storeTransactionState( + txId = transaction.txId, + status = it, + refundTokenCurrency = null, + ) + } + appPreferencesStore.editData { mutablePreferences -> + val savedTransactions: List? = mutablePreferences.getObjectList( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + ) + val tokenTransactions = savedTransactions + ?.firstOrNull { + it.checkId( + checkUserWalletId = userWalletId, + fromCurrencyId = fromCryptoCurrency.id, + toCurrencyId = toCryptoCurrency.id, + ) + } + ?.transactions + ?.addOrReplace( + item = converter.convert(transaction), + predicate = { it.txId == transaction.txId }, + ) ?: listOf(converter.convert(transaction)) + + mutablePreferences.setObject( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + value = savedTransactions?.updateList( + userWalletId = userWalletId, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + transactions = tokenTransactions, + ) ?: listOf( + listConverter.default( + userWalletId = userWalletId, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + tokenTransactions = listOf(converter.convert(transaction)), + ), + ), + ) + } + } + + override suspend fun getTransactions( + userWallet: UserWallet, + cryptoCurrencyId: CryptoCurrency.ID, + ): Flow?> { + return withContext(dispatchers.io) { + val txStatuses = appPreferencesStore.getObjectMapSync( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ) + appPreferencesStore.getObjectList( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + ).map { savedTransactions -> + val currencyTxs = savedTransactions + ?.filter { + it.userWalletId == userWallet.walletId.stringValue && + ( + it.toCryptoCurrencyId == cryptoCurrencyId.value || + it.fromCryptoCurrencyId == cryptoCurrencyId.value + ) + } + + currencyTxs?.mapNotNull { + listConverter.convertBack( + value = it, + scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + txStatuses = txStatuses, + ) + } + }.flowOn(dispatchers.io) + } + } + + override suspend fun removeTransaction( + userWalletId: UserWalletId, + fromCryptoCurrency: CryptoCurrency, + toCryptoCurrency: CryptoCurrency, + txId: String, + ) { + clearTransactionsStatuses(txId = txId) + appPreferencesStore.editData { mutablePreferences -> + val savedList: List? = mutablePreferences.getObjectList( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + ) + val tokenTransactions = savedList + ?.firstOrNull { + it.checkId( + checkUserWalletId = userWalletId, + fromCurrencyId = fromCryptoCurrency.id, + toCurrencyId = toCryptoCurrency.id, + ) + } + ?.transactions + ?.filterNot { it.txId == txId } + + val editedList = + if (tokenTransactions.isNullOrEmpty()) { + savedList?.filterNot { + it.checkId( + checkUserWalletId = userWalletId, + fromCurrencyId = fromCryptoCurrency.id, + toCurrencyId = toCryptoCurrency.id, + ) + } + } else { + savedList.updateList( + userWalletId = userWalletId, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + transactions = tokenTransactions, + ) + } + + if (editedList.isNullOrEmpty()) { + mutablePreferences.remove(key = PreferencesKeys.SWAP_TRANSACTIONS_KEY) + } else { + mutablePreferences.setObject( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + value = editedList, + ) + } + } + } + + override suspend fun storeTransactionState( + txId: String, + status: SwapStatusModel, + refundTokenCurrency: CryptoCurrency?, + ) { + appPreferencesStore.editData { mutablePreferences -> + val savedMap = mutablePreferences.getObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ) + + val updatesMap = savedMap.toMutableMap() + updatesMap[txId] = savedStatusConverter.convertBack( + status.copy( + refundTokensResponse = refundTokenCurrency?.let { + userTokensResponseFactory.createResponseToken(refundTokenCurrency) + }, + ), + ) + + mutablePreferences.setObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + value = updatesMap, + ) + } + } + + override suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String? { + val lastSwappedCurrencies = appPreferencesStore.getObjectListSync( + key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY, + ) + + return lastSwappedCurrencies.find { userWalletId.stringValue == it.userWalletId }?.cryptoCurrencyId + } + + override suspend fun storeLastSwappedCryptoCurrencyId( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ) { + appPreferencesStore.editData { mutablePreferences -> + val lastSwappedCryptoCurrencies: List? = mutablePreferences.getObjectList( + key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY, + ) + + val newList = if (lastSwappedCryptoCurrencies != null) { + lastSwappedCryptoCurrencies.filter { + it.userWalletId != userWalletId.stringValue + } + LastSwappedCryptoCurrencyDTO(userWalletId.stringValue, cryptoCurrencyId.value) + } else { + listOf(LastSwappedCryptoCurrencyDTO(userWalletId.stringValue, cryptoCurrencyId.value)) + } + + mutablePreferences.setObjectList( + key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY, + value = newList, + ) + } + } + + private fun SwapTransactionListDTO.checkId( + checkUserWalletId: UserWalletId, + fromCurrencyId: CryptoCurrency.ID, + toCurrencyId: CryptoCurrency.ID, + ): Boolean { + return userWalletId == checkUserWalletId.stringValue && + toCryptoCurrencyId == toCurrencyId.value && + fromCryptoCurrencyId == fromCurrencyId.value + } + + private fun List.updateList( + userWalletId: UserWalletId, + fromCryptoCurrency: CryptoCurrency, + toCryptoCurrency: CryptoCurrency, + transactions: List, + ): List { + return addOrReplace( + item = listConverter.default( + userWalletId = userWalletId, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + tokenTransactions = transactions, + ), + predicate = { + it.checkId( + checkUserWalletId = userWalletId, + fromCurrencyId = fromCryptoCurrency.id, + toCurrencyId = toCryptoCurrency.id, + ) + }, + ) + } + + private suspend fun clearTransactionsStatuses(txId: String) { + appPreferencesStore.editData { mutablePreferences -> + val savedList = mutablePreferences.getObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ) + val editedList = savedList.filterNot { it.key == txId } + + if (editedList.isEmpty()) { + mutablePreferences.remove(key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY) + } else { + mutablePreferences.setObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + value = editedList, + ) + } + } + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/SwapStatusConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/SwapStatusConverter.kt new file mode 100644 index 0000000000..33ae4fa064 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/SwapStatusConverter.kt @@ -0,0 +1,24 @@ +package com.tangem.data.swap.converter + +import com.tangem.datasource.api.express.models.response.ExchangeStatusResponse +import com.tangem.domain.swap.models.SwapStatus +import com.tangem.domain.swap.models.SwapStatusModel +import com.tangem.utils.converter.Converter + +internal class SwapStatusConverter : Converter { + override fun convert(value: ExchangeStatusResponse): SwapStatusModel { + return SwapStatusModel( + providerId = value.providerId, + status = SwapStatus.entries.firstOrNull { + it.name.lowercase() == value.status.name.lowercase() + }, + txId = value.externalTxId, + txExternalUrl = value.externalTxUrl, + txExternalId = value.externalTxId, + refundNetwork = value.refundNetwork, + refundContractAddress = value.refundContractAddress, + createdAt = value.createdAt, + averageDuration = value.averageDuration, + ) + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt new file mode 100644 index 0000000000..c3d8f36903 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt @@ -0,0 +1,38 @@ +package com.tangem.data.swap.converter.transaction + +import com.tangem.data.swap.models.SavedSwapStatus +import com.tangem.data.swap.models.SwapStatusDTO +import com.tangem.domain.swap.models.SwapStatus +import com.tangem.domain.swap.models.SwapStatusModel +import com.tangem.utils.converter.TwoWayConverter + +internal class SavedSwapStatusConverter : TwoWayConverter { + + override fun convert(value: SwapStatusDTO) = SwapStatusModel( + providerId = value.providerId, + status = SwapStatus.entries.firstOrNull { + it.name.lowercase() == value.status?.name?.lowercase() + }, + txId = value.txExternalId, + txExternalUrl = value.txExternalUrl, + txExternalId = value.txExternalId, + refundNetwork = value.refundNetwork, + refundContractAddress = value.refundContractAddress, + createdAt = value.createdAt, + averageDuration = value.averageDuration, + ) + + override fun convertBack(value: SwapStatusModel) = SwapStatusDTO( + providerId = value.providerId, + status = SavedSwapStatus.entries.firstOrNull { + it.name.lowercase() == value.status?.name?.lowercase() + }, + txId = value.txExternalId, + txExternalUrl = value.txExternalUrl, + txExternalId = value.txExternalId, + refundNetwork = value.refundNetwork, + refundContractAddress = value.refundContractAddress, + createdAt = value.createdAt, + averageDuration = value.averageDuration, + ) +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt new file mode 100644 index 0000000000..ab7b21e61d --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt @@ -0,0 +1,60 @@ +package com.tangem.data.swap.converter.transaction + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.swap.models.SwapStatusDTO +import com.tangem.data.swap.models.SwapTransactionDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.swap.models.SwapTransactionModel +import com.tangem.utils.converter.TwoWayConverter + +internal class SavedSwapTransactionConverter( + excludedBlockchains: ExcludedBlockchains, +) : TwoWayConverter { + private val responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) + private val statusConverter by lazy(LazyThreadSafetyMode.NONE) { + SavedSwapStatusConverter() + } + + override fun convert(value: SwapTransactionModel) = SwapTransactionDTO( + txId = value.txId, + timestamp = value.timestamp, + fromCryptoAmount = value.fromCryptoAmount, + toCryptoAmount = value.toCryptoAmount, + provider = value.provider, + status = value.status, + ) + + override fun convertBack(value: SwapTransactionDTO) = SwapTransactionModel( + txId = value.txId, + timestamp = value.timestamp, + fromCryptoAmount = value.fromCryptoAmount, + toCryptoAmount = value.toCryptoAmount, + provider = value.provider, + status = value.status, + ) + + fun convertBack( + value: SwapTransactionDTO, + scanResponse: ScanResponse, + txStatuses: Map, + ): SwapTransactionModel { + val status = txStatuses[value.txId] + val refundCurrency = status?.refundTokensResponse?.let { id -> + responseCryptoCurrenciesFactory.createCurrency( + responseToken = id, + scanResponse = scanResponse, + ) + } + val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency) + + return SwapTransactionModel( + txId = value.txId, + timestamp = value.timestamp, + fromCryptoAmount = value.fromCryptoAmount, + toCryptoAmount = value.toCryptoAmount, + provider = value.provider, + status = statusWithRefundCurrency?.let(statusConverter::convert), + ) + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt new file mode 100644 index 0000000000..176c1eb8f6 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt @@ -0,0 +1,83 @@ +package com.tangem.data.swap.converter.transaction + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.swap.models.SwapStatusDTO +import com.tangem.data.swap.models.SwapTransactionDTO +import com.tangem.data.swap.models.SwapTransactionListDTO +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.swap.models.SwapTransactionListModel +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.converter.Converter + +internal class SavedSwapTransactionListConverter( + excludedBlockchains: ExcludedBlockchains, +) : Converter { + + private val responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) + private val userTokensResponseFactory = UserTokensResponseFactory() + private val savedSwapTransactionConverter by lazy(LazyThreadSafetyMode.NONE) { + SavedSwapTransactionConverter(excludedBlockchains) + } + + override fun convert(value: SwapTransactionListModel) = SwapTransactionListDTO( + userWalletId = value.userWalletId, + fromCryptoCurrencyId = value.fromCryptoCurrencyId, + toCryptoCurrencyId = value.toCryptoCurrencyId, + fromTokensResponse = userTokensResponseFactory.createResponseToken( + value.fromCryptoCurrency, + ), + toTokensResponse = userTokensResponseFactory.createResponseToken( + value.toCryptoCurrency, + ), + transactions = savedSwapTransactionConverter.convertList(value.transactions), + ) + + fun convertBack( + value: SwapTransactionListDTO, + scanResponse: ScanResponse, + txStatuses: Map, + ): SwapTransactionListModel? { + val fromToken = value.fromTokensResponse + val toToken = value.toTokensResponse + return if (fromToken == null || toToken == null) { + null + } else { + val fromCryptoCurrency = responseCryptoCurrenciesFactory.createCurrency( + responseToken = fromToken, + scanResponse = scanResponse, + ) ?: return null + val toCryptoCurrency = responseCryptoCurrenciesFactory.createCurrency( + responseToken = toToken, + scanResponse = scanResponse, + ) ?: return null + + return SwapTransactionListModel( + transactions = value.transactions.map { tx -> + savedSwapTransactionConverter.convertBack(tx, scanResponse, txStatuses) + }, + userWalletId = value.userWalletId, + fromCryptoCurrencyId = value.fromCryptoCurrencyId, + toCryptoCurrencyId = value.toCryptoCurrencyId, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + ) + } + } + + fun default( + userWalletId: UserWalletId, + fromCryptoCurrency: CryptoCurrency, + toCryptoCurrency: CryptoCurrency, + tokenTransactions: List, + ) = SwapTransactionListDTO( + userWalletId = userWalletId.stringValue, + fromCryptoCurrencyId = fromCryptoCurrency.id.value, + toCryptoCurrencyId = toCryptoCurrency.id.value, + fromTokensResponse = userTokensResponseFactory.createResponseToken(fromCryptoCurrency), + toTokensResponse = userTokensResponseFactory.createResponseToken(toCryptoCurrency), + transactions = tokenTransactions, + ) +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt index 0ee1971b25..bd75b9d1ad 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt @@ -1,9 +1,11 @@ package com.tangem.data.swap.di import com.squareup.moshi.Moshi +import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.express.converter.ExpressErrorConverter import com.tangem.data.swap.DefaultSwapErrorResolver import com.tangem.data.swap.DefaultSwapRepositoryV2 +import com.tangem.data.swap.DefaultSwapTransactionRepository import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.di.NetworkMoshi @@ -11,6 +13,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.express.ExpressRepository import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.SwapTransactionRepository import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -49,4 +52,18 @@ internal object SwapDataModule { currencyStatusOperations = currencyStatusOperations, ) } + + @Provides + @Singleton + fun provideSwapTransactionRepository( + coroutineDispatcher: CoroutineDispatcherProvider, + appPreferencesStore: AppPreferencesStore, + excludedBlockchains: ExcludedBlockchains, + ): SwapTransactionRepository { + return DefaultSwapTransactionRepository( + appPreferencesStore = appPreferencesStore, + dispatchers = coroutineDispatcher, + excludedBlockchains = excludedBlockchains, + ) + } } \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/models/LastSwappedCryptoCurrencyDTO.kt b/data/swap/src/main/java/com/tangem/data/swap/models/LastSwappedCryptoCurrencyDTO.kt new file mode 100644 index 0000000000..61d738acd0 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/models/LastSwappedCryptoCurrencyDTO.kt @@ -0,0 +1,12 @@ +package com.tangem.data.swap.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class LastSwappedCryptoCurrencyDTO( + @Json(name = "userWalletId") + val userWalletId: String, + @Json(name = "cryptoCurrencyId") + val cryptoCurrencyId: String, +) \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/models/SwapStatusDTO.kt b/data/swap/src/main/java/com/tangem/data/swap/models/SwapStatusDTO.kt new file mode 100644 index 0000000000..45c37a2005 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/models/SwapStatusDTO.kt @@ -0,0 +1,78 @@ +package com.tangem.data.swap.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.currency.CryptoCurrency +import org.joda.time.DateTime + +@JsonClass(generateAdapter = true) +internal data class SwapStatusDTO( + @Json(name = "providerId") + val providerId: String, + @Json(name = "status") + val status: SavedSwapStatus? = null, + @Json(name = "txId") + val txId: String? = null, + @Json(name = "txExternalUrl") + val txExternalUrl: String? = null, + @Json(name = "txExternalId") + val txExternalId: String? = null, + @Json(name = "refundNetwork") + val refundNetwork: String? = null, + @Json(name = "refundContractAddress") + val refundContractAddress: String? = null, + @Json(name = "refundTokensResponse") + val refundTokensResponse: UserTokensResponse.Token? = null, + @Json(ignore = true) + val refundCurrency: CryptoCurrency? = null, + @Json(name = "createdAt") + val createdAt: DateTime? = null, + @Json(name = "averageDuration") + val averageDuration: Int? = null, +) + +@JsonClass(generateAdapter = false) +internal enum class SavedSwapStatus { + @Json(name = "New") + New, + + @Json(name = "Waiting") + Waiting, + + @Json(name = "WaitingTxHash") + WaitingTxHash, + + @Json(name = "Confirming") + Confirming, + + @Json(name = "Verifying") + Verifying, + + @Json(name = "Exchanging") + Exchanging, + + @Json(name = "Failed") + Failed, + + @Json(name = "Sending") + Sending, + + @Json(name = "Finished") + Finished, + + @Json(name = "Refunded") + Refunded, + + @Json(name = "Cancelled") + Cancelled, + + @Json(name = "TxFailed") + TxFailed, + + @Json(name = "Unknown") + Unknown, + + @Json(name = "Paused") + Paused, +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/models/SwapTransactionListDTO.kt b/data/swap/src/main/java/com/tangem/data/swap/models/SwapTransactionListDTO.kt new file mode 100644 index 0000000000..e5249da91f --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/models/SwapTransactionListDTO.kt @@ -0,0 +1,40 @@ +package com.tangem.data.swap.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.swap.models.SwapStatusModel +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +internal data class SwapTransactionListDTO( + @Json(name = "userWalletId") + val userWalletId: String, + @Json(name = "fromCryptoCurrencyId") + val fromCryptoCurrencyId: String, + @Json(name = "toCryptoCurrencyId") + val toCryptoCurrencyId: String, + @Json(name = "fromTokensResponse") + val fromTokensResponse: UserTokensResponse.Token? = null, + @Json(name = "toTokensResponse") + val toTokensResponse: UserTokensResponse.Token? = null, + @Json(name = "transactions") + val transactions: List, +) + +@JsonClass(generateAdapter = true) +internal data class SwapTransactionDTO( + @Json(name = "txId") + val txId: String, + @Json(name = "timestamp") + val timestamp: Long, + @Json(name = "fromCryptoAmount") + val fromCryptoAmount: BigDecimal, + @Json(name = "toCryptoAmount") + val toCryptoAmount: BigDecimal, + @Json(name = "provider") + val provider: ExpressProvider, + @Json(name = "status") + val status: SwapStatusModel? = null, +) \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapStatusModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapStatusModel.kt new file mode 100644 index 0000000000..53cfd45e13 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapStatusModel.kt @@ -0,0 +1,62 @@ +package com.tangem.domain.swap.models + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.currency.CryptoCurrency +import org.joda.time.DateTime + +/** + * Swapped status model + */ +data class SwapStatusModel( + val providerId: String, + val status: SwapStatus? = null, + val txId: String? = null, + val txExternalUrl: String? = null, + val txExternalId: String? = null, + val refundNetwork: String? = null, + val refundContractAddress: String? = null, + val refundTokensResponse: UserTokensResponse.Token? = null, + val refundCurrency: CryptoCurrency? = null, + val createdAt: DateTime? = null, + val averageDuration: Int? = null, +) { + val hasLongTime: Boolean + get() = if (createdAt != null && averageDuration != null) { + DateTime.now().minusSeconds(averageDuration * 5) > createdAt + } else { + false + } +} + +enum class SwapStatus { + New, + Waiting, + WaitingTxHash, + Confirming, + Verifying, + Exchanging, + Failed, + Sending, + Finished, + Refunded, + Cancelled, + TxFailed, + Unknown, + Paused, + ; + + val isTerminal: Boolean + get() = this == Refunded || + this == Finished || + this == Cancelled || + this == TxFailed || + this == Paused || + this == Unknown + + val isAutoDisposable: Boolean + get() = this == Finished + + companion object { + fun SwapStatus?.isFailed(): Boolean = this == Failed || this == TxFailed + } +} \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt new file mode 100644 index 0000000000..e296ca6fe3 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.swap.models + +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.models.currency.CryptoCurrency +import java.math.BigDecimal + +/** + * List of saved swap transactions + */ +data class SwapTransactionListModel( + val userWalletId: String, + val fromCryptoCurrencyId: String, + val toCryptoCurrencyId: String, + val fromCryptoCurrency: CryptoCurrency, + val toCryptoCurrency: CryptoCurrency, + val transactions: List, +) + +/** + * Saved swap transactions + */ +data class SwapTransactionModel( + val txId: String, + val timestamp: Long, + val fromCryptoAmount: BigDecimal, + val toCryptoAmount: BigDecimal, + val provider: ExpressProvider, + val status: SwapStatusModel? = null, +) \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt new file mode 100644 index 0000000000..5efa512937 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt @@ -0,0 +1,80 @@ +package com.tangem.domain.swap + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.SwapStatusModel +import com.tangem.domain.swap.models.SwapTransactionListModel +import com.tangem.domain.swap.models.SwapTransactionModel +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Swap repository for statuses + */ +interface SwapTransactionRepository { + + /** + * Store new swap transaction + * + * @param userWalletId selected user wallet id + * @param fromCryptoCurrency currency swap from + * @param toCryptoCurrency currency swap to + * @param transaction swap transaction + */ + suspend fun storeTransaction( + userWalletId: UserWalletId, + fromCryptoCurrency: CryptoCurrency, + toCryptoCurrency: CryptoCurrency, + transaction: SwapTransactionModel, + ) + + /** + * Get list of swap transactions + * + * @param userWallet selected user wallet + * @param cryptoCurrencyId transactions for specific crypto currency + */ + suspend fun getTransactions( + userWallet: UserWallet, + cryptoCurrencyId: CryptoCurrency.ID, + ): Flow?> + + /** + * Remove stores swap transaction + * + * @param userWalletId selected user wallet id + * @param fromCryptoCurrency currency swap from + * @param toCryptoCurrency currency swap to + * @param txId transaction id to remove + */ + suspend fun removeTransaction( + userWalletId: UserWalletId, + fromCryptoCurrency: CryptoCurrency, + toCryptoCurrency: CryptoCurrency, + txId: String, + ) + + /** + * Update swap transaction + * + * @param txId transaction id to update + * @param status new transaction status + * @param refundTokenCurrency refund token + */ + suspend fun storeTransactionState(txId: String, status: SwapStatusModel, refundTokenCurrency: CryptoCurrency?) + + /** + * Save last swapped crypto currency token + * + * @param userWalletId selected user wallet id + * @param cryptoCurrencyId last swapped currency id + */ + suspend fun storeLastSwappedCryptoCurrencyId(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID) + + /** + * Get last swapped crypto currency token + * + * @param userWalletId selected user wallet id + */ + suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String? +} \ No newline at end of file From 357955b1dd0fa351a4cb895412d9893081769ab8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Jun 2025 16:31:35 +0500 Subject: [PATCH 55/99] Updated on 2026-08-14 --- .../tangem/tap/di/domain/SwapDomainModule.kt | 38 +++++++++ .../converters/error/OnrampErrorConverter.kt | 1 + .../data/swap/DefaultSwapRepositoryV2.kt | 38 +++++++++ .../domain/swap/models/SwapCurrencies.kt | 26 ++++++ .../domain/swap/models/SwapDirection.kt | 16 ++++ .../domain/swap/models/SwapQuoteModel.kt | 17 ++++ .../tangem/domain/swap/SwapRepositoryV2.kt | 27 +++++++ .../swap/usecase/GetSwapPairsUseCase.kt | 80 +++++++++++++++++++ .../swap/usecase/GetSwapQuoteUseCase.kt | 45 +++++++++++ .../swap/usecase/SelectInitialPairUseCase.kt | 71 ++++++++++++++++ 10 files changed, 359 insertions(+) create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt create mode 100644 domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt create mode 100644 domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapQuoteUseCase.kt create mode 100644 domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt index a392d9c30e..f0ba15b856 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt @@ -2,7 +2,11 @@ package com.tangem.tap.di.domain import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.SwapTransactionRepository +import com.tangem.domain.swap.usecase.GetSwapPairsUseCase +import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase +import com.tangem.domain.swap.usecase.SelectInitialPairUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase import dagger.Module import dagger.Provides @@ -35,4 +39,38 @@ internal object SwapDomainModule { swapErrorResolver = swapErrorResolver, ) } + + @Provides + @Singleton + fun provideGetSwapPairsUseCase( + swapRepositoryV2: SwapRepositoryV2, + swapErrorResolver: SwapErrorResolver, + ): GetSwapPairsUseCase { + return GetSwapPairsUseCase( + swapRepositoryV2 = swapRepositoryV2, + swapErrorResolver = swapErrorResolver, + ) + } + + @Provides + @Singleton + fun provideSelectInitialPairUseCase( + swapTransactionRepository: SwapTransactionRepository, + ): SelectInitialPairUseCase { + return SelectInitialPairUseCase( + swapTransactionRepository = swapTransactionRepository, + ) + } + + @Provides + @Singleton + fun provideGetSwapQuoteUseCase( + swapRepositoryV2: SwapRepositoryV2, + swapErrorResolver: SwapErrorResolver, + ): GetSwapQuoteUseCase { + return GetSwapQuoteUseCase( + swapRepositoryV2 = swapRepositoryV2, + swapErrorResolver = swapErrorResolver, + ) + } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt index 895da30c18..1abd3b4d3d 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.utils.converter.Converter +@Deprecated("Use ExpressErrorConverter") internal class OnrampErrorConverter( private val jsonAdapter: JsonAdapter, ) : Converter { diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 1931f346f2..9adca6af89 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -9,9 +9,11 @@ import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.express.ExpressRepository import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.swap.models.SwapQuoteModel import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.wallets.models.UserWallet @@ -22,6 +24,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import timber.log.Timber +import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList") @@ -34,6 +37,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( ) : SwapRepositoryV2 { private val tokenInfoConverter = TokenInfoConverter() + override suspend fun getPairs( userWallet: UserWallet, initialCurrency: CryptoCurrency, @@ -115,6 +119,40 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( }.awaitAll().filterNotNull() } + override suspend fun getSwapQuote( + userWallet: UserWallet, + fromCryptoCurrency: CryptoCurrency, + toCryptoCurrency: CryptoCurrency, + fromAmount: BigDecimal, + provider: ExpressProvider, + rateType: ExpressRateType, + ): SwapQuoteModel = withContext(coroutineDispatcher.io) { + val response = tangemExpressApi.getExchangeQuote( + fromAmount = fromAmount.movePointRight(fromCryptoCurrency.decimals).toString(), + fromNetwork = fromCryptoCurrency.network.backendId, + fromContractAddress = fromCryptoCurrency.getContractAddress(), + fromDecimals = fromCryptoCurrency.decimals, + toNetwork = toCryptoCurrency.network.backendId, + toContractAddress = toCryptoCurrency.getContractAddress(), + toDecimals = toCryptoCurrency.decimals, + providerId = provider.providerId, + rateType = rateType.name.lowercase(), + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ).getOrThrow() + + val toTokenAmount = requireNotNull(response.toAmount.toBigDecimalOrNull()?.movePointLeft(response.toDecimals)) + + return@withContext SwapQuoteModel( + provider = provider, + toTokenAmount = toTokenAmount, + allowanceContract = response.allowanceContract, + ) + } + private suspend fun CoroutineScope.getPairsInternal( userWallet: UserWallet, initialCurrency: CryptoCurrency, diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt index 6a1f16be09..ef559c44e9 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -3,17 +3,43 @@ package com.tangem.domain.swap.models import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.tokens.model.CryptoCurrencyStatus +/** + * Model of currencies available to swap + * + * @param fromGroup group of currencies available swap from + * @param toGroup group of currencies available swap to + */ data class SwapCurrencies( val fromGroup: SwapCurrenciesGroup, val toGroup: SwapCurrenciesGroup, ) +/** + * Return swap group depending on [swapDirection] + */ +fun SwapCurrencies.getGroupWithReverse(swapDirection: SwapDirection): SwapCurrenciesGroup { + return when (swapDirection) { + SwapDirection.Reverse -> fromGroup + SwapDirection.Direct -> toGroup + } +} + +/** + * Swap group model + * + * @param available list of available currencies to swap + * @param available list of unavailable currencies to swap + * @param afterSearch flag indicates whether user searched token + */ data class SwapCurrenciesGroup( val available: List, val unavailable: List, val afterSearch: Boolean, ) +/** + * Crypto currency with providers to swap + */ data class SwapCryptoCurrency( val currencyStatus: CryptoCurrencyStatus, val providers: List, diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt new file mode 100644 index 0000000000..9c60cc2ec0 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.swap.models + +import com.tangem.domain.swap.models.SwapDirection.Direct +import com.tangem.domain.swap.models.SwapDirection.Reverse + +/** + * Swap direction + * + * Initial currency can be swap to or from. + * If swap being swap from direction is [Direct], + * Otherwise direction is [Reverse] + */ +enum class SwapDirection { + Direct, + Reverse, +} \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt new file mode 100644 index 0000000000..a808f07e67 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.swap.models + +import com.tangem.domain.express.models.ExpressProvider +import java.math.BigDecimal + +/** + * Quote model holds data about current amounts of swaps and fees + * + * @property provider swap provider + * @property toTokenAmount amount of token you want to receive + * @property allowanceContract whether swap occurs via third token + */ +data class SwapQuoteModel( + val provider: ExpressProvider, + val toTokenAmount: BigDecimal, + val allowanceContract: String?, +) \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index b2c768cdc7..7bb2c091ba 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -1,10 +1,17 @@ package com.tangem.domain.swap +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.swap.models.SwapQuoteModel import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import java.math.BigDecimal +/** + * Swap repository + */ interface SwapRepositoryV2 { /** @@ -26,4 +33,24 @@ interface SwapRepositoryV2 { initialCurrency: CryptoCurrency, cryptoCurrencyList: List, ): List + + /** + * Returns swap quotes on selected pair + * + * @param userWallet selected user wallet + * @param fromCryptoCurrency currency being swapped from + * @param toCryptoCurrency currency being swapped to + * @param fromAmount swap amount + * @param provider selected express provider + * @param rateType rate type + */ + @Suppress("LongParameterList") + suspend fun getSwapQuote( + userWallet: UserWallet, + fromCryptoCurrency: CryptoCurrency, + toCryptoCurrency: CryptoCurrency, + fromAmount: BigDecimal, + provider: ExpressProvider, + rateType: ExpressRateType, + ): SwapQuoteModel } \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt new file mode 100644 index 0000000000..d0a1cd0bf2 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt @@ -0,0 +1,80 @@ +package com.tangem.domain.swap.usecase + +import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.models.SwapCryptoCurrency +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapCurrenciesGroup +import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet + +/** + * Get list of swap pairs + */ +class GetSwapPairsUseCase( + private val swapRepositoryV2: SwapRepositoryV2, + private val swapErrorResolver: SwapErrorResolver, +) { + + /** + * @param userWallet selected user wallet + * @param initialCurrency initial currency to swap (to or from) + * @param cryptoCurrencyStatusList list of added cryptocurrencies + */ + suspend operator fun invoke( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyStatusList: List, + ) = Either.catch { + val pairs = swapRepositoryV2.getPairs( + userWallet = userWallet, + initialCurrency = initialCurrency, + cryptoCurrencyStatusList = cryptoCurrencyStatusList, + ) + + val fromGroup = pairs.groupPairs( + initialCurrency = initialCurrency, + filteringCurrency = { it.from }, + groupingCurrency = { it.to }, + cryptoCurrencyStatusList = cryptoCurrencyStatusList, + ) + val toGroup = pairs.groupPairs( + initialCurrency = initialCurrency, + filteringCurrency = { it.to }, + groupingCurrency = { it.from }, + cryptoCurrencyStatusList = cryptoCurrencyStatusList, + ) + + SwapCurrencies( + fromGroup = fromGroup, + toGroup = toGroup, + ) + }.mapLeft(swapErrorResolver::resolve) + + private fun List.groupPairs( + initialCurrency: CryptoCurrency, + filteringCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + groupingCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + cryptoCurrencyStatusList: List, + ): SwapCurrenciesGroup { + val availableCryptoCurrencies = filter { pair -> filteringCurrency(pair).currency.id == initialCurrency.id } + // Search available to swap currency + .filter { pair -> + cryptoCurrencyStatusList.any { currencyStatus -> + currencyStatus.currency.id == pair.to.currency.id + } + }.map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) } + + val unavailableCryptoCurrencies = + cryptoCurrencyStatusList - availableCryptoCurrencies.map { it.currencyStatus }.toSet() + + return SwapCurrenciesGroup( + available = availableCryptoCurrencies, + unavailable = unavailableCryptoCurrencies.map { pair -> SwapCryptoCurrency(pair, emptyList()) }, + afterSearch = false, + ) + } +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapQuoteUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapQuoteUseCase.kt new file mode 100644 index 0000000000..f0594c8f02 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapQuoteUseCase.kt @@ -0,0 +1,45 @@ +package com.tangem.domain.swap.usecase + +import arrow.core.Either +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.models.SwapQuoteModel +import com.tangem.domain.wallets.models.UserWallet +import java.math.BigDecimal + +/** + * Get swap quote for selected pair + */ +class GetSwapQuoteUseCase( + private val swapRepositoryV2: SwapRepositoryV2, + private val swapErrorResolver: SwapErrorResolver, +) { + + /** + * @param userWallet selected user wallet + * @param fromCryptoCurrency currency swap from + * @param toCryptoCurrency currency swap to + * @param fromAmount swap amount + * @param provider swap provider + */ + suspend operator fun invoke( + userWallet: UserWallet, + fromCryptoCurrency: CryptoCurrency, + toCryptoCurrency: CryptoCurrency, + fromAmount: BigDecimal, + provider: ExpressProvider, + ): Either = Either.catch { + swapRepositoryV2.getSwapQuote( + userWallet = userWallet, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + fromAmount = fromAmount, + provider = provider, + rateType = ExpressRateType.Float, // todo rate type + ) + }.mapLeft(swapErrorResolver::resolve) +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt new file mode 100644 index 0000000000..f348bdc52a --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.swap.usecase + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.SwapTransactionRepository +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapCurrenciesGroup +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.getGroupWithReverse +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.utils.extensions.orZero + +/** + * Select initial pair to swap + */ +class SelectInitialPairUseCase( + private val swapTransactionRepository: SwapTransactionRepository, +) { + + /** + * @param userWallet selected user wallet + * @param primaryCryptoCurrency initial currency + * @param secondaryCryptoCurrency selected currency + * @param swapCurrencies list of currencies with providers + * @param swapDirection swap direction + */ + suspend operator fun invoke( + userWallet: UserWallet, + primaryCryptoCurrency: CryptoCurrency, + secondaryCryptoCurrency: CryptoCurrency?, + swapCurrencies: SwapCurrencies, + swapDirection: SwapDirection, + ): CryptoCurrencyStatus? { + val swapCurrenciesGroup = swapCurrencies.getGroupWithReverse(swapDirection) + return tryToGetAlreadySelectedCurrency(secondaryCryptoCurrency, swapCurrenciesGroup) + ?: tryGetFromCache(userWallet, primaryCryptoCurrency, swapCurrenciesGroup) + ?: tryGetWithMaxAmount(swapCurrenciesGroup) + ?: swapCurrenciesGroup.available.firstOrNull()?.currencyStatus + } + + private fun tryToGetAlreadySelectedCurrency( + secondaryCryptoCurrency: CryptoCurrency?, + swapCurrenciesGroup: SwapCurrenciesGroup, + ): CryptoCurrencyStatus? { + return secondaryCryptoCurrency?.let { + swapCurrenciesGroup.available.firstOrNull { + secondaryCryptoCurrency.id == it.currencyStatus.currency.id + }?.currencyStatus + } + } + + private suspend fun tryGetFromCache( + userWallet: UserWallet, + primaryCryptoCurrency: CryptoCurrency, + swapCurrenciesGroup: SwapCurrenciesGroup, + ): CryptoCurrencyStatus? { + val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(userWallet.walletId) ?: return null + + return if (id != primaryCryptoCurrency.id.value) { + swapCurrenciesGroup.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus + } else { + null + } + } + + private fun tryGetWithMaxAmount(swapCurrenciesGroup: SwapCurrenciesGroup): CryptoCurrencyStatus? { + return swapCurrenciesGroup.available.maxByOrNull { + it.currencyStatus.value.fiatAmount.orZero() + }?.currencyStatus + } +} \ No newline at end of file From 0ff4768da951f9436cfc865dd4494e000f6d42f0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Jun 2025 12:26:24 +0300 Subject: [PATCH 56/99] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 9 --- ci_resources/build_ci_gradle.properties | 13 ---- ci_resources/find-latest-release-branch.sh | 63 ------------------- ci_resources/tests_ci_gradle.properties | 13 ---- fastlane/Fastfile | 8 +-- tangem-android-tools | 2 +- 6 files changed, 5 insertions(+), 103 deletions(-) delete mode 100644 ci_resources/build_ci_gradle.properties delete mode 100755 ci_resources/find-latest-release-branch.sh delete mode 100644 ci_resources/tests_ci_gradle.properties diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index d404225116..b5c7601cde 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -56,16 +56,7 @@ abstract class BaseTestCase : TestCase( hiltRule.inject() Intents.init() additionalBeforeSection() - - runBlocking { - delay(INIT_DELAY) - } - }.after { - runBlocking { - appPreferencesStore.editData { prefs -> prefs.clear() } - } - additionalAfterSection() Intents.release() } diff --git a/ci_resources/build_ci_gradle.properties b/ci_resources/build_ci_gradle.properties deleted file mode 100644 index bb0256c8c4..0000000000 --- a/ci_resources/build_ci_gradle.properties +++ /dev/null @@ -1,13 +0,0 @@ -org.gradle.jvmargs=-Xmx7g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -org.gradle.parallel=true -org.gradle.workers.max=8 -org.gradle.daemon=false -org.gradle.unsafe.configuration-cache=false -org.gradle.console=plain -org.gradle.caching=false - -android.useAndroidX=true -android.nonTransitiveRClass=false -android.aapt2FromMavenOverride=/opt/android-sdk/build-tools/34.0.0/aapt2 - -kotlin.incremental=true diff --git a/ci_resources/find-latest-release-branch.sh b/ci_resources/find-latest-release-branch.sh deleted file mode 100755 index 091cb911e9..0000000000 --- a/ci_resources/find-latest-release-branch.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -# Input args -if [ -z "$1" ]; then - echo "Error: No base branch argument provided or empty string given." - echo "Usage: $0 " - exit 1 -fi -base_branch=$1 - -# Initialize arrays to store branch-commit mapping -declare -a branches -declare -a commits - -# Source refs to find branches from -local_refs="refs/heads/releases/*" # For debug and development -#TODO remove pre_release branches matching after 5.21 release -remote_refs=$(git for-each-ref --sort=-committerdate --format="%(refname:short)" refs/remotes/origin/ | grep -E "origin/([a-zA-Z0-9._-]+_pre_release|releases/[a-zA-Z0-9._-]+)$") - -# Iterate over all remote release branches, sorted by commit date (most recent first) -index=0 -for branch in $remote_refs; do - # Find the latest commit that is an ancestor of base_branch using --first-parent strategy - candidate_commit=$(git rev-list --first-parent "$branch..$base_branch" | tail -1) - - if [ -n "$candidate_commit" ]; then - branches[$index]="$branch" - commits[$index]="$candidate_commit" - index=$((index + 1)) - fi -done - -# Debug output of branches and commits -echo "Branches and their corresponding commits:" >&2 -for i in "${!branches[@]}"; do - echo "${branches[$i]} -> ${commits[$i]}" >&2 -done - -# Find the branch with the most recent commit -latest_branch="" -latest_commit="" -for i in "${!branches[@]}"; do - branch=${branches[$i]} - commit=${commits[$i]} - if [ -z "$latest_commit" ] || [ "$(git rev-list --count $latest_commit..$commit)" -gt 0 ]; then - latest_branch=$branch - latest_commit=$commit - fi -done - -# Output validation -if [ -z "$latest_branch" ]; then - echo "Error: Can't find the latest 'release/*' branch for the base branch '$base_branch'" >&2 - exit 2 -fi - -# Stripping 'origin/' prefix if needed -latest_branch="${latest_branch#origin/}" - -echo "$latest_branch" > "find-latest-release-branch.output" -echo "Latest release branch created directly from '$base_branch' or its ancestor: '$latest_branch'" diff --git a/ci_resources/tests_ci_gradle.properties b/ci_resources/tests_ci_gradle.properties deleted file mode 100644 index 860c64d2b5..0000000000 --- a/ci_resources/tests_ci_gradle.properties +++ /dev/null @@ -1,13 +0,0 @@ -org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=512m -XX:+UseParallelGC -XX:ParallelGCThreads=3 -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -org.gradle.parallel=true -org.gradle.workers.max=3 -org.gradle.daemon=false -org.gradle.unsafe.configuration-cache=false -org.gradle.console=plain -org.gradle.caching=false - -android.useAndroidX=true -android.nonTransitiveRClass=false -android.aapt2FromMavenOverride=/opt/android-sdk/build-tools/35.0.0/aapt2 - -kotlin.incremental=false diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 24ec4eb861..0dd13a52a2 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :android do desc "Run detekt" lane :detekt do - FileUtils.cp("../ci_resources/tests_ci_gradle.properties", "../gradle.properties") + FileUtils.cp("../tangem-android-tools/CI/gradle_properties/tests_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") gradle(task: "detekt") end @@ -27,7 +27,7 @@ platform :android do desc "Run tests" lane :test do FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") - FileUtils.cp("../ci_resources/tests_ci_gradle.properties", "../gradle.properties") + FileUtils.cp("../tangem-android-tools/CI/gradle_properties/tests_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") gradle(task: "testDebugUnitTest") end @@ -36,7 +36,7 @@ platform :android do desc "Build release AAB and APK" lane :buildRelease do |options| FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/release/google-services.json", "../app") - FileUtils.cp("../ci_resources/build_ci_gradle.properties", "../gradle.properties") + FileUtils.cp("../tangem-android-tools/CI/gradle_properties/build_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") gradle( @@ -69,7 +69,7 @@ platform :android do desc "Build internal APK Firebase App Distribution" lane :buildInternal do |options| FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") - FileUtils.cp("../ci_resources/build_ci_gradle.properties", "../gradle.properties") + FileUtils.cp("../tangem-android-tools/CI/gradle_properties/build_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") gradle( diff --git a/tangem-android-tools b/tangem-android-tools index 8d971eb9d9..034ad04fdf 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 8d971eb9d901358419da78408ecf7dc3f0c4331f +Subproject commit 034ad04fdf0810e338cb8898f50554874cec96d7 From b9bb9e1086f78b60f992c0b8dfbe2a695415a191 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Jun 2025 12:48:32 +0300 Subject: [PATCH 57/99] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../common/ui/userwallet/UserWalletItem.kt | 2 +- .../core/ui/components/fields/PinTextField.kt | 6 + .../res/drawable/img_tangem_logo_90_24.xml | 0 features/welcome/api/.gitignore | 1 + features/welcome/api/build.gradle.kts | 22 ++ .../features/welcome/WelcomeComponent.kt | 14 ++ features/welcome/impl/.gitignore | 1 + features/welcome/impl/build.gradle.kts | 57 +++++ .../welcome/impl/DefaultWelcomeComponent.kt | 37 +++ .../features/welcome/impl/di/FeatureModule.kt | 31 +++ .../welcome/impl/model/WelcomeModel.kt | 18 ++ .../welcome/impl/ui/AddWalletBottomSheet.kt | 89 +++++++ .../features/welcome/impl/ui/Welcome.kt | 89 +++++++ .../welcome/impl/ui/WelcomeEnterAccessCode.kt | 94 +++++++ .../features/welcome/impl/ui/WelcomePlain.kt | 29 +++ .../welcome/impl/ui/WelcomeSelectWallet.kt | 230 ++++++++++++++++++ .../ui/state/AddWalletBottomSheetContentUM.kt | 14 ++ .../welcome/impl/ui/state/WalletUM.kt | 22 ++ .../welcome/impl/ui/state/WelcomeUM.kt | 27 ++ settings.gradle.kts | 7 +- 21 files changed, 789 insertions(+), 3 deletions(-) rename {features/wallet/impl => core/ui}/src/main/res/drawable/img_tangem_logo_90_24.xml (100%) create mode 100644 features/welcome/api/.gitignore create mode 100644 features/welcome/api/build.gradle.kts create mode 100644 features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt create mode 100644 features/welcome/impl/.gitignore create mode 100644 features/welcome/impl/build.gradle.kts create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/di/FeatureModule.kt create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/AddWalletBottomSheetContentUM.kt create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt create mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1c4d33f627..85b539e159 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -224,6 +224,8 @@ dependencies { implementation(projects.features.hotWallet.impl) implementation(projects.features.kyc.api) implementation(projects.features.kyc.impl) + implementation(projects.features.welcome.api) + implementation(projects.features.welcome.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 5d2c226351..698ec1ccf5 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -164,7 +164,7 @@ private fun NameAndInfo( @Suppress("MagicNumber") @Composable -private fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modifier) { +fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modifier) { val imageModifier = modifier .width(TangemTheme.dimens.size36) .height(TangemTheme.dimens.size24) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt index bc6d664fb1..7c7285b37d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.utils.StringsSigns.PASSWORD_VISUAL_CHAR +import kotlinx.coroutines.delay import kotlin.random.Random @Composable @@ -74,6 +75,11 @@ fun PinTextField( ) }, ) + + LaunchedEffect(Unit) { + delay(timeMillis = 200) + focusRequester.requestFocus() + } } @Suppress("MagicNumber") diff --git a/features/wallet/impl/src/main/res/drawable/img_tangem_logo_90_24.xml b/core/ui/src/main/res/drawable/img_tangem_logo_90_24.xml similarity index 100% rename from features/wallet/impl/src/main/res/drawable/img_tangem_logo_90_24.xml rename to core/ui/src/main/res/drawable/img_tangem_logo_90_24.xml diff --git a/features/welcome/api/.gitignore b/features/welcome/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/welcome/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/welcome/api/build.gradle.kts b/features/welcome/api/build.gradle.kts new file mode 100644 index 0000000000..92d3e16d53 --- /dev/null +++ b/features/welcome/api/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.welcome.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.common.routing) + + /** Domain models */ + implementation(projects.domain.wallets.models) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt new file mode 100644 index 0000000000..50b914548b --- /dev/null +++ b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.welcome + +import com.tangem.common.routing.entity.SerializableIntent +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface WelcomeComponent : ComposableContentComponent { + + data class Params( + val intent: SerializableIntent?, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/welcome/impl/.gitignore b/features/welcome/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/welcome/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/welcome/impl/build.gradle.kts b/features/welcome/impl/build.gradle.kts new file mode 100644 index 0000000000..499c0ab82e --- /dev/null +++ b/features/welcome/impl/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.welcome.impl" +} + +dependencies { + implementation(projects.features.welcome.api) + + /** Core */ + implementation(projects.core.configToggles) + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.common.routing) + implementation(projects.common.ui) + + /** Domain models */ + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + /** Domain */ + implementation(projects.domain.appCurrency) + implementation(projects.domain.wallets) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /** AndroidX */ + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.coil) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** Other */ + implementation(deps.arrow.core) + implementation(deps.decompose) + implementation(deps.decompose.ext.compose) + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt new file mode 100644 index 0000000000..7bc6a1d982 --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.welcome.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.welcome.WelcomeComponent +import com.tangem.features.welcome.impl.model.WelcomeModel +import com.tangem.features.welcome.impl.ui.Welcome +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultWelcomeComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: WelcomeComponent.Params, +) : WelcomeComponent, AppComponentContext by context { + + private val model: WelcomeModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + Welcome( + modifier = modifier, + state = state, + ) + } + + @AssistedFactory + interface Factory : WelcomeComponent.Factory { + override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent + } +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/di/FeatureModule.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/di/FeatureModule.kt new file mode 100644 index 0000000000..fa56c72481 --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/di/FeatureModule.kt @@ -0,0 +1,31 @@ +package com.tangem.features.welcome.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.welcome.WelcomeComponent +import com.tangem.features.welcome.impl.DefaultWelcomeComponent +import com.tangem.features.welcome.impl.model.WelcomeModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface FeatureModule { + + @Binds + fun bindComponentFactory(impl: DefaultWelcomeComponent.Factory): WelcomeComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(WelcomeModel::class) + fun provideModel(model: WelcomeModel): Model +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt new file mode 100644 index 0000000000..da1f8c9c21 --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -0,0 +1,18 @@ +package com.tangem.features.welcome.impl.model + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.features.welcome.impl.ui.state.WelcomeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class WelcomeModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + val uiState: StateFlow + field = MutableStateFlow(WelcomeUM.Plain) +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt new file mode 100644 index 0000000000..4331461929 --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt @@ -0,0 +1,89 @@ +package com.tangem.features.welcome.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM + +@Composable +fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + titleText = TextReference.Str("Add Wallet"), + containerColor = TangemTheme.colors.background.tertiary, + content = { Content(it) }, + ) +} + +@Composable +private fun Content(content: AddWalletBottomSheetContentUM) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + InputRowDefault( + text = TextReference.Str("Create New Wallet"), + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = 0, + lastIndex = 3, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Create) }, + ) + InputRowDefault( + text = TextReference.Str("Add Existing Wallet"), + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = 1, + lastIndex = 2, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Add) }, + ) + InputRowDefault( + text = TextReference.Str("Buy Tangem Wallet"), + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = 2, + lastIndex = 2, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Buy) }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SecurityScoreBottomSheetPreview() { + TangemThemePreview { + AddWalletBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = AddWalletBottomSheetContentUM(), + ), + ) + } +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt new file mode 100644 index 0000000000..caf6ecd9bd --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt @@ -0,0 +1,89 @@ +package com.tangem.features.welcome.impl.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.welcome.impl.ui.state.WalletUM +import com.tangem.features.welcome.impl.ui.state.WelcomeUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun Welcome(state: WelcomeUM, modifier: Modifier = Modifier) { + Box( + modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + AnimatedContent( + targetState = state, + contentKey = { it::class.java.simpleName }, + ) { st -> + when (st) { + is WelcomeUM.Plain -> WelcomePlain(modifier = modifier) + is WelcomeUM.SelectWallet -> WelcomeSelectWallet( + state = st, + modifier = modifier, + ) + is WelcomeUM.EnterAccessCode -> WelcomeEnterAccessCode( + state = st, + modifier = modifier, + ) + } + } + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + val state = WelcomeUM.SelectWallet( + wallets = persistentListOf( + WalletUM( + name = TextReference.Str("Wallet 1"), + subtitle = TextReference.Str("3 cards"), + imageState = WalletUM.ImageState.Loading, + onClick = {}, + ), + WalletUM( + name = TextReference.Str("Wallet 1"), + subtitle = TextReference.Str("Mobile wallet"), + imageState = WalletUM.ImageState.MobileWallet, + onClick = {}, + ), + ), + ) + + var currentState by remember { mutableStateOf(WelcomeUM.EnterAccessCode()) } + + Box { + Welcome(currentState) + + Button( + modifier = Modifier.align(Alignment.BottomStart), + onClick = { + currentState = when (currentState) { + is WelcomeUM.Plain -> state + is WelcomeUM.SelectWallet -> WelcomeUM.EnterAccessCode( + value = "", + onValueChange = {}, + ) + is WelcomeUM.EnterAccessCode -> WelcomeUM.Plain + } + }, + ) { + Text("Switch") + } + } + } +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt new file mode 100644 index 0000000000..3cd6085b58 --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt @@ -0,0 +1,94 @@ +package com.tangem.features.welcome.impl.ui + +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.appbar.TopAppBarButton +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.fields.PinTextField +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.welcome.impl.ui.state.WelcomeUM + +@Suppress("MagicNumber") +@Composable +internal fun AnimatedContentScope.WelcomeEnterAccessCode( + state: WelcomeUM.EnterAccessCode, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxSize() + .statusBarsPadding(), + ) { + Column { + TopAppBarButton( + modifier = Modifier + .padding(12.dp), + button = TopAppBarButtonUM.Back(onBackClicked = state.onBackClick), + tint = TangemTheme.colors.icon.primary1, + ) + + SpacerH(68.dp) + + Text( + modifier = Modifier + .animateEnterExit( + enter = slideInVertically( + tween(delayMillis = 300), + initialOffsetY = { it + 200 }, + ) + fadeIn(tween(delayMillis = 300)), + exit = fadeOut(), + ) + .align(Alignment.CenterHorizontally), + text = "Enter Access Code", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + + SpacerH24() + + Box( + modifier = Modifier + .animateEnterExit( + enter = slideInVertically( + tween(delayMillis = 300), + initialOffsetY = { it + 200 }, + ) + fadeIn(tween(delayMillis = 300)), + exit = fadeOut(), + ) + .fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + PinTextField( + length = 6, + isPasswordVisual = true, + value = state.value, + onValueChange = state.onValueChange, + ) + } + } + + SecondaryButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(16.dp) + .navigationBarsPadding() + .imePadding() + .animateEnterExit(fadeIn(), fadeOut()), + text = "Log in with biometric", + onClick = state.onUnlockWithBiometricClick, + ) + } +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt new file mode 100644 index 0000000000..13aaae14db --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt @@ -0,0 +1,29 @@ +package com.tangem.features.welcome.impl.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.welcome.impl.R + +@Composable +internal fun WelcomePlain(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(84.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt new file mode 100644 index 0000000000..ed41fe5ad4 --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -0,0 +1,230 @@ +package com.tangem.features.welcome.impl.ui + +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.userwallet.CardImage +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.welcome.impl.R +import com.tangem.features.welcome.impl.ui.state.WalletUM +import com.tangem.features.welcome.impl.ui.state.WelcomeUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.delay + +@Suppress("MagicNumber") +@Composable +internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWallet, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .statusBarsPadding(), + ) { + TopBar(state) + TitleText() + SpacerH12() + + var actualWallets by remember { mutableStateOf>(persistentListOf()) } + + Box(modifier = Modifier.weight(1f)) { + LazyColumn( + modifier = Modifier + .animateEnterExit( + enter = slideInVertically( + tween(delayMillis = 300), + initialOffsetY = { it / 10 }, + ) + fadeIn(tween(delayMillis = 300)), + exit = fadeOut(), + ) + .fillMaxHeight() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + itemsIndexed(actualWallets) { index, walletState -> + WalletItem( + state = walletState, + modifier = Modifier, + ) + } + } + + BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) + + SecondaryButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(16.dp) + .navigationBarsPadding() + .animateEnterExit(fadeIn(), fadeOut()), + text = "Unlock all with biometric", + onClick = state.onUnlockWithBiometricClick, + ) + } + + LaunchedEffect(state.wallets) { + delay(200) + actualWallets = state.wallets + } + } + + AddWalletBottomSheet(state.addWalletBottomSheet) +} + +@Suppress("MagicNumber") +@Composable +private fun AnimatedContentScope.TopBar(state: WelcomeUM.SelectWallet, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .animateEnterExit( + enter = slideInVertically( + tween(delayMillis = 100), + initialOffsetY = { it + 100 }, + ) + fadeIn(tween(delayMillis = 100)), + exit = fadeOut(), + ) + .padding( + top = 16.dp, + bottom = 16.dp, + start = 16.dp, + ) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.img_tangem_logo_90_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + + TextButton( + modifier = Modifier.clip(TangemTheme.shapes.roundedCornersLarge), + text = "Add Wallet", + colors = TangemButtonsDefaults.defaultTextButtonColors.copy( + contentColor = TangemTheme.colors.text.primary1, + ), + textStyle = TangemTheme.typography.body1, + onClick = state.addWalletClick, + ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { + Column( + modifier.padding(16.dp), + ) { + Text( + modifier = Modifier.animateEnterExit( + enter = slideInVertically( + tween(delayMillis = 300), + initialOffsetY = { it + 200 }, + ) + fadeIn(tween(delayMillis = 300)), + exit = fadeOut(), + ), + text = "Welcome back!", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + + SpacerH16() + + Text( + modifier = Modifier.animateEnterExit( + enter = slideInVertically( + tween(delayMillis = 300), + initialOffsetY = { it + 200 }, + ) + fadeIn(tween(delayMillis = 300)), + exit = fadeOut(), + ), + text = "Select a wallet to log in", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun WalletItem(state: WalletUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.secondary, TangemTheme.shapes.roundedCornersXMedium) + .clickable(onClick = state.onClick) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + WalletImage(state.imageState) + + SpacerW12() + + Column(Modifier.weight(1f)) { + Text( + text = state.name.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +@Composable +private fun WalletImage(state: WalletUM.ImageState, modifier: Modifier = Modifier) { + when (state) { + WalletUM.ImageState.MobileWallet -> { + Box( + modifier = modifier + .size(36.dp) + .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f), CircleShape), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_wallet_filled_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + } + else -> { + CardImage( + imageState = when (state) { + is WalletUM.ImageState.Image -> UserWalletItemUM.ImageState.Image(state.artwork) + WalletUM.ImageState.Loading -> UserWalletItemUM.ImageState.Loading + else -> error("") + }, + modifier = modifier, + ) + } + } +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/AddWalletBottomSheetContentUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/AddWalletBottomSheetContentUM.kt new file mode 100644 index 0000000000..40a14eb1bb --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/AddWalletBottomSheetContentUM.kt @@ -0,0 +1,14 @@ +package com.tangem.features.welcome.impl.ui.state + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +internal data class AddWalletBottomSheetContentUM( + val onOptionClick: (Option) -> Unit = {}, +) : TangemBottomSheetConfigContent { + + enum class Option { + Create, + Add, + Buy, + } +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt new file mode 100644 index 0000000000..216a28f7d2 --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.welcome.impl.ui.state + +import com.tangem.core.ui.components.artwork.ArtworkUM +import com.tangem.core.ui.extensions.TextReference +import javax.annotation.concurrent.Immutable + +internal data class WalletUM( + val name: TextReference, + val subtitle: TextReference, + val imageState: ImageState, + val onClick: () -> Unit, +) { + + @Immutable + sealed class ImageState { + data object MobileWallet : ImageState() + data object Loading : ImageState() + data class Image( + val artwork: ArtworkUM, + ) : ImageState() + } +} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt new file mode 100644 index 0000000000..1e2e509d58 --- /dev/null +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt @@ -0,0 +1,27 @@ +package com.tangem.features.welcome.impl.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +internal sealed class WelcomeUM { + + data object Plain : WelcomeUM() + + data class SelectWallet( + val wallets: ImmutableList = persistentListOf(), + val showUnlockWithBiometricButton: Boolean = false, + val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, + val onUnlockWithBiometricClick: () -> Unit = {}, + val addWalletClick: () -> Unit = {}, + ) : WelcomeUM() + + data class EnterAccessCode( + val value: String = "", + val onUnlockWithBiometricClick: () -> Unit = {}, + val onValueChange: (String) -> Unit = {}, + val onBackClick: () -> Unit = {}, + ) : WelcomeUM() +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index ba875e3bb9..faba1334a8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -254,6 +254,9 @@ include(":features:kyc:impl") include(":features:create-wallet-selection:api") include(":features:create-wallet-selection:impl") + +include(":features:welcome:api") +include(":features:welcome:impl") // endregion Feature modules // region Domain modules @@ -341,6 +344,6 @@ include(":data:onramp") include(":data:quotes") include(":data:notifications") include(":data:blockaid") -// endregion Data modules include(":data:swap") -include(":data:express") \ No newline at end of file +include(":data:express") +// endregion Data modules \ No newline at end of file From 058a6ab4d51ab04039dedfed1f7c78d430c2f173 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 23 Jun 2025 16:37:57 +0400 Subject: [PATCH 58/99] Updated on 2026-08-14 --- common/test/build.gradle.kts | 1 + .../com/tangem/common/test/utils/TruthExt.kt | 14 + .../api/tangemTech/TangemTechApi.kt | 11 +- .../tangem/data/common/di/DataCommonModule.kt | 8 + .../data/common/quote/DefaultQuotesFetcher.kt | 240 ++++++++++ .../tangem/data/common/quote/QuotesFetcher.kt | 61 +++ .../data/common/quote/utils/FieldExt.kt | 5 + .../common/quote/DefaultQuotesFetcherTest.kt | 444 ++++++++++++++++++ .../markets/DefaultMarketsTokenRepository.kt | 36 +- .../data/markets/MarketsBatchUpdateFetcher.kt | 31 +- .../data/markets/di/MarketsDataModule.kt | 6 +- .../multi/DefaultMultiQuoteStatusFetcher.kt | 25 +- .../DefaultMultiQuoteStatusFetcherTest.kt | 143 +++--- .../DefaultSingleQuoteStatusFetcherTest.kt | 183 ++------ .../tangem/data/visa/DefaultVisaRepository.kt | 17 +- 15 files changed, 969 insertions(+), 256 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/quote/QuotesFetcher.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/quote/utils/FieldExt.kt create mode 100644 data/common/src/test/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcherTest.kt diff --git a/common/test/build.gradle.kts b/common/test/build.gradle.kts index 52931ce5f3..d0c4c8fd00 100644 --- a/common/test/build.gradle.kts +++ b/common/test/build.gradle.kts @@ -33,4 +33,5 @@ dependencies { implementation(tangemDeps.card.core) implementation(deps.test.junit5) + implementation(deps.test.truth) } \ No newline at end of file diff --git a/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt b/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt new file mode 100644 index 0000000000..d28fe3df7e --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt @@ -0,0 +1,14 @@ +package com.tangem.common.test.utils + +import arrow.core.Either +import com.google.common.truth.Truth + +fun assertEither(actual: Either, expected: Either) { + actual + .onRight { Truth.assertThat(it).isEqualTo(expected) } + .onLeft { + val expectedError = expected.leftOrNull() ?: error("Expected must be Either.Left") + + Truth.assertThat(it::class.java).isEqualTo(expectedError::class.java) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 0ac0fdc78a..a6793286f0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -65,7 +65,7 @@ interface TangemTechApi { suspend fun getQuotes( @Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String, - @Query("fields") fields: String = "price,priceChange24h,lastUpdatedAt", + @Query("fields") fields: String, ): ApiResponse @GET("promotion") @@ -176,13 +176,4 @@ interface TangemTechApi { @GET("user-wallets/wallets/by-app/{app_id}") suspend fun getWallets(@Path("app_id") appId: String): ApiResponse> // endregion - - companion object { - val marketsQuoteFields = listOf( - "price", - "priceChange24h", - "priceChange1w", - "priceChange30d", - ) - } } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index f443f4bd8a..6ceeb800e9 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -5,6 +5,8 @@ import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.DefaultCardCryptoCurrencyFactory import com.tangem.data.common.currency.UserTokensResponseAddressesEnricher import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.common.quote.DefaultQuotesFetcher +import com.tangem.data.common.quote.QuotesFetcher import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore @@ -70,4 +72,10 @@ internal object DataCommonModule { userTokensResponseAddressesEnricher = enricher, ) } + + @Provides + @Singleton + fun provideQuotesFetcher(tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider): QuotesFetcher { + return DefaultQuotesFetcher(tangemTechApi = tangemTechApi, dispatchers = dispatchers) + } } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt b/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt new file mode 100644 index 0000000000..ca867be571 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt @@ -0,0 +1,240 @@ +package com.tangem.data.common.quote + +import androidx.annotation.VisibleForTesting +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.ensure +import arrow.core.raise.ensureNotNull +import com.tangem.data.common.api.safeApiCallWithTimeout +import com.tangem.data.common.quote.QuotesFetcher.Error +import com.tangem.data.common.quote.QuotesFetcher.Field +import com.tangem.data.common.quote.utils.combine +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.core.utils.eitherOn +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.joda.time.DateTime +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration.Companion.seconds + +/** + * Default implementation of [QuotesFetcher] + * + * @property tangemTechApi Tangem tech API + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultQuotesFetcher( + private val tangemTechApi: TangemTechApi, + private val dispatchers: CoroutineDispatcherProvider, +) : QuotesFetcher { + + /** Mutex for synchronization of the [getFetchMutex] method */ + private val getFetchMethodMutex = Mutex() + + /** + * Map for tracking active [fetch] methods. + * The key is the parameters of the [fetch] method, and the value is [Mutex]. + */ + private val fetchMutexMap = ConcurrentHashMap() + + /** + * Cache that stores a list of quotes for a certain fiat currency. + * Quotes are considered expired if they are in the cache for more than [tenSecInMillis] seconds. + */ + private val quotesCache = ConcurrentHashMap>() + + override suspend fun fetch( + fiatCurrencyId: String, + currenciesIds: Set, + fields: Set, + ): Either = eitherOn(dispatchers.default) { + val validatedParams = validateParams(fiatCurrencyId, currenciesIds, fields) + + if (validatedParams.currenciesIds.isEmpty()) return@eitherOn emptyQuotesResponse + + val mutex = getFetchMutex(params = validatedParams) + + return@eitherOn fetch(params = validatedParams, mutex = mutex) + } + .onLeft { + val params = RequestParams(fiatCurrencyId, currenciesIds, fields) + fetchMutexMap.remove(params) + } + + private fun Raise.validateParams( + fiatCurrencyId: String, + currenciesIds: Set, + fields: Set, + ): RequestParams { + ensure(fiatCurrencyId.isNotBlank() && fields.isNotEmpty()) { + raise(Error.InvalidArgumentsError) + } + + val filterCurrenciesIds = currenciesIds.filter(String::isNotEmpty).toSet() + + return RequestParams(fiatCurrencyId = fiatCurrencyId, currenciesIds = filterCurrenciesIds, fields = fields) + } + + /** + * The method for determining the [Mutex], which will be used by the [fetch] method. + * + * @param params request params + * + * @return if at the moment the method of [fetch] is already executed for an adjacent set of parameters + + */ + private suspend fun Raise.getFetchMutex(params: RequestParams): Mutex { + return getFetchMethodMutex.withLock { + val similarJobsMutexes = fetchMutexMap.filterKeys { metadata -> + metadata.fiatCurrencyId == params.fiatCurrencyId && + params.currenciesIds.any { it in metadata.currenciesIds } + } + + val storedMutex = similarJobsMutexes.firstNotNullOfOrNull { it.value } + + when { + storedMutex == null || !storedMutex.isLocked -> Mutex() + storedMutex.isLocked -> storedMutex + else -> raise(Error.CacheOperationError) + } + .also { fetchMutexMap[params] = it } + } + } + + /** + * Fetch quotes by [params]. + * + * It works in the scope of the transferred [mutex]. + * If at the moment [Mutex] is busy, then it is working on fetching related data, then the current request will + * wait for its execution. + * + * @return [QuotesResponse] + */ + private suspend fun Raise.fetch(params: RequestParams, mutex: Mutex): QuotesResponse { + val (fiatCurrencyId, currenciesIds) = params + + return mutex.withLock { + val quotes = quotesCache[fiatCurrencyId].orEmpty() + + val expiredOrAbsentIds = currenciesIds.filterExpiredOrAbsent(quotes) + + // We will fetch quotes only for those quotes that are absent in cache or expired + if (expiredOrAbsentIds.isNotEmpty()) { + val response = requestQuotes( + fiatCurrencyId = fiatCurrencyId, + currenciesIds = expiredOrAbsentIds.toSet(), + fields = params.fields, + ) + + saveQuotes(fiatCurrencyId = fiatCurrencyId, response = response) + } + + fetchMutexMap.remove(params) + + getCachedResult(fiatCurrencyId = fiatCurrencyId, currenciesIds = currenciesIds) + } + } + + private fun Set.filterExpiredOrAbsent(quotes: Set?): List { + return filter { id -> + val quote = quotes?.firstOrNull { it.cryptoCurrencyId == id && !it.isExpired } + + quote == null + } + } + + private fun Raise.getCachedResult(fiatCurrencyId: String, currenciesIds: Set): QuotesResponse { + val storedQuotes = quotesCache[fiatCurrencyId] + + ensureNotNull(storedQuotes) { + raise(Error.CacheOperationError) + } + + val quotes = catch( + block = { + currenciesIds.associateWith { currencyId -> + storedQuotes.first { it.cryptoCurrencyId == currencyId }.value + } + }, + catch = { raise(Error.CacheOperationError) }, + ) + + return QuotesResponse(quotes = quotes) + } + + private suspend fun Raise.requestQuotes( + fiatCurrencyId: String, + currenciesIds: Set, + fields: Set, + ) = withContext(dispatchers.io) { + safeApiCallWithTimeout( + call = { + tangemTechApi.getQuotes( + currencyId = fiatCurrencyId, + coinIds = currenciesIds.joinToString(separator = ","), + fields = fields.combine(), + ) + .bind() + }, + onError = { raise(Error.ApiOperationError(it)) }, + ) + } + + private fun saveQuotes(fiatCurrencyId: String, response: QuotesResponse) { + val newQuotes = response.quotes.mapTo(destination = hashSetOf()) { (currencyId, quote) -> + QuoteMetadata( + cryptoCurrencyId = currencyId, + timestamp = DateTime.now().millis, + value = quote, + ) + } + + val storedQuotes = quotesCache[fiatCurrencyId].orEmpty() + + val storedUniqueQuotes = storedQuotes.filterNot { stored -> + newQuotes.any { stored.cryptoCurrencyId == it.cryptoCurrencyId } + } + + quotesCache[fiatCurrencyId] = (storedUniqueQuotes + newQuotes).toSet() + } + + data class RequestParams( + val fiatCurrencyId: String, + val currenciesIds: Set, + val fields: Set, + ) + + data class QuoteMetadata( + val cryptoCurrencyId: String, + val timestamp: Long, + val value: QuotesResponse.Quote, + ) { + + val isExpired: Boolean + get() = DateTime.now().millis - timestamp > tenSecInMillis + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun getCachedQuotes() = quotesCache + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun setCachedQuotes(fiatCurrencyId: String, quotes: Set) { + quotesCache[fiatCurrencyId] = quotes + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun clearCache() { + quotesCache.clear() + } + + private companion object { + val emptyQuotesResponse = QuotesResponse(quotes = emptyMap()) + val tenSecInMillis = 10.seconds.inWholeMilliseconds + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/quote/QuotesFetcher.kt b/data/common/src/main/kotlin/com/tangem/data/common/quote/QuotesFetcher.kt new file mode 100644 index 0000000000..4f1e8b92ce --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/quote/QuotesFetcher.kt @@ -0,0 +1,61 @@ +package com.tangem.data.common.quote + +import arrow.core.Either +import com.tangem.data.common.quote.utils.combine +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.tangemTech.models.QuotesResponse + +/** + * Fetcher of quotes + * +[REDACTED_AUTHOR] + * + * @see Documentation + */ +interface QuotesFetcher { + + /** + * Fetch + * + * @param fiatCurrencyId fiat currency id + * @param currenciesIds crypto currencies ids + * @param fields fields of [QuotesResponse.Quote] + */ + suspend fun fetch( + fiatCurrencyId: String, + currenciesIds: Set, + fields: Set, + ): Either + + /** + * Fetch + * + * @param fiatCurrencyId fiat currency id + * @param currencyId crypto currencies ids + * @param field fields of [QuotesResponse.Quote] + */ + suspend fun fetch(fiatCurrencyId: String, currencyId: String, field: Field): Either { + return fetch(fiatCurrencyId = fiatCurrencyId, currenciesIds = setOf(currencyId), fields = setOf(field)) + } + + enum class Field(internal val value: String) { + PRICE(value = "price"), + PRICE_CHANGE_24H(value = "priceChange24h"), + PRICE_CHANGE_1W(value = "priceChange1w"), + PRICE_CHANGE_30D(value = "priceChange30d"), + ALL_PRICES( + value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D).combine(), + ), + LAST_UPDATED_AT(value = "lastUpdatedAt"), + ; + } + + sealed interface Error { + + data object InvalidArgumentsError : Error + + data object CacheOperationError : Error + + data class ApiOperationError(val apiError: ApiResponseError) : Error + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/quote/utils/FieldExt.kt b/data/common/src/main/kotlin/com/tangem/data/common/quote/utils/FieldExt.kt new file mode 100644 index 0000000000..e7ea17d255 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/quote/utils/FieldExt.kt @@ -0,0 +1,5 @@ +package com.tangem.data.common.quote.utils + +import com.tangem.data.common.quote.QuotesFetcher + +fun Set.combine(): String = joinToString(separator = ",", transform = QuotesFetcher.Field::value) \ No newline at end of file diff --git a/data/common/src/test/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcherTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcherTest.kt new file mode 100644 index 0000000000..add4f12f94 --- /dev/null +++ b/data/common/src/test/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcherTest.kt @@ -0,0 +1,444 @@ +package com.tangem.data.common.quote + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.data.common.quote.DefaultQuotesFetcher.QuoteMetadata +import com.tangem.data.common.quote.QuotesFetcher.Field +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.test.runTest +import org.joda.time.DateTime +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.math.BigDecimal +import java.util.concurrent.ConcurrentHashMap + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultQuotesFetcherTest { + + private val tangemTechApi = mockk() + private val fetcher = DefaultQuotesFetcher( + tangemTechApi = tangemTechApi, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun resetMocks() { + clearMocks(tangemTechApi) + fetcher.clearCache() + } + + @Test + fun `fetch if fiatCurrencyId is EMPTY`() = runTest { + // Act + val actual = fetcher.fetch( + fiatCurrencyId = "", + currenciesIds = setOf("ethereum"), + fields = setOf(Field.PRICE), + ) + + val actualCacheData = fetcher.getCachedQuotes() + + // Assert + val expected = QuotesFetcher.Error.InvalidArgumentsError.left() + Truth.assertThat(actual).isEqualTo(expected) + + val expectedCacheData = ConcurrentHashMap>() + Truth.assertThat(actualCacheData).isEqualTo(expectedCacheData) + + coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any(), fields = any()) } + } + + @Test + fun `fetch if currenciesIds is EMPTY`() = runTest { + // Act + val actual = fetcher.fetch( + fiatCurrencyId = "usd", + currenciesIds = emptySet(), + fields = setOf(Field.PRICE), + ) + + val actualCacheData = fetcher.getCachedQuotes() + + // Assert + val expected = QuotesResponse(quotes = emptyMap()).right() + Truth.assertThat(actual).isEqualTo(expected) + + val expectedCacheData = ConcurrentHashMap>() + Truth.assertThat(actualCacheData).isEqualTo(expectedCacheData) + + coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any(), fields = any()) } + } + + @Test + fun `fetch if fields is EMPTY`() = runTest { + // Act + val actual = fetcher.fetch( + fiatCurrencyId = "usd", + currenciesIds = setOf("ethereum"), + fields = emptySet(), + ) + + val actualCacheData = fetcher.getCachedQuotes() + + // Assert + val expected = QuotesFetcher.Error.InvalidArgumentsError.left() + Truth.assertThat(actual).isEqualTo(expected) + + val expectedCacheData = ConcurrentHashMap>() + Truth.assertThat(actualCacheData).isEqualTo(expectedCacheData) + + coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any(), fields = any()) } + } + + @Test + fun `fetch if all currencies ids ARE CACHED and ARE NOT EXPIRED`() = runTest { + // Arrange + val quote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) + + fetcher.setCachedQuotes( + fiatCurrencyId = "usd", + quotes = setOf( + QuoteMetadata( + cryptoCurrencyId = "ethereum", + timestamp = DateTime.now().millis, + value = quote, + ), + ), + ) + + // Act + val actual = fetcher.fetch( + fiatCurrencyId = "usd", + currenciesIds = setOf("ethereum"), + fields = setOf(Field.PRICE), + ) + + val actualCacheData = fetcher.getCachedQuotes() + + // Assert + val expected = QuotesResponse(quotes = mapOf("ethereum" to quote)).right() + Truth.assertThat(actual).isEqualTo(expected) + + Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(1) + Truth.assertThat(actualCacheData["usd"].toResponseQuotes()).isEqualTo(mapOf("ethereum" to quote)) + + coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any(), fields = any()) } + } + + @Test + fun `fetch if all currencies ids ARE CACHED and EXPIRED`() = runTest { + // Arrange + val cachedQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) + val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) + val apiResponse = ApiResponse.Success( + data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)), + ) + + fetcher.setCachedQuotes( + fiatCurrencyId = "usd", + quotes = setOf( + QuoteMetadata( + cryptoCurrencyId = "ethereum", + timestamp = DateTime.now().millis - 10_000, + value = cachedQuote, + ), + ), + ) + + coEvery { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") + } returns apiResponse + + // Act + val actual = fetcher.fetch( + fiatCurrencyId = "usd", + currenciesIds = setOf("ethereum"), + fields = setOf(Field.PRICE), + ) + + val actualCacheData = fetcher.getCachedQuotes() + + // Assert + val expected = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)).right() + Truth.assertThat(actual).isEqualTo(expected) + + Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(1) + Truth.assertThat(actualCacheData["usd"].toResponseQuotes()).isEqualTo(mapOf("ethereum" to apiQuote)) + + coVerify(exactly = 1) { tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") } + } + + @Test + fun `fetch if cache contain EXPIRED and NOT EXPIRED quotes`() = runTest { + // Arrange + val cachedQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) + val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) + val apiResponse = ApiResponse.Success( + data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)), + ) + + fetcher.setCachedQuotes( + fiatCurrencyId = "usd", + quotes = setOf( + QuoteMetadata( + cryptoCurrencyId = "ethereum", + timestamp = DateTime.now().millis - 10_000, + value = cachedQuote, + ), + QuoteMetadata( + cryptoCurrencyId = "bitcoin", + timestamp = DateTime.now().millis, + value = cachedQuote, + ), + ), + ) + + coEvery { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") + } returns apiResponse + + // Act + val actual = fetcher.fetch( + fiatCurrencyId = "usd", + currenciesIds = setOf("ethereum", "bitcoin"), + fields = setOf(Field.PRICE), + ) + + val actualCacheData = fetcher.getCachedQuotes() + + // Assert + val expected = QuotesResponse( + quotes = mapOf("ethereum" to apiQuote, "bitcoin" to cachedQuote), + ).right() + + Truth.assertThat(actual).isEqualTo(expected) + + Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(1) + Truth.assertThat(actualCacheData["usd"].toResponseQuotes()) + .isEqualTo(mapOf("ethereum" to apiQuote, "bitcoin" to cachedQuote)) + + coVerify(exactly = 1) { tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") } + } + + @Test + fun `fetch if cache DOES NOT CONTAIN fiat currency`() = runTest { + // Arrange + val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) + val apiResponse = ApiResponse.Success( + data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)), + ) + + fetcher.setCachedQuotes(fiatCurrencyId = "eu", quotes = emptySet()) + + coEvery { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") + } returns apiResponse + + // Act + val actual = fetcher.fetch( + fiatCurrencyId = "usd", + currenciesIds = setOf("ethereum"), + fields = setOf(Field.PRICE), + ) + + val actualCacheData = fetcher.getCachedQuotes() + + // Assert + val expected = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)).right() + Truth.assertThat(actual).isEqualTo(expected) + + Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(2) + Truth.assertThat(actualCacheData["eu"]).isEmpty() + Truth.assertThat(actualCacheData["usd"].toResponseQuotes()).isEqualTo(mapOf("ethereum" to apiQuote)) + + coVerify(exactly = 1) { tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") } + } + + @Test + fun `fetch if cached quotes are ABSENT`() = runTest { + // Arrange + val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) + val apiResponse = ApiResponse.Success( + data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)), + ) + + fetcher.setCachedQuotes(fiatCurrencyId = "usd", quotes = emptySet()) + + coEvery { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") + } returns apiResponse + + // Act + val actual = fetcher.fetch( + fiatCurrencyId = "usd", + currenciesIds = setOf("ethereum"), + fields = setOf(Field.PRICE), + ) + + val actualCacheData = fetcher.getCachedQuotes() + + // Assert + val expected = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)).right() + Truth.assertThat(actual).isEqualTo(expected) + + Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(1) + Truth.assertThat(actualCacheData["usd"].toResponseQuotes()) + .isEqualTo(mapOf("ethereum" to apiQuote)) + + coVerify(exactly = 1) { tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") } + } + + @Test + fun `two parallel fetch if cache is empty`() = runTest { + // Arrange + val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) + + val usdApiResponse = ApiResponse.Success( + data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)), + ) + + val euApiResponse = ApiResponse.Success( + data = QuotesResponse(quotes = mapOf("bitcoin" to apiQuote)), + ) + + coEvery { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") + } returns usdApiResponse + + coEvery { + tangemTechApi.getQuotes(currencyId = "eu", coinIds = "bitcoin", fields = "price") + } returns euApiResponse + + // Act + val actual = listOf( + async { + fetcher.fetch( + fiatCurrencyId = "usd", + currenciesIds = setOf("ethereum"), + fields = setOf(Field.PRICE), + ) + }, + async { + fetcher.fetch( + fiatCurrencyId = "eu", + currenciesIds = setOf("bitcoin"), + fields = setOf(Field.PRICE), + ) + }, + ) + .awaitAll() + + val actual1 = actual[0] + val actual2 = actual[1] + + val actualCacheData = fetcher.getCachedQuotes() + + // Assert + val expected1 = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)).right() + val expected2 = QuotesResponse(quotes = mapOf("bitcoin" to apiQuote)).right() + Truth.assertThat(actual1).isEqualTo(expected1) + Truth.assertThat(actual2).isEqualTo(expected2) + + Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(2) + Truth.assertThat(actualCacheData["usd"].toResponseQuotes()) + .isEqualTo(mapOf("ethereum" to apiQuote)) + Truth.assertThat(actualCacheData["eu"].toResponseQuotes()) + .isEqualTo(mapOf("bitcoin" to apiQuote)) + + coVerify(exactly = 1) { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum", fields = "price") + tangemTechApi.getQuotes(currencyId = "eu", coinIds = "bitcoin", fields = "price") + } + } + + @Test + fun `two parallel fetch`() = runTest { + // Arrange + val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) + + val firstApiResponse = ApiResponse.Success( + data = QuotesResponse( + quotes = mapOf("ethereum" to apiQuote, "solana" to apiQuote), + ), + ) + + val secondApiResponse = ApiResponse.Success( + data = QuotesResponse( + quotes = mapOf("bitcoin" to apiQuote, "solana" to apiQuote), + ), + ) + + fetcher.setCachedQuotes( + fiatCurrencyId = "usd", + quotes = setOf( + QuoteMetadata( + cryptoCurrencyId = "solana", + timestamp = DateTime.now().millis - 10_000, + value = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO), + ), + ), + ) + + coEvery { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum,solana", fields = "price") + } returns firstApiResponse + + coEvery { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = "bitcoin", fields = "price") + } returns secondApiResponse + + // Act + val actual = listOf( + async { + fetcher.fetch( + fiatCurrencyId = "usd", + currenciesIds = setOf("ethereum", "solana"), + fields = setOf(Field.PRICE), + ) + }, + async { + fetcher.fetch( + fiatCurrencyId = "usd", + currenciesIds = setOf("bitcoin", "solana"), + fields = setOf(Field.PRICE), + ) + }, + ) + .awaitAll() + + val actual1 = actual[0] + val actual2 = actual[1] + + val actualCacheData = fetcher.getCachedQuotes() + + // Assert + val expected1 = QuotesResponse(quotes = mapOf("ethereum" to apiQuote, "solana" to apiQuote)).right() + val expected2 = QuotesResponse(quotes = mapOf("bitcoin" to apiQuote, "solana" to apiQuote)).right() + Truth.assertThat(actual1).isEqualTo(expected1) + Truth.assertThat(actual2).isEqualTo(expected2) + + Truth.assertThat(actualCacheData.keys().toList().size).isEqualTo(1) + Truth.assertThat(actualCacheData["usd"].toResponseQuotes()).isEqualTo( + mapOf("ethereum" to apiQuote, "bitcoin" to apiQuote, "solana" to apiQuote), + ) + + coVerifyOrder { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum,solana", fields = "price") + tangemTechApi.getQuotes(currencyId = "usd", coinIds = "bitcoin", fields = "price") + } + } + + private fun Iterable?.toResponseQuotes() = this!!.associate { it.cryptoCurrencyId to it.value } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index cba7c2bf69..f6d9ebbfbd 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.markets +import arrow.core.getOrElse import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.applyL2Compatibility import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network @@ -9,6 +10,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.common.utils.retryOnError import com.tangem.data.markets.analytics.MarketsDataAnalyticsEvent import com.tangem.data.markets.converters.* @@ -16,8 +18,6 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.TangemTechApi.Companion.marketsQuoteFields import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.markets.* @@ -37,7 +37,7 @@ import java.util.concurrent.atomic.AtomicLong @Suppress("LongParameterList") internal class DefaultMarketsTokenRepository( private val marketsApi: TangemTechMarketsApi, - private val tangemTechApi: TangemTechApi, + private val quotesFetcher: QuotesFetcher, private val userWalletsStore: UserWalletsStore, private val dispatcherProvider: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, @@ -112,7 +112,7 @@ internal class DefaultMarketsTokenRepository( nextBatchSize: Int, ): BatchFlow, TokenMarketUpdateRequest> { val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher( - tangemTechApi = tangemTechApi, + quotesFetcher = quotesFetcher, marketsApi = marketsApi, analyticsEventHandler = analyticsEventHandler, onApiResponseError = { @@ -225,16 +225,24 @@ internal class DefaultMarketsTokenRepository( withContext(dispatcherProvider.io) { // for second markets iteration we should use extended api method with all required fields - val result = catchDetailsErrorAndSendEvent( - request = MarketsDataAnalyticsEvent.Details.Error.Request.Info, - tokenSymbol = tokenSymbol, - ) { - tangemTechApi.getQuotes( - currencyId = fiatCurrencyCode, - coinIds = tokenId.value, - fields = marketsQuoteFields.joinToString(separator = ","), - ).getOrThrow() - } + val result = quotesFetcher.fetch( + fiatCurrencyId = fiatCurrencyCode, + currencyId = tokenId.value, + field = QuotesFetcher.Field.ALL_PRICES, + ) + .getOrElse { + val error = it as QuotesFetcher.Error.ApiOperationError + + val errorEvent = createDetailsErrorEvent( + error = error.apiError, + request = MarketsDataAnalyticsEvent.Details.Error.Request.Info, + tokenSymbol = tokenSymbol, + ) + + analyticsEventHandler.send(errorEvent.toEvent()) + + throw error.apiError + } return@withContext TokenQuotesShortConverter.convert(tokenId, result).toFull() } diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt index 3b5e662379..82ab261728 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -1,6 +1,8 @@ package com.tangem.data.markets +import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.common.utils.retryOnError import com.tangem.data.markets.analytics.MarketsDataAnalyticsEvent import com.tangem.data.markets.converters.TokenMarketChartsConverter @@ -11,8 +13,6 @@ import com.tangem.datasource.api.common.response.catchApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.TangemTechApi.Companion.marketsQuoteFields import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarketUpdateRequest import com.tangem.pagination.Batch @@ -24,7 +24,7 @@ import kotlinx.coroutines.launch internal class MarketsBatchUpdateFetcher( private val marketsApi: TangemTechMarketsApi, - private val tangemTechApi: TangemTechApi, + private val quotesFetcher: QuotesFetcher, private val analyticsEventHandler: AnalyticsEventHandler, private val onApiResponseError: (ApiResponseError) -> Unit, ) : BatchUpdateFetcher, TokenMarketUpdateRequest> { @@ -72,13 +72,24 @@ internal class MarketsBatchUpdateFetcher( } is TokenMarketUpdateRequest.UpdateQuotes -> { val quotesRes = retryOnError { - catchApiResponseError(onApiResponseError) { - tangemTechApi.getQuotes( - currencyId = updateRequest.currencyId, - coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","), - fields = marketsQuoteFields.joinToString(separator = ","), - ).getOrThrow() - } + val currenciesIds = idsToUpdate.flatMapTo(hashSetOf()) { it.second.map { rawID -> rawID.value } } + + quotesFetcher.fetch( + fiatCurrencyId = updateRequest.currencyId, + currenciesIds = currenciesIds, + fields = setOf(QuotesFetcher.Field.ALL_PRICES), + ) + .getOrElse { + val exception = if (it is QuotesFetcher.Error.ApiOperationError) { + onApiResponseError(it.apiError) + + it.apiError + } else { + error("Cause: $it") + } + + throw exception + } } update { diff --git a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt index 1c1830b9cc..da2ddeb1b7 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt @@ -4,9 +4,9 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.markets.DefaultMarketsTokenRepository import com.tangem.datasource.api.markets.TangemTechMarketsApi -import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.markets.repositories.MarketsTokenRepository @@ -25,7 +25,7 @@ internal object MarketsDataModule { @Singleton fun provideMarketsTokenRepository( marketsApi: TangemTechMarketsApi, - tangemTechApi: TangemTechApi, + quotesFetcher: QuotesFetcher, userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, analyticsEventHandler: AnalyticsEventHandler, @@ -35,7 +35,7 @@ internal object MarketsDataModule { ): MarketsTokenRepository { return DefaultMarketsTokenRepository( marketsApi = marketsApi, - tangemTechApi = tangemTechApi, + quotesFetcher = quotesFetcher, dispatcherProvider = dispatchers, userWalletsStore = userWalletsStore, analyticsEventHandler = analyticsEventHandler, diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt index c605e43574..618f75178d 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt @@ -1,18 +1,18 @@ package com.tangem.data.quotes.multi import arrow.core.Either -import com.tangem.data.common.api.safeApiCallWithTimeout +import arrow.core.getOrElse +import com.tangem.data.common.quote.QuotesFetcher +import com.tangem.data.common.quote.QuotesFetcher.Field import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.data.quotes.store.setSourceAsCache import com.tangem.data.quotes.store.setSourceAsOnlyCache import com.tangem.data.quotes.utils.QuotesUnsupportedCurrenciesIdAdapter -import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -20,16 +20,16 @@ import javax.inject.Singleton /** * Default implementation of [MultiQuoteStatusFetcher] * - * @property tangemTechApi tangemTech api + * @property quotesFetcher quotes fetcher * @property appCurrencyResponseStore app currency response store - * @property quotesStatusesStore quotes store + * @property quotesStatusesStore quotes statuses store * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ @Singleton internal class DefaultMultiQuoteStatusFetcher @Inject constructor( - private val tangemTechApi: TangemTechApi, + private val quotesFetcher: QuotesFetcher, private val appCurrencyResponseStore: AppCurrencyResponseStore, private val quotesStatusesStore: QuotesStatusesStore, private val dispatchers: CoroutineDispatcherProvider, @@ -51,16 +51,13 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor( ) val appCurrencyId = getAppCurrencyId(params = params) - val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") - val response = safeApiCallWithTimeout( - call = { - withContext(dispatchers.io) { - tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds).bind() - } - }, - onError = { error -> throw error }, + val response = quotesFetcher.fetch( + fiatCurrencyId = appCurrencyId, + currenciesIds = replacementIdsResult.idsForRequest, + fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H), ) + .getOrElse { error("Cause: $it") } val updatedResponse = QuotesUnsupportedCurrenciesIdAdapter.getResponseWithUnsupportedCurrencies( response = response, diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt index 4db5f02c4f..d522ca5ba9 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt @@ -1,107 +1,125 @@ package com.tangem.data.quotes.multi +import arrow.core.left +import arrow.core.right import com.google.common.truth.Truth import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.common.test.utils.assertEither +import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.data.quotes.store.setSourceAsCache import com.tangem.data.quotes.store.setSourceAsOnlyCache -import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError -import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.coVerifyOrder -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.test.runTest -import org.junit.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance import java.math.BigDecimal /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultMultiQuoteStatusFetcherTest { - private val tangemTechApi = mockk(relaxed = true) - private val appCurrencyResponseStore = mockk(relaxed = true) + private val quotesFetcher = mockk() + private val appCurrencyResponseStore = mockk() private val quotesStore = mockk(relaxed = true) private val fetcher = DefaultMultiQuoteStatusFetcher( - tangemTechApi = tangemTechApi, + quotesFetcher = quotesFetcher, appCurrencyResponseStore = appCurrencyResponseStore, quotesStatusesStore = quotesStore, dispatchers = TestingCoroutineDispatcherProvider(), ) + @BeforeEach + fun resetMocks() { + clearMocks(quotesFetcher, appCurrencyResponseStore, quotesStore) + } + @Test - fun `fetch quotes successfully`() = runTest { + fun `fetch successfully`() = runTest { + // Arrange val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency - val coinIds = "BTC,ETH" - coEvery { - tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - } returns ApiResponse.Success(successResponse) + val currenciesIds = setOf("BTC", "ETH") + coEvery { + quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields) + } returns successResponse.right() + + // Act val actual = fetcher(params) + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + coVerifyOrder { quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) - appCurrencyResponseStore.getSyncOrNull() - - tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - + quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields) quotesStore.store(values = successResponse.quotes) } coVerify(inverse = true) { quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } - - Truth.assertThat(actual.isRight()).isTrue() } @Test - fun `fetch quotes successfully if currenciesIds from params is empty`() = runTest { + fun `fetch successfully if currenciesIds from params is empty`() = runTest { + // Arrange val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = null) + // Act val actual = fetcher(params) + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + coVerify(inverse = true) { quotesStore.setSourceAsCache(currenciesIds = any()) appCurrencyResponseStore.getSyncOrNull() - tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) + quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any()) quotesStore.store(values = any()) quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } - - Truth.assertThat(actual.isRight()).isTrue() } @Test - fun `fetch quotes successfully if appCurrencyId from params is not null`() = runTest { + fun `fetch successfully if appCurrencyId from params is not null`() = runTest { + // Arrange val appCurrencyId = "usd" val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) - val coinIds = "BTC,ETH" - coEvery { - tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds) - } returns ApiResponse.Success(successResponse) + val currenciesIds = setOf("BTC", "ETH") + coEvery { + quotesFetcher.fetch(fiatCurrencyId = appCurrencyId, currenciesIds = currenciesIds, fields = fields) + } returns successResponse.right() + + // Act val actual = fetcher(params) + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + coVerifyOrder { quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) - - tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - + quotesFetcher.fetch(fiatCurrencyId = appCurrencyId, currenciesIds = currenciesIds, fields = fields) quotesStore.store(values = successResponse.quotes) } @@ -109,17 +127,22 @@ internal class DefaultMultiQuoteStatusFetcherTest { appCurrencyResponseStore.getSyncOrNull() quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } - - Truth.assertThat(actual.isRight()).isTrue() } @Test - fun `fetch quotes failure because appCurrencyId from params is blank`() = runTest { + fun `fetch failure because appCurrencyId from params is blank`() = runTest { + // Arrange val appCurrencyId = "" val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) + // Act val actual = fetcher(params) + // Assert + val expected = IllegalStateException("Unable to get AppCurrency for updating quotes").left() + + assertEither(actual, expected) + coVerifyOrder { quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) @@ -127,55 +150,59 @@ internal class DefaultMultiQuoteStatusFetcherTest { coVerify(inverse = true) { appCurrencyResponseStore.getSyncOrNull() - tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) + quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any()) quotesStore.store(values = any()) } - - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(IllegalStateException::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat() - .isEqualTo("Unable to get AppCurrency for updating quotes") } @Test - fun `fetch quotes failure because api request failed`() = runTest { + fun `fetch failure because api request failed`() = runTest { + // Arrange val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) + val currenciesIds = setOf("BTC", "ETH") + val error = QuotesFetcher.Error.ApiOperationError(ApiResponseError.NetworkException) + coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency - val coinIds = "BTC,ETH" - - @Suppress("UNCHECKED_CAST") - val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse - coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse + coEvery { + quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields) + } returns error.left() + // Act val actual = fetcher(params) + // Assert + val expected = IllegalStateException("Cause: ApiOperationError(apiError=NetworkException)").left() + assertEither(actual, expected) + coVerifyOrder { quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) - appCurrencyResponseStore.getSyncOrNull() - - tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - + quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields) quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } coVerify(inverse = true) { quotesStore.store(values = any()) } - - Truth.assertThat(actual.isLeft()).isTrue() } @Test - fun `fetch quotes failure because app currency not found`() = runTest { + fun `fetch failure because app currency not found`() = runTest { + // Arrange val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null + // Act val actual = fetcher(params) + // Assert + val expected = IllegalStateException("Unable to get AppCurrency for updating quotes").left() + + assertEither(actual, expected) + coVerifyOrder { quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) appCurrencyResponseStore.getSyncOrNull() @@ -183,11 +210,9 @@ internal class DefaultMultiQuoteStatusFetcherTest { } coVerify(inverse = true) { - tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) + quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any()) quotesStore.store(values = any()) } - - Truth.assertThat(actual.isLeft()).isTrue() } private companion object { @@ -212,5 +237,7 @@ internal class DefaultMultiQuoteStatusFetcherTest { "ETH" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.TEN), ), ) + + val fields = setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H) } } \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt index 245d0364b9..c13faf11c6 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt @@ -1,176 +1,79 @@ package com.tangem.data.quotes.single +import arrow.core.left +import arrow.core.right import com.google.common.truth.Truth -import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.data.quotes.multi.DefaultMultiQuoteStatusFetcher -import com.tangem.data.quotes.store.QuotesStatusesStore -import com.tangem.data.quotes.store.setSourceAsCache -import com.tangem.data.quotes.store.setSourceAsOnlyCache -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.common.response.ApiResponseError -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse -import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.coVerifyOrder import io.mockk.mockk import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import java.math.BigDecimal +import org.junit.jupiter.api.TestInstance +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultSingleQuoteStatusFetcherTest { - private val tangemTechApi = mockk(relaxed = true) - private val appCurrencyResponseStore = mockk(relaxed = true) - private val quotesStore = mockk(relaxed = true) - - private val multiFetcher = DefaultMultiQuoteStatusFetcher( - tangemTechApi = tangemTechApi, - appCurrencyResponseStore = appCurrencyResponseStore, - quotesStatusesStore = quotesStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - + private val multiFetcher = mockk() private val singleFetcher = DefaultSingleQuoteStatusFetcher(multiFetcher) - @Test - fun `fetch single quote successfully`() = runTest { - val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) - - coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency - - val coinIds = "BTC" - coEvery { - tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - } returns ApiResponse.Success(successResponse) - - val actual = singleFetcher(params) - - coVerifyOrder { - quotesStore.setSourceAsCache(currenciesIds = setOf(params.rawCurrencyId)) - appCurrencyResponseStore.getSyncOrNull() - tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.store(values = successResponse.quotes) - } - - coVerify(inverse = true) { - quotesStore.setSourceAsOnlyCache(currenciesIds = any()) - } - - Truth.assertThat(actual.isRight()).isTrue() + @BeforeEach + fun resetMocks() { + clearMocks(multiFetcher) } @Test - fun `fetch single quote successfully if appCurrencyId from params is not null`() = runTest { - val appCurrencyId = "usd" - val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) + fun `fetch successfully if multiFetcher returns success`() = runTest { + // Arrange + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currencyId, appCurrencyId = null) - val coinIds = "BTC" - coEvery { - tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds) - } returns ApiResponse.Success(successResponse) + val multiFetcherParams = MultiQuoteStatusFetcher.Params( + currenciesIds = setOf(params.rawCurrencyId), + appCurrencyId = params.appCurrencyId, + ) + coEvery { multiFetcher(multiFetcherParams) } returns Unit.right() + + // Act val actual = singleFetcher(params) - coVerifyOrder { - quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) - tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.store(values = successResponse.quotes) - } + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) - Truth.assertThat(actual.isRight()).isTrue() + coVerify(exactly = 1) { multiFetcher(multiFetcherParams) } } @Test - fun `fetch single quote failure because appCurrencyId from params is blank`() = runTest { - val appCurrencyId = "" - val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) + fun `fetch failure if multiFetcher returns erro`() = runTest { + // Arrange + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currencyId, appCurrencyId = null) + val multiFetcherParams = MultiQuoteStatusFetcher.Params( + currenciesIds = setOf(params.rawCurrencyId), + appCurrencyId = params.appCurrencyId, + ) + + val multiFetcherError = IllegalStateException("").left() + + coEvery { multiFetcher(multiFetcherParams) } returns multiFetcherError + + // Act val actual = singleFetcher(params) - coVerifyOrder { - quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) - quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) - } + // Assert + val expected = multiFetcherError + Truth.assertThat(actual).isEqualTo(expected) - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(IllegalStateException::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat() - .isEqualTo("Unable to get AppCurrency for updating quotes") - } - - @Test - fun `fetch single quote failure because api request failed`() = runTest { - val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) - - coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency - - val coinIds = "BTC" - - @Suppress("UNCHECKED_CAST") - val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse - coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse - - val actual = singleFetcher(params) - - coVerifyOrder { - quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) - appCurrencyResponseStore.getSyncOrNull() - tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) - } - - coVerify(inverse = true) { - quotesStore.store(values = any()) - } - - Truth.assertThat(actual.isLeft()).isTrue() - } - - @Test - fun `fetch single quote failure because app currency not found`() = runTest { - val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) - - coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null - - val actual = singleFetcher(params) - - coVerifyOrder { - quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) - appCurrencyResponseStore.getSyncOrNull() - quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) - } - - coVerify(inverse = true) { - tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.store(values = any()) - } - - Truth.assertThat(actual.isLeft()).isTrue() + coVerify(exactly = 1) { multiFetcher(multiFetcherParams) } } private companion object { - val currenciesId = CryptoCurrency.RawID(value = "BTC") - - val usdAppCurrency = CurrenciesResponse.Currency( - id = "USD".lowercase(), - code = "USD", - name = "US Dollar", - unit = "$", - type = "fiat", - rateBTC = "", - ) - - val successResponse = QuotesResponse( - quotes = mapOf( - "BTC" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE), - ), - ) + val currencyId = CryptoCurrency.RawID(value = "BTC") } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 3e3faade06..09eaf90d10 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -3,6 +3,7 @@ package com.tangem.data.visa import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData +import arrow.core.getOrElse import arrow.fx.coroutines.parZip import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType @@ -10,10 +11,9 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.visa.config.VisaLibLoader import com.tangem.data.visa.utils.* -import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.visa.TangemVisaApi import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -38,7 +38,7 @@ import javax.inject.Singleton @Singleton internal class DefaultVisaRepository @Inject constructor( private val visaLibLoader: VisaLibLoader, - private val tangemTechApi: TangemTechApi, + private val quotesFetcher: QuotesFetcher, private val cacheRegistry: CacheRegistry, private val userWalletsStore: UserWalletsStore, private val dispatchers: CoroutineDispatcherProvider, @@ -190,10 +190,13 @@ internal class DefaultVisaRepository @Inject constructor( private suspend fun getFiatRate(): BigDecimal { val fiatCurrencyId = VisaConstants.fiatCurrency.code.lowercase() - val quotes = tangemTechApi.getQuotes( - currencyId = fiatCurrencyId, - coinIds = VisaConstants.TOKEN_ID, - ).getOrThrow() + + val quotes = quotesFetcher.fetch( + fiatCurrencyId = fiatCurrencyId, + currencyId = VisaConstants.TOKEN_ID, + field = QuotesFetcher.Field.PRICE, + ) + .getOrElse { error("Cause: $it") } return quotes.quotes[VisaConstants.TOKEN_ID]?.price ?: error("No price found") } From 451fdc5e0bd57eaf06ef10abc43e31e2b71a8f81 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Jun 2025 17:19:42 +0500 Subject: [PATCH 59/99] Updated on 2026-08-14 --- .../ui/amountScreen/utils/FormatterUtils.kt | 8 +- .../ethereum/WcEthSendTransactionUseCase.kt | 2 +- .../ethereum/WcEthSignTransactionUseCase.kt | 2 +- .../usecase/method/WcTransactionUseCase.kt | 2 +- features/send-v2/api/build.gradle.kts | 13 +- .../send/v2/api/FeeSelectorBlockComponent.kt | 3 +- .../send/v2/api/params/FeeSelectorParams.kt | 27 +++ .../DefaultFeeSelectorBlockComponent.kt | 129 +++++++++++-- .../v2/feeselector/entity/FeeSelectorUM.kt | 34 ++-- .../v2/feeselector/model/FeeSelectorModel.kt | 78 +++++++- .../model/transformers/FeeItemConverter.kt | 64 +++++++ .../FeeItemSelectedTransformer.kt | 13 ++ .../FeeSelectorCustomFieldConverter.kt | 114 +++++++++++ .../FeeSelectorErrorTransformer.kt | 12 ++ .../FeeSelectorLoadedTransformer.kt | 54 ++++++ .../ui/FeeSelectorModalBottomSheet.kt | 181 +++++++++--------- .../converters/SendFeeCustomFieldConverter.kt | 8 +- .../bitcoin/BitcoinCustomFeeConverter.kt | 12 +- .../ethereum/EthereumCustomFeeConverter.kt | 14 +- .../ethereum/EthereumEIPCustomFeeConverter.kt | 7 +- .../EthereumLegacyCustomFeeConverter.kt | 5 +- .../custom/kaspa/KaspaCustomFeeConverter.kt | 5 +- features/walletconnect/impl/build.gradle.kts | 3 + .../routing/DefaultWcRoutingComponent.kt | 2 - .../chain/WcAddNetworkContainerComponent.kt | 4 +- .../WcCommonTransactionComponentDelegate.kt | 6 - .../send/WcSendTransactionComponent.kt | 15 +- .../WcSendTransactionContainerComponent.kt | 6 +- .../sign/WcSignTransactionComponent.kt | 3 - .../WcSignTransactionContainerComponent.kt | 5 +- .../converter/WcSendTransactionUMConverter.kt | 13 +- .../entity/common/WcTransactionFeeState.kt | 9 + .../entity/send/WcSendTransactionUM.kt | 3 +- .../model/WcSendTransactionModel.kt | 93 ++++++--- .../ui/common/WcSendTransactionItems.kt | 6 +- .../send/WcSendTransactionModalBottomSheet.kt | 6 +- ...cSignTransactionModalBottomSheetContent.kt | 13 +- 37 files changed, 748 insertions(+), 226 deletions(-) create mode 100644 features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemSelectedTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorErrorTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionFeeState.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt index c578193df0..9ee08290a8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt @@ -13,12 +13,18 @@ fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurr return stringReference(formattedFiat) } -fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { +fun getFiatString( + value: BigDecimal?, + rate: BigDecimal?, + appCurrency: AppCurrency, + approximate: Boolean = false, +): String { if (value == null || rate == null) return EMPTY_BALANCE_SIGN val feeValue = value.multiply(rate) return BigDecimalFormatter.formatFiatAmount( fiatAmount = feeValue, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, + withApproximateSign = approximate, ) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index 9a4e8e8d7c..ccce4e7bad 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -102,7 +102,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( emit(newState) } - override suspend fun dAppFee(): Fee.Ethereum.Legacy? { + override fun dAppFee(): Fee.Ethereum.Legacy? { return dAppFee } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index 2726cb8389..ed984c971c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -123,7 +123,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( emitAll(delegate.invoke(transactionData)) } - override suspend fun dAppFee(): Fee.Ethereum.Legacy? { + override fun dAppFee(): Fee.Ethereum.Legacy? { return dAppFee } diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt index d4f126ead7..c4410c3a7f 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt @@ -34,7 +34,7 @@ interface WcListTransactionUseCase : * [updateFee] triggered a new [TransactionData] emit */ interface WcMutableFee { - suspend fun dAppFee(): Fee? + fun dAppFee(): Fee? fun updateFee(fee: Fee) } diff --git a/features/send-v2/api/build.gradle.kts b/features/send-v2/api/build.gradle.kts index 9aa3093878..43576f6348 100644 --- a/features/send-v2/api/build.gradle.kts +++ b/features/send-v2/api/build.gradle.kts @@ -16,10 +16,17 @@ dependencies { /** Domain models */ api(projects.domain.models) implementation(projects.domain.appCurrency.models) - implementation(projects.domain.wallets.models) - implementation(projects.domain.tokens.models) implementation(projects.domain.nft.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction.models) + implementation(projects.domain.wallets.models) - /* Compose */ + /** Compose */ implementation(deps.compose.runtime) + + /** Tangem */ + implementation(tangemDeps.blockchain) + + /** Other */ + implementation(deps.arrow.core) } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt index bfb687f2eb..adbe917341 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt @@ -2,8 +2,9 @@ package com.tangem.features.send.v2.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.api.params.FeeSelectorParams interface FeeSelectorBlockComponent : ComposableContentComponent { - interface Factory : ComponentFactory + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt new file mode 100644 index 0000000000..946936b160 --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt @@ -0,0 +1,27 @@ +package com.tangem.features.send.v2.api.params + +import arrow.core.Either +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.network.Network +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError + +sealed class FeeSelectorParams { + abstract val onLoadFee: suspend () -> Either + abstract val network: Network + abstract val cryptoCurrencyStatus: CryptoCurrencyStatus + + data class FeeSelectorBlockParams( + override val onLoadFee: suspend () -> Either, + override val network: Network, + override val cryptoCurrencyStatus: CryptoCurrencyStatus, + val suggestedFeeState: SuggestedFeeState, + ) : FeeSelectorParams() + + sealed class SuggestedFeeState { + data object None : SuggestedFeeState() + data class Suggestion(val title: TextReference, val fee: Fee) : SuggestedFeeState() + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index 96c3c5fd9e..e619f49d9b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -6,41 +6,70 @@ import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateUM +import com.tangem.features.send.v2.feeselector.entity.FeeItem +import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.entity.PrimaryButtonConfig +import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel import com.tangem.features.send.v2.impl.R import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal -@Suppress("UnusedPrivateMember") internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted params: Unit, + @Assisted params: FeeSelectorParams.FeeSelectorBlockParams, ) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext { + private val model: FeeSelectorModel = getOrCreateModel(params = params) + @Composable override fun Content(modifier: Modifier) { - FeeSelectorBlockContent(modifier = modifier) + val state by model.uiState.collectAsStateWithLifecycle() + FeeSelectorBlockContent(modifier = modifier, state = state) } @AssistedFactory interface Factory : FeeSelectorBlockComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultFeeSelectorBlockComponent + override fun create( + context: AppComponentContext, + params: FeeSelectorParams.FeeSelectorBlockParams, + ): DefaultFeeSelectorBlockComponent } } @Composable -private fun FeeSelectorBlockContent(modifier: Modifier = Modifier) { +private fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier = Modifier) { Row( modifier = modifier .background(TangemTheme.colors.background.action) @@ -67,26 +96,90 @@ private fun FeeSelectorBlockContent(modifier: Modifier = Modifier) { contentDescription = null, tint = TangemTheme.colors.icon.informative, ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "~ 0.25 \$", - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - ) - Icon( - modifier = Modifier.size(width = 18.dp, height = 24.dp), - painter = painterResource(id = R.drawable.ic_select_18_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) + SpacerWMax() + when (state) { + is FeeSelectorUM.Content -> FeeContent(state) + is FeeSelectorUM.Loading -> FeeLoading() + is FeeSelectorUM.Error -> FeeError() + } } } +@Composable +private fun RowScope.FeeError() { + Text( + text = EMPTY_BALANCE_SIGN, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2, + ) +} + +@Composable +private fun RowScope.FeeLoading() { + TextShimmer( + radius = TangemTheme.dimens.radius3, + style = TangemTheme.typography.body1, + modifier = Modifier.width(width = TangemTheme.dimens.size90), + ) +} + +@Composable +private fun RowScope.FeeContent(state: FeeSelectorUM.Content) { + EllipsisText( + text = if (state.feeFiatRateUM != null) { + getFiatString( + value = state.selectedFeeItem.fee.amount.value, + rate = state.feeFiatRateUM.rate, + appCurrency = state.feeFiatRateUM.appCurrency, + approximate = state.isFeeApproximate, + ) + } else { + state.selectedFeeItem.fee.amount.value.format { + crypto( + symbol = state.selectedFeeItem.fee.amount.currencySymbol, + decimals = state.selectedFeeItem.fee.amount.decimals, + ).fee(canBeLower = state.isFeeApproximate) + } + }, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + modifier = Modifier + .weight(1f) + .padding(start = TangemTheme.dimens.spacing4), + ) + Icon( + modifier = Modifier.size(width = 18.dp, height = 24.dp), + painter = painterResource(id = R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) +} + @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun FeeSelectorBlockContent_Preview() { TangemThemePreview { - FeeSelectorBlockContent(modifier = Modifier.fillMaxWidth()) + val feeItem = FeeItem.Market( + Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)), + ) + FeeSelectorBlockContent( + modifier = Modifier.fillMaxWidth(), + state = FeeSelectorUM.Content( + doneButtonConfig = PrimaryButtonConfig(enabled = true, onClick = {}), + feeItems = persistentListOf(feeItem), + onFeeSelected = {}, + selectedFeeItem = feeItem, + isFeeApproximate = false, + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("2500"), + appCurrency = AppCurrency.Default, + ), + displayNonceInput = false, + nonce = null, + onNonceChange = {}, + ), + ) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt index f12dfd588a..5d5272f9e0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt @@ -1,9 +1,10 @@ package com.tangem.features.send.v2.feeselector.entity import androidx.compose.runtime.Immutable -import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -12,33 +13,44 @@ import java.math.BigInteger @Immutable internal sealed class FeeSelectorUM { - data object Loading : FeeSelectorUM() + abstract val doneButtonConfig: PrimaryButtonConfig + + data class Loading(private val onDone: () -> Unit) : FeeSelectorUM() { + override val doneButtonConfig = PrimaryButtonConfig(enabled = false, onClick = onDone) + } + + data class Error(val error: GetFeeError, private val onDone: () -> Unit) : FeeSelectorUM() { + override val doneButtonConfig = PrimaryButtonConfig(enabled = false, onClick = onDone) + } data class Content( - val isDoneEnabled: Boolean, + override val doneButtonConfig: PrimaryButtonConfig, val feeItems: ImmutableList, + val onFeeSelected: (FeeItem) -> Unit, val selectedFeeItem: FeeItem, val isFeeApproximate: Boolean, - val feeFiatRateDataHolder: FeeFiatRateDataHolder?, + val feeFiatRateUM: FeeFiatRateUM?, val displayNonceInput: Boolean, val nonce: BigInteger?, val onNonceChange: (String) -> Unit, ) : FeeSelectorUM() } +internal data class PrimaryButtonConfig(val enabled: Boolean, val onClick: () -> Unit) + @Immutable -data class FeeFiatRateDataHolder( +internal data class FeeFiatRateUM( val rate: BigDecimal, val appCurrency: AppCurrency, ) @Immutable internal sealed class FeeItem { - abstract val onSelect: () -> Unit + abstract val fee: Fee - data class Suggested(val title: TextReference, val amount: Amount, override val onSelect: () -> Unit) : FeeItem() - data class Slow(val amount: Amount, override val onSelect: () -> Unit) : FeeItem() - data class Market(val amount: Amount, override val onSelect: () -> Unit) : FeeItem() - data class Fast(val amount: Amount, override val onSelect: () -> Unit) : FeeItem() - data class Custom(val customValues: ImmutableList, override val onSelect: () -> Unit) : FeeItem() + data class Suggested(val title: TextReference, override val fee: Fee) : FeeItem() + data class Slow(override val fee: Fee) : FeeItem() + data class Market(override val fee: Fee) : FeeItem() + data class Fast(override val fee: Fee) : FeeItem() + data class Custom(override val fee: Fee, val customValues: ImmutableList) : FeeItem() } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index fa4ea825a6..7e2b1df414 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -1,31 +1,101 @@ package com.tangem.features.send.v2.feeselector.model import androidx.compose.runtime.Stable +import arrow.core.getOrElse +import com.tangem.blockchain.common.AmountType import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router -import com.tangem.features.send.v2.api.FeeSelectorComponent +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.feeselector.entity.FeeItem import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.model.transformers.FeeItemSelectedTransformer +import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorErrorTransformer +import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorLoadedTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @Stable @ModelScoped internal class FeeSelectorModel @Inject constructor( paramsContainer: ParamsContainer, + private val isFeeApproximateUseCase: IsFeeApproximateUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { - @Suppress("UnusedPrivateMember") - private val params = paramsContainer.require() + private val params = paramsContainer.require() + private var appCurrency: AppCurrency = AppCurrency.Default + val uiState: StateFlow - field = MutableStateFlow(FeeSelectorUM.Loading) + field = MutableStateFlow(FeeSelectorUM.Loading(onDone = ::onDone)) + + init { + initAppCurrency() + loadFee() + } fun dismiss() { router.pop() } + + private fun initAppCurrency() { + modelScope.launch { + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + } + } + + private fun loadFee() { + modelScope.launch { + params.onLoadFee() + .fold( + ifLeft = { uiState.update(FeeSelectorErrorTransformer(it)) }, + ifRight = { + uiState.update( + FeeSelectorLoadedTransformer( + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + appCurrency = appCurrency, + fees = it, + suggestedFeeState = params.suggestedFeeState, + isFeeApproximate = isFeeApproximate(it.normal.amount.type), + onFeeSelected = ::onFeeItemSelected, + onCustomFeeValueChange = ::onCustomFeeValueChange, + onNextClick = ::onNextClick, + ), + ) + }, + ) + } + } + + private fun isFeeApproximate(amountType: AmountType): Boolean { + val networkId = params.network.id + return isFeeApproximateUseCase(networkId = networkId, amountType = amountType) + } + + private fun onFeeItemSelected(feeItem: FeeItem) { + uiState.update(FeeItemSelectedTransformer(feeItem)) + } + + @Suppress("UnusedPrivateMember") + private fun onCustomFeeValueChange(index: Int, value: String) { + // TODO: [REDACTED_JIRA] + } + + private fun onNextClick() { + // TODO: [REDACTED_JIRA] + } + + private fun onDone() { + // TODO: [REDACTED_JIRA] + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt new file mode 100644 index 0000000000..6ec3832f8f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt @@ -0,0 +1,64 @@ +package com.tangem.features.send.v2.feeselector.model.transformers + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.feeselector.entity.FeeItem +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class FeeItemConverter( + private val suggestedFeeState: FeeSelectorParams.SuggestedFeeState, + private val normalFee: Fee, + private val onCustomFeeValueChange: (Int, String) -> Unit, + private val onNextClick: () -> Unit, + private val appCurrency: AppCurrency, + cryptoCurrencyStatus: CryptoCurrencyStatus, +) : Converter> { + + private val customFeeFieldConverter = FeeSelectorCustomFieldConverter( + onCustomFeeValueChange = onCustomFeeValueChange, + onNextClick = onNextClick, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = cryptoCurrencyStatus, + normalFee = normalFee, + ) + + override fun convert(value: TransactionFee): ImmutableList { + val fees = mutableListOf() + + when (suggestedFeeState) { + FeeSelectorParams.SuggestedFeeState.None -> Unit + is FeeSelectorParams.SuggestedFeeState.Suggestion -> fees.add( + FeeItem.Suggested( + title = suggestedFeeState.title, + fee = suggestedFeeState.fee, + ), + ) + } + when (value) { + is TransactionFee.Choosable -> { + fees.add(FeeItem.Slow(fee = value.minimum)) + fees.add(FeeItem.Market(fee = value.normal)) + fees.add(FeeItem.Fast(fee = value.priority)) + } + is TransactionFee.Single -> { + fees.add(FeeItem.Market(fee = value.normal)) + } + } + val customFeeFields = customFeeFieldConverter.convert(normalFee) + if (customFeeFields.isNotEmpty()) { + fees.add( + FeeItem.Custom( + fee = customFeeFieldConverter.convertBack(customFeeFields), + customValues = customFeeFields, + ), + ) + } + + return fees.toImmutableList() + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemSelectedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemSelectedTransformer.kt new file mode 100644 index 0000000000..f8b81f56e7 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemSelectedTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.features.send.v2.feeselector.model.transformers + +import com.tangem.features.send.v2.feeselector.entity.FeeItem +import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.utils.transformer.Transformer + +internal class FeeItemSelectedTransformer(private val selectedFeeItem: FeeItem) : Transformer { + override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { + prevState as? FeeSelectorUM.Content ?: return prevState + + return prevState.copy(selectedFeeItem = selectedFeeItem) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt new file mode 100644 index 0000000000..3357fdd5d9 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt @@ -0,0 +1,114 @@ +package com.tangem.features.send.v2.feeselector.model.transformers + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.utils.converter.TwoWayConverter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class FeeSelectorCustomFieldConverter( + private val onCustomFeeValueChange: (Int, String) -> Unit, + private val onNextClick: () -> Unit, + private val appCurrency: AppCurrency, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + private val normalFee: Fee, +) : TwoWayConverter> { + + private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { + EthereumCustomFeeConverter( + onCustomFeeValueChange = onCustomFeeValueChange, + onNextClick = onNextClick, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + + private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { + BitcoinCustomFeeConverter( + onCustomFeeValueChange = onCustomFeeValueChange, + onNextClick = onNextClick, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + + private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { + KaspaCustomFeeConverter( + onCustomFeeValueChange = onCustomFeeValueChange, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + + override fun convert(value: Fee): ImmutableList { + return when (value) { + is Fee.Ethereum -> ethereumCustomFeeConverter.convert(value) + is Fee.Bitcoin -> bitcoinCustomFeeConverter.convert(value) + is Fee.Kaspa -> kaspaCustomFeeConverter.convert(value) + else -> persistentListOf() + } + } + + override fun convertBack(value: ImmutableList): Fee { + return if (value.isEmpty()) { + normalFee + } else { + when (normalFee) { + is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = value) + is Fee.Bitcoin -> bitcoinCustomFeeConverter.convertBack(normalFee = normalFee, value = value) + is Fee.Kaspa -> kaspaCustomFeeConverter.convertBack(normalFee = normalFee, value = value) + else -> { + val customFee = value.firstOrNull() + Fee.Common( + normalFee.amount.copy( + value = customFee?.value?.parseToBigDecimal(customFee.decimals), + ), + ) + } + } + } + } + + fun onValueChange(feeSelectorState: FeeSelectorUM.Content, index: Int, value: String) = + when (val fee = feeSelectorState.fees.normal) { + is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange( + feeValue = fee, + customValues = feeSelectorState.customValues, + index = index, + value = value, + ) + is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange( + customValues = feeSelectorState.customValues, + index = index, + value = value, + txSize = fee.txSize, + ) + is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange( + customValues = feeSelectorState.customValues, + index = index, + value = value, + ) + else -> feeSelectorState.customValues + } + + fun tryAutoFixValue(feeSelectorState: FeeSelectorUM.Content) = when (feeSelectorState.fees) { + is TransactionFee.Choosable -> feeSelectorState.fees.minimum + is TransactionFee.Single -> feeSelectorState.fees.normal + }.let { + when (it) { + is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue( + minimumFee = it, + customValues = feeSelectorState.customValues, + ) + else -> feeSelectorState.customValues + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorErrorTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorErrorTransformer.kt new file mode 100644 index 0000000000..d4340701a9 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorErrorTransformer.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.v2.feeselector.model.transformers + +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.utils.transformer.Transformer + +internal class FeeSelectorErrorTransformer(private val error: GetFeeError) : Transformer { + + override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { + return FeeSelectorUM.Error(error = error, onDone = prevState.doneButtonConfig.onClick) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt new file mode 100644 index 0000000000..a33f5e831f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt @@ -0,0 +1,54 @@ +package com.tangem.features.send.v2.feeselector.model.transformers + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateUM +import com.tangem.features.send.v2.feeselector.entity.FeeItem +import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList + +@Suppress("LongParameterList") +internal class FeeSelectorLoadedTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val appCurrency: AppCurrency, + private val fees: TransactionFee, + private val suggestedFeeState: FeeSelectorParams.SuggestedFeeState, + private val isFeeApproximate: Boolean, + private val onFeeSelected: (FeeItem) -> Unit, + private val onCustomFeeValueChange: (Int, String) -> Unit, + private val onNextClick: () -> Unit, +) : Transformer { + + private val feeItemsConverter = FeeItemConverter( + suggestedFeeState = suggestedFeeState, + normalFee = fees.normal, + onCustomFeeValueChange = onCustomFeeValueChange, + onNextClick = onNextClick, + appCurrency = appCurrency, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + + override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { + val feeItems: ImmutableList = feeItemsConverter.convert(fees) + val selectedFee = feeItems.find { it is FeeItem.Suggested } ?: feeItems.first { it is FeeItem.Market } + return FeeSelectorUM.Content( + doneButtonConfig = prevState.doneButtonConfig.copy(enabled = true), + feeItems = feeItems, + selectedFeeItem = selectedFee, + isFeeApproximate = isFeeApproximate, + onFeeSelected = onFeeSelected, + feeFiatRateUM = cryptoCurrencyStatus.value.fiatRate?.let { rate -> + FeeFiatRateUM( + rate = rate, + appCurrency = appCurrency, + ) + }, + displayNonceInput = false, + nonce = null, + onNonceChange = {}, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index e904bf6477..9acf05e797 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.atoms.text.EllipsisText @@ -47,9 +48,10 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateDataHolder +import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateUM import com.tangem.features.send.v2.feeselector.entity.FeeItem import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.entity.PrimaryButtonConfig import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.utils.StringsSigns @@ -88,8 +90,8 @@ internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () -> .fillMaxWidth() .padding(16.dp), text = stringResourceSafe(R.string.common_done), - onClick = onDismiss, - enabled = state.isDoneEnabled, + onClick = state.doneButtonConfig.onClick, + enabled = state.doneButtonConfig.enabled, ) }, ) @@ -114,7 +116,7 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = }, label = "Fee selector icon background change", ) - val onSelect by rememberUpdatedState(item.onSelect) + val onSelect by rememberUpdatedState(state.onFeeSelected) val itemModifier = Modifier .fillMaxWidth() .background(TangemTheme.colors.background.primary) @@ -133,7 +135,7 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = Modifier }, ) - .clickableSingle(onClick = onSelect) + .clickableSingle(onClick = { onSelect(item) }) when (item) { is FeeItem.Suggested -> RegularFeeItemContent( modifier = itemModifier, @@ -142,23 +144,23 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = iconBackgroundColor = iconBackgroundColor, iconTint = iconTint, preDot = stringReference( - item.amount.value.format { + item.fee.amount.value.format { crypto( - symbol = item.amount.currencySymbol, - decimals = item.amount.decimals, + symbol = item.fee.amount.currencySymbol, + decimals = item.fee.amount.decimals, ).fee(canBeLower = state.isFeeApproximate) }, ), - postDot = if (state.feeFiatRateDataHolder != null) { + postDot = if (state.feeFiatRateUM != null) { getFiatReference( - value = item.amount.value, - rate = state.feeFiatRateDataHolder.rate, - appCurrency = state.feeFiatRateDataHolder.appCurrency, + value = item.fee.amount.value, + rate = state.feeFiatRateUM.rate, + appCurrency = state.feeFiatRateUM.appCurrency, ) } else { null }, - ellipsizeOffset = item.amount.currencySymbol.length, + ellipsizeOffset = item.fee.amount.currencySymbol.length, showDivider = !isSelected && !lastItem, ) is FeeItem.Slow -> RegularFeeItemContent( @@ -168,23 +170,23 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = iconBackgroundColor = iconBackgroundColor, iconTint = iconTint, preDot = stringReference( - item.amount.value.format { + item.fee.amount.value.format { crypto( - symbol = item.amount.currencySymbol, - decimals = item.amount.decimals, + symbol = item.fee.amount.currencySymbol, + decimals = item.fee.amount.decimals, ).fee(canBeLower = state.isFeeApproximate) }, ), - postDot = if (state.feeFiatRateDataHolder != null) { + postDot = if (state.feeFiatRateUM != null) { getFiatReference( - value = item.amount.value, - rate = state.feeFiatRateDataHolder.rate, - appCurrency = state.feeFiatRateDataHolder.appCurrency, + value = item.fee.amount.value, + rate = state.feeFiatRateUM.rate, + appCurrency = state.feeFiatRateUM.appCurrency, ) } else { null }, - ellipsizeOffset = item.amount.currencySymbol.length, + ellipsizeOffset = item.fee.amount.currencySymbol.length, showDivider = !isSelected && !lastItem, ) is FeeItem.Market -> RegularFeeItemContent( @@ -194,23 +196,23 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = iconBackgroundColor = iconBackgroundColor, iconTint = iconTint, preDot = stringReference( - item.amount.value.format { + item.fee.amount.value.format { crypto( - symbol = item.amount.currencySymbol, - decimals = item.amount.decimals, + symbol = item.fee.amount.currencySymbol, + decimals = item.fee.amount.decimals, ).fee(canBeLower = state.isFeeApproximate) }, ), - postDot = if (state.feeFiatRateDataHolder != null) { + postDot = if (state.feeFiatRateUM != null) { getFiatReference( - value = item.amount.value, - rate = state.feeFiatRateDataHolder.rate, - appCurrency = state.feeFiatRateDataHolder.appCurrency, + value = item.fee.amount.value, + rate = state.feeFiatRateUM.rate, + appCurrency = state.feeFiatRateUM.appCurrency, ) } else { null }, - ellipsizeOffset = item.amount.currencySymbol.length, + ellipsizeOffset = item.fee.amount.currencySymbol.length, showDivider = !isSelected && !lastItem, ) is FeeItem.Fast -> RegularFeeItemContent( @@ -220,23 +222,23 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = iconBackgroundColor = iconBackgroundColor, iconTint = iconTint, preDot = stringReference( - item.amount.value.format { + item.fee.amount.value.format { crypto( - symbol = item.amount.currencySymbol, - decimals = item.amount.decimals, + symbol = item.fee.amount.currencySymbol, + decimals = item.fee.amount.decimals, ).fee(canBeLower = state.isFeeApproximate) }, ), - postDot = if (state.feeFiatRateDataHolder != null) { + postDot = if (state.feeFiatRateUM != null) { getFiatReference( - value = item.amount.value, - rate = state.feeFiatRateDataHolder.rate, - appCurrency = state.feeFiatRateDataHolder.appCurrency, + value = item.fee.amount.value, + rate = state.feeFiatRateUM.rate, + appCurrency = state.feeFiatRateUM.appCurrency, ) } else { null }, - ellipsizeOffset = item.amount.currencySymbol.length, + ellipsizeOffset = item.fee.amount.currencySymbol.length, showDivider = !isSelected && !lastItem, ) is FeeItem.Custom -> CustomFeeBlock( @@ -471,34 +473,24 @@ private fun FeeSelectorBS_Preview( private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider( collection = listOf( FeeSelectorUM.Content( - isDoneEnabled = false, + doneButtonConfig = PrimaryButtonConfig(enabled = true, onClick = {}), feeItems = persistentListOf( FeeItem.Suggested( title = stringReference("Suggested by Tangem"), - amount = Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum), - onSelect = onSelectStub, + fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)), ), - FeeItem.Slow( - amount = Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum), - onSelect = onSelectStub, - ), - FeeItem.Market( - amount = Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum), - onSelect = onSelectStub, - ), - FeeItem.Fast( - amount = Amount(value = BigDecimal("0.3"), blockchain = Blockchain.Ethereum), - onSelect = onSelectStub, - ), - FeeItem.Custom(customValues = customFeeFields, onSelect = onSelectStub), + FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))), + FeeItem.Market(fee = Fee.Common(Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum))), + FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))), + customFeeItem, ), + onFeeSelected = {}, // selectedFeeItem = FeeItem.Market( // amount = Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum), - // onSelect = onSelectStub // ), - selectedFeeItem = FeeItem.Custom(customValues = customFeeFields, onSelect = onSelectStub), + selectedFeeItem = customFeeItem, isFeeApproximate = true, - feeFiatRateDataHolder = FeeFiatRateDataHolder( + feeFiatRateUM = FeeFiatRateUM( rate = BigDecimal.TEN, appCurrency = AppCurrency.Default, ), @@ -509,43 +501,44 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< ), ) -private val onSelectStub: () -> Unit = {} - -private val customFeeFields = persistentListOf( - CustomFeeFieldUM( - value = "0.119806", - onValueChange = {}, - keyboardOptions = KeyboardOptions(), - keyboardActions = KeyboardActions(), - symbol = "ETH", - decimals = 8, - title = resourceReference(R.string.send_max_fee), - footer = resourceReference(R.string.send_custom_amount_fee_footer), - label = stringReference("~ 0,03 \$"), - isReadonly = false, - ), - CustomFeeFieldUM( - value = "40000", - onValueChange = {}, - keyboardOptions = KeyboardOptions(), - keyboardActions = KeyboardActions(), - symbol = "GWEI", - decimals = 8, - title = resourceReference(R.string.send_gas_price), - footer = resourceReference(R.string.send_gas_price_footer), - label = null, - isReadonly = false, - ), - CustomFeeFieldUM( - value = "31400", - onValueChange = {}, - keyboardOptions = KeyboardOptions(), - keyboardActions = KeyboardActions(), - symbol = null, - decimals = 8, - title = resourceReference(R.string.send_gas_limit), - footer = resourceReference(R.string.send_gas_limit_footer), - label = null, - isReadonly = false, +private val customFeeItem = FeeItem.Custom( + fee = Fee.Common(Amount(value = BigDecimal("0.05"), blockchain = Blockchain.Ethereum)), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.119806", + onValueChange = {}, + keyboardOptions = KeyboardOptions(), + keyboardActions = KeyboardActions(), + symbol = "ETH", + decimals = 8, + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_custom_amount_fee_footer), + label = stringReference("~ 0,03 \$"), + isReadonly = false, + ), + CustomFeeFieldUM( + value = "40000", + onValueChange = {}, + keyboardOptions = KeyboardOptions(), + keyboardActions = KeyboardActions(), + symbol = "GWEI", + decimals = 8, + title = resourceReference(R.string.send_gas_price), + footer = resourceReference(R.string.send_gas_price_footer), + label = null, + isReadonly = false, + ), + CustomFeeFieldUM( + value = "31400", + onValueChange = {}, + keyboardOptions = KeyboardOptions(), + keyboardActions = KeyboardActions(), + symbol = null, + decimals = 8, + title = resourceReference(R.string.send_gas_limit), + footer = resourceReference(R.string.send_gas_limit_footer), + label = null, + isReadonly = false, + ), ), ) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt index 78304904a5..1f6cfc86d7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt @@ -24,7 +24,8 @@ internal class SendFeeCustomFieldConverter( private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { EthereumCustomFeeConverter( - clickIntents = clickIntents, + onCustomFeeValueChange = clickIntents::onCustomFeeValueChange, + onNextClick = clickIntents::onNextClick, appCurrency = appCurrency, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) @@ -32,7 +33,8 @@ internal class SendFeeCustomFieldConverter( private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { BitcoinCustomFeeConverter( - clickIntents = clickIntents, + onCustomFeeValueChange = clickIntents::onCustomFeeValueChange, + onNextClick = clickIntents::onNextClick, appCurrency = appCurrency, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) @@ -40,7 +42,7 @@ internal class SendFeeCustomFieldConverter( private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { KaspaCustomFeeConverter( - clickIntents = clickIntents, + onCustomFeeValueChange = clickIntents::onCustomFeeValueChange, appCurrency = appCurrency, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt index a2f5349672..feaed422ac 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt @@ -12,7 +12,6 @@ import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.impl.R -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM @@ -24,7 +23,8 @@ import java.math.BigDecimal import java.math.RoundingMode internal class BitcoinCustomFeeConverter( - private val clickIntents: SendFeeClickIntents, + private val onCustomFeeValueChange: (Int, String) -> Unit, + private val onNextClick: () -> Unit, private val appCurrency: AppCurrency, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, ) : CustomFeeConverter { @@ -40,7 +40,7 @@ internal class BitcoinCustomFeeConverter( value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), decimals = value.amount.decimals, symbol = value.amount.currencySymbol, - onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) }, + onValueChange = { onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Companion.Next, keyboardType = KeyboardType.Companion.Number, @@ -65,7 +65,7 @@ internal class BitcoinCustomFeeConverter( symbol = "", title = resourceReference(R.string.send_satoshi_per_byte_title), footer = resourceReference(R.string.send_satoshi_per_byte_text), - onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) }, + onValueChange = { onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) }, keyboardOptions = KeyboardOptions( imeAction = if (checkExceedBalance( feeBalance = currencyStatus.amount, @@ -78,9 +78,7 @@ internal class BitcoinCustomFeeConverter( }, keyboardType = KeyboardType.Companion.Number, ), - keyboardActions = KeyboardActions( - onDone = { clickIntents.onNextClick() }, - ), + keyboardActions = KeyboardActions(onDone = { onNextClick() }), ), ) } else { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt index ce2ba52261..674ef7d066 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt @@ -11,14 +11,14 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.impl.R -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList internal class EthereumCustomFeeConverter( - private val clickIntents: SendFeeClickIntents, + private val onCustomFeeValueChange: (Int, String) -> Unit, + private val onNextClick: () -> Unit, private val appCurrency: AppCurrency, feeCryptoCurrencyStatus: CryptoCurrencyStatus, ) : BaseEthereumCustomFeeConverter { @@ -26,13 +26,13 @@ internal class EthereumCustomFeeConverter( private val currencyStatus = feeCryptoCurrencyStatus.value private val legacyFeeConverter = EthereumLegacyCustomFeeConverter( - clickIntents = clickIntents, + onCustomFeeValueChange = onCustomFeeValueChange, appCurrency = appCurrency, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) private val eipFeeConverter = EthereumEIPCustomFeeConverter( - clickIntents = clickIntents, + onCustomFeeValueChange = onCustomFeeValueChange, appCurrency = appCurrency, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) @@ -84,7 +84,7 @@ internal class EthereumCustomFeeConverter( value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), decimals = value.amount.decimals, symbol = value.amount.currencySymbol, - onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT, it) }, + onValueChange = { onCustomFeeValueChange(FEE_AMOUNT, it) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number), title = resourceReference(R.string.send_max_fee), footer = resourceReference(R.string.send_custom_amount_fee_footer), @@ -106,13 +106,13 @@ internal class EthereumCustomFeeConverter( symbol = "", title = resourceReference(R.string.send_gas_limit), footer = resourceReference(R.string.send_gas_limit_footer), - onValueChange = { clickIntents.onCustomFeeValueChange(getGasLimitIndex(value), it) }, + onValueChange = { onCustomFeeValueChange(getGasLimitIndex(value), it) }, keyboardOptions = KeyboardOptions( imeAction = if (isExceedBalance) ImeAction.None else ImeAction.Done, keyboardType = KeyboardType.Number, ), keyboardActions = KeyboardActions( - onDone = { clickIntents.onNextClick() }, + onDone = { onNextClick() }, ), ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt index 2a7d389e13..24d9dab7fc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt @@ -12,7 +12,6 @@ import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.impl.R -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.FEE_AMOUNT @@ -26,7 +25,7 @@ import kotlinx.collections.immutable.toImmutableList import java.math.RoundingMode internal class EthereumEIPCustomFeeConverter( - private val clickIntents: SendFeeClickIntents, + private val onCustomFeeValueChange: (Int, String) -> Unit, private val appCurrency: AppCurrency, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, ) : BaseEthereumCustomFeeConverter { @@ -41,7 +40,7 @@ internal class EthereumEIPCustomFeeConverter( symbol = ETHEREUM_GAS_UNIT, title = resourceReference(R.string.send_custom_evm_max_fee), footer = resourceReference(R.string.send_custom_evm_max_fee_footer), - onValueChange = { clickIntents.onCustomFeeValueChange(MAX_FEE, it) }, + onValueChange = { onCustomFeeValueChange(MAX_FEE, it) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number), keyboardActions = KeyboardActions(), ), @@ -51,7 +50,7 @@ internal class EthereumEIPCustomFeeConverter( symbol = ETHEREUM_GAS_UNIT, title = resourceReference(R.string.send_custom_evm_priority_fee), footer = resourceReference(R.string.send_custom_evm_priority_fee_footer), - onValueChange = { clickIntents.onCustomFeeValueChange(PRIORITY_FEE, it) }, + onValueChange = { onCustomFeeValueChange(PRIORITY_FEE, it) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number), keyboardActions = KeyboardActions(), ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt index 8b2f86beb7..ac680946cb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt @@ -12,7 +12,6 @@ import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.impl.R -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.FEE_AMOUNT @@ -26,7 +25,7 @@ import kotlinx.collections.immutable.toImmutableList import java.math.RoundingMode internal class EthereumLegacyCustomFeeConverter( - private val clickIntents: SendFeeClickIntents, + private val onCustomFeeValueChange: (Int, String) -> Unit, private val appCurrency: AppCurrency, feeCryptoCurrencyStatus: CryptoCurrencyStatus, ) : BaseEthereumCustomFeeConverter { @@ -41,7 +40,7 @@ internal class EthereumLegacyCustomFeeConverter( symbol = ETHEREUM_GAS_UNIT, title = resourceReference(R.string.send_gas_price), footer = resourceReference(R.string.send_gas_price_footer), - onValueChange = { clickIntents.onCustomFeeValueChange(GAS_PRICE, it) }, + onValueChange = { onCustomFeeValueChange(GAS_PRICE, it) }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, keyboardType = KeyboardType.Number, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt index 2714e7d9f6..4dfd766663 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt @@ -12,7 +12,6 @@ import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.impl.R -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList @@ -21,7 +20,7 @@ import kotlinx.collections.immutable.toImmutableList import java.math.RoundingMode internal class KaspaCustomFeeConverter( - private val clickIntents: SendFeeClickIntents, + private val onCustomFeeValueChange: (Int, String) -> Unit, private val appCurrency: AppCurrency, feeCryptoCurrencyStatus: CryptoCurrencyStatus, ) : CustomFeeConverter { @@ -35,7 +34,7 @@ internal class KaspaCustomFeeConverter( value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), decimals = value.amount.decimals, symbol = value.amount.currencySymbol, - onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) }, + onValueChange = { onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Companion.Next, keyboardType = KeyboardType.Companion.Number, diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 625e3afceb..f0cd4ec491 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.qrScanning.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) implementation(projects.domain.walletConnect) implementation(projects.domain.walletConnect.models) @@ -37,8 +38,10 @@ dependencies { /** Domain */ implementation(projects.domain.appCurrency) implementation(projects.domain.balanceHiding) + implementation(projects.domain.legacy) implementation(projects.domain.qrScanning) implementation(projects.domain.tokens) + implementation(projects.domain.transaction) implementation(projects.domain.wallets) /** DI */ diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt index adbaa219c2..84a1d7c20d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt @@ -64,12 +64,10 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( is WcInnerRoute.SignMessage -> WcSignTransactionContainerComponent( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), - feeSelectorBlockComponentFactory = feeSelectorBlockComponentFactory, ) is WcInnerRoute.AddNetwork -> WcAddNetworkContainerComponent( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), - feeSelectorBlockComponentFactory = feeSelectorBlockComponentFactory, ) is WcInnerRoute.Send -> WcSendTransactionContainerComponent( appComponentContext = childContext, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt index 86d6e850a2..7c9b4b33fc 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt @@ -5,7 +5,6 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.components.common.WcCommonTransactionComponentDelegate import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.common.getWcCommonScreen @@ -15,8 +14,7 @@ import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal class WcAddNetworkContainerComponent( appComponentContext: AppComponentContext, params: WcTransactionModelParams, - feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, -) : WcCommonTransactionComponentDelegate(appComponentContext, feeSelectorBlockComponentFactory) { +) : WcCommonTransactionComponentDelegate(appComponentContext) { private val model: WcAddNetworkModel = getOrCreateModel(params = params) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt index 87ae3eec22..1be9296325 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt @@ -15,12 +15,10 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal abstract class WcCommonTransactionComponentDelegate( private val appComponentContext: AppComponentContext, - private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, ) : AppComponentContext by appComponentContext, ComposableContentComponent { private var stackNavigation: StackNavigation? = null @@ -31,10 +29,6 @@ internal abstract class WcCommonTransactionComponentDelegate( InnerRouter(stackNavigation = it, popCallback = { onChildBack() }) } } - protected val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create( - context = appComponentContext, - params = Unit, - ) protected fun init( navigation: StackNavigation, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt index a6b69fd2b0..4a9271ab98 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt @@ -6,15 +6,28 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel import com.tangem.features.walletconnect.transaction.ui.send.WcSendTransactionModalBottomSheet internal class WcSendTransactionComponent( private val appComponentContext: AppComponentContext, private val model: WcSendTransactionModel, - private val feeSelectorBlockComponent: FeeSelectorBlockComponent, + private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, ) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + private val feeSelectorBlockComponent by lazy { + feeSelectorBlockComponentFactory.create( + context = appComponentContext, + params = FeeSelectorParams.FeeSelectorBlockParams( + onLoadFee = model::loadFee, + network = model.useCase.network, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + suggestedFeeState = model.suggestedFeeState, + ), + ) + } + override fun dismiss() { model.dismiss() } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt index e80290de33..5de06f65d4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt @@ -15,8 +15,8 @@ import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal class WcSendTransactionContainerComponent( appComponentContext: AppComponentContext, params: WcTransactionModelParams, - feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, -) : WcCommonTransactionComponentDelegate(appComponentContext, feeSelectorBlockComponentFactory) { + private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : WcCommonTransactionComponentDelegate(appComponentContext) { private val model: WcSendTransactionModel = getOrCreateModel(params = params) @@ -45,7 +45,7 @@ internal class WcSendTransactionContainerComponent( return WcSendTransactionComponent( appComponentContext = appComponentContext, model = model, - feeSelectorBlockComponent = feeSelectorBlockComponent, + feeSelectorBlockComponentFactory = feeSelectorBlockComponentFactory, ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt index 4ca73d8c25..92550465c1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt @@ -5,14 +5,12 @@ import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel import com.tangem.features.walletconnect.transaction.ui.sign.WcSignTransactionModalBottomSheetContent internal class WcSignTransactionComponent( private val appComponentContext: AppComponentContext, private val model: WcSignTransactionModel, - private val feeSelectorBlockComponent: FeeSelectorBlockComponent, ) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { override fun dismiss() { @@ -26,7 +24,6 @@ internal class WcSignTransactionComponent( if (content != null) { WcSignTransactionModalBottomSheetContent( state = content!!.transaction, - feeSelectorBlockComponent = feeSelectorBlockComponent, onClickTransactionRequest = model::showTransactionRequest, onBack = router::pop, onDismiss = ::dismiss, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt index c9d6c4bdba..4f137d1841 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt @@ -5,7 +5,6 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.components.common.WcCommonTransactionComponentDelegate import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.common.getWcCommonScreen @@ -15,8 +14,7 @@ import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal class WcSignTransactionContainerComponent( appComponentContext: AppComponentContext, params: WcTransactionModelParams, - feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, -) : WcCommonTransactionComponentDelegate(appComponentContext, feeSelectorBlockComponentFactory) { +) : WcCommonTransactionComponentDelegate(appComponentContext) { private val model: WcSignTransactionModel = getOrCreateModel(params = params) @@ -45,7 +43,6 @@ internal class WcSignTransactionContainerComponent( return WcSignTransactionComponent( appComponentContext = appComponentContext, model = model, - feeSelectorBlockComponent = feeSelectorBlockComponent, ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt index d91c5cfc46..ca1ba812c4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt @@ -3,15 +3,13 @@ package com.tangem.features.walletconnect.transaction.converter import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.model.WcSolanaMethod +import com.tangem.domain.walletconnect.usecase.method.WcMutableFee import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestBlockUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.* import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionItemUM import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionUM import com.tangem.utils.converter.Converter @@ -39,6 +37,7 @@ internal class WcSendTransactionUMConverter @Inject constructor( onShowVerifiedAlert = value.actions.onShowVerifiedAlert, ), ), + feeState = constructFeeState(useCase = value.useCase), walletName = value.useCase.session.wallet.name, networkInfo = networkInfoUMConverter.convert(value.useCase.network), address = value.useCase.walletAddress.toShortAddressText(), @@ -81,6 +80,12 @@ internal class WcSendTransactionUMConverter @Inject constructor( else -> null } + private fun constructFeeState(useCase: WcTransactionUseCase): WcTransactionFeeState { + val mutableFee = useCase as? WcMutableFee ?: return WcTransactionFeeState.None + val dAppFee = mutableFee.dAppFee() + return if (dAppFee != null) WcTransactionFeeState.Success(dAppFee) else WcTransactionFeeState.Loading + } + data class Input( val useCase: WcTransactionUseCase, val signState: WcSignState<*>, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionFeeState.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionFeeState.kt new file mode 100644 index 0000000000..7c38bf7f48 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionFeeState.kt @@ -0,0 +1,9 @@ +package com.tangem.features.walletconnect.transaction.entity.common + +import com.tangem.blockchain.common.transaction.Fee + +internal sealed class WcTransactionFeeState { + object Loading : WcTransactionFeeState() + data class Success(val fee: Fee) : WcTransactionFeeState() + data object None : WcTransactionFeeState() +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt index 89a9e3c554..64c1ba82d3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt @@ -6,6 +6,7 @@ import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendRecei import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM internal data class WcSendTransactionUM( @@ -15,13 +16,13 @@ internal data class WcSendTransactionUM( ) : WcCommonTransactionUM internal data class WcSendTransactionItemUM( + val feeState: WcTransactionFeeState, val onDismiss: () -> Unit, val onSend: () -> Unit, val appInfo: WcTransactionAppInfoContentUM, val estimatedWalletChanges: WcSendReceiveTransactionCheckResultsUM?, val walletName: String, val networkInfo: WcNetworkInfoUM, - val networkFee: String? = null, val address: String? = null, val isLoading: Boolean = false, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 3e745c13f9..3f9908da90 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -7,14 +7,25 @@ import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.pushNew import com.domain.blockaid.models.transaction.ValidationResult import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce +import com.tangem.domain.models.network.Network +import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.usecase.method.* +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.walletconnect.connections.routing.WcInnerRoute import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.converter.WcCommonTransactionUMConverter @@ -29,6 +40,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import java.math.BigDecimal import javax.inject.Inject +import kotlin.properties.Delegates @Suppress("LongParameterList") @Stable @@ -41,6 +53,8 @@ internal class WcSendTransactionModel @Inject constructor( private val useCaseFactory: WcRequestUseCaseFactory, private val converter: WcCommonTransactionUMConverter, private val blockAidUiConverter: WcSendAndReceiveBlockAidUiConverter, + private val getFeeUseCase: GetFeeUseCase, + private val getNetworkCoinUseCase: GetNetworkCoinStatusUseCase, ) : Model(), WcCommonTransactionModel { private val params = paramsContainer.require() @@ -50,6 +64,10 @@ internal class WcSendTransactionModel @Inject constructor( val stackNavigation = StackNavigation() + internal var useCase: WcSignUseCase<*> by Delegates.notNull() + internal var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + internal var suggestedFeeState: FeeSelectorParams.SuggestedFeeState = FeeSelectorParams.SuggestedFeeState.None + private var signState: WcSignState<*> by Delegates.notNull() private var wcApproval: WcApproval? = null private var sign: () -> Unit = {} @@ -63,36 +81,65 @@ internal class WcSendTransactionModel @Inject constructor( when (useCase) { is WcListTransactionUseCase, is WcTransactionUseCase, - -> combine( - useCase.invoke(), - useCase.securityStatus, - ) { signState, securityCheck -> signState to securityCheck } - .distinctUntilChanged() - .collectLatest { - val (signState, securityCheck) = it - if (signingIsDone(signState, useCase)) return@collectLatest - val signModel: Any = signState.signModel - val isMutableFee = useCase is WcMutableFee - val dAppFee = if (isMutableFee) useCase.dAppFee() else null - val selectedFee = when (signModel) { - is TransactionData.Compiled -> signModel.fee - is TransactionData.Uncompiled -> signModel.fee - else -> null + -> { + this@WcSendTransactionModel.cryptoCurrencyStatus = + getCryptoCurrencyStatus(userWallet = useCase.wallet, network = useCase.network) + .onLeft { unknownMethodRunnable() } + .getOrNull() ?: return@launch + this@WcSendTransactionModel.useCase = useCase + (useCase as? WcMutableFee) + ?.dAppFee() + ?.let { dAppFee -> + suggestedFeeState = FeeSelectorParams.SuggestedFeeState.Suggestion( + title = stringReference(useCase.session.sdkModel.appMetaData.name), + fee = dAppFee, + ) } - val isDAppFeeSelected = dAppFee != null && dAppFee == selectedFee + combine( + useCase.invoke(), + useCase.securityStatus, + ) { signState, securityCheck -> signState to securityCheck } + .distinctUntilChanged() + .collectLatest { (signState, securityCheck) -> + if (signingIsDone(signState, useCase)) return@collectLatest - val isSecurityCheckContent = securityCheck is Lce.Content - var isApprovalMethod = isSecurityCheckContent && - securityCheck.content is BlockAidTransactionCheck.Result.Approval - wcApproval = useCase as? WcApproval - sign = { useCase.sign() } - buildUiState(securityCheck, useCase, signState) - } + this@WcSendTransactionModel.signState = signState + val isSecurityCheckContent = securityCheck is Lce.Content + var isApprovalMethod = isSecurityCheckContent && + securityCheck.content is BlockAidTransactionCheck.Result.Approval + wcApproval = useCase as? WcApproval + sign = { useCase.sign() } + buildUiState(securityCheck, useCase, signState) + } + } else -> unknownMethodRunnable() } } } + suspend fun loadFee(): Either { + val signModel = signState.signModel + val transactionData = signModel as? TransactionData.Uncompiled ?: error("TransactionData must be Uncompiled") + return getFeeUseCase.invoke( + userWallet = useCase.wallet, + network = useCase.network, + transactionData = transactionData, + ) + } + + private suspend fun getCryptoCurrencyStatus( + userWallet: UserWallet, + network: Network, + ): Either { + return getNetworkCoinUseCase.invokeSync( + userWalletId = userWallet.walletId, + networkId = network.id, + derivationPath = network.derivationPath, + isSingleWalletWithTokens = userWallet is UserWallet.Cold && + userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + ) + } + private suspend fun buildUiState( securityCheck: Lce, useCase: WcSignUseCase<*>, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt index 80054def8d..e0495da0ba 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt @@ -14,11 +14,13 @@ import com.tangem.core.ui.components.divider.DividerWithPadding import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState @Composable internal fun WcSendTransactionItems( walletName: String, networkInfo: WcNetworkInfoUM, + feeState: WcTransactionFeeState, feeSelectorBlockComponent: FeeSelectorBlockComponent, modifier: Modifier = Modifier, address: String? = null, @@ -52,6 +54,8 @@ internal fun WcSendTransactionItems( ) } DividerWithPadding(start = 40.dp, end = 12.dp) - feeSelectorBlockComponent.Content(Modifier) + if (feeState != WcTransactionFeeState.None) { + feeSelectorBlockComponent.Content(Modifier) + } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index 9e66c41bc8..0c56447574 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -34,6 +34,7 @@ import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimated import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionItemUM import com.tangem.features.walletconnect.transaction.ui.blockaid.TransactionCheckResultsItem import com.tangem.features.walletconnect.transaction.ui.common.WcSendTransactionItems @@ -42,7 +43,7 @@ import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequ import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem import kotlinx.collections.immutable.persistentListOf -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LongMethod") @Composable internal fun WcSendTransactionModalBottomSheet( state: WcSendTransactionItemUM, @@ -103,6 +104,7 @@ internal fun WcSendTransactionModalBottomSheet( WcSendTransactionItems( walletName = state.walletName, networkInfo = state.networkInfo, + feeState = state.feeState, feeSelectorBlockComponent = feeSelectorBlockComponent, address = state.address, ) @@ -192,7 +194,7 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide ), walletName = "Tangem 2.0", networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - networkFee = "~ 0.22 $", + feeState = WcTransactionFeeState.Loading, address = "0x345FF...34FA", ), ), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt index bdf2adb43a..80e3c3712f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt @@ -25,11 +25,9 @@ import com.tangem.core.ui.components.divider.DividerWithPadding import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.components.PreviewFeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM @@ -38,7 +36,6 @@ import com.tangem.features.walletconnect.transaction.ui.common.* @Composable internal fun WcSignTransactionModalBottomSheetContent( state: WcSignTransactionItemUM, - feeSelectorBlockComponent: FeeSelectorBlockComponent, onClickTransactionRequest: () -> Unit, onBack: () -> Unit, onDismiss: () -> Unit, @@ -87,7 +84,7 @@ internal fun WcSignTransactionModalBottomSheetContent( ) } Column(modifier = Modifier.padding(top = 16.dp)) { - WcSignTransactionItems(state, feeSelectorBlockComponent) + WcSignTransactionItems(state) } } }, @@ -104,11 +101,7 @@ internal fun WcSignTransactionModalBottomSheetContent( } @Composable -private fun WcSignTransactionItems( - state: WcSignTransactionItemUM, - feeSelectorBlockComponent: FeeSelectorBlockComponent, - modifier: Modifier = Modifier, -) { +private fun WcSignTransactionItems(state: WcSignTransactionItemUM, modifier: Modifier = Modifier) { Column( modifier = modifier .clip(RoundedCornerShape(14.dp)) @@ -138,7 +131,6 @@ private fun WcSignTransactionItems( ) } DividerWithPadding(start = 40.dp, end = 12.dp) - feeSelectorBlockComponent.Content(modifier = Modifier) } } @@ -168,7 +160,6 @@ private fun WcSignTransactionBottomSheetPreview( content = { WcSignTransactionModalBottomSheetContent( state = state, - feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(), onClickTransactionRequest = {}, onBack = {}, onDismiss = {}, From 03b382782ea3a2afa393d26408622ca8dd9c9f4d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Jun 2025 21:21:04 +0900 Subject: [PATCH 60/99] Updated on 2026-08-14 --- .../tangem/data/managetokens/DefaultCustomTokensRepository.kt | 1 + .../tangem/domain/transaction/usecase/OpenTrustlineUseCase.kt | 2 ++ gradle/tangem_dependencies.toml | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index b3250351dd..9064c8fb08 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -58,6 +58,7 @@ internal class DefaultCustomTokensRepository( Blockchain.Cardano, Blockchain.Sui, Blockchain.Stellar, + Blockchain.XRP, -> blockchain.validateContractAddress(contractAddress) else -> blockchain.validateAddress(contractAddress) } diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/OpenTrustlineUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/OpenTrustlineUseCase.kt index 1e3a9655a9..6f2fc407d4 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/OpenTrustlineUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/OpenTrustlineUseCase.kt @@ -32,6 +32,8 @@ class OpenTrustlineUseCase( is SimpleResult.Failure -> when (val error = result.error) { is BlockchainSdkError.Stellar.MinReserveRequired -> raise(OpenTrustlineError.NotEnoughCoin(error.amount, result.error.customMessage)) + is BlockchainSdkError.Xrp.MinReserveRequired -> + raise(OpenTrustlineError.NotEnoughCoin(error.amount, result.error.customMessage)) else -> raise(OpenTrustlineError.SomeError(result.error.customMessage)) } SimpleResult.Success -> Unit diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 1aa479a9c8..1be17abe62 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1087" +tangemBlockchainSdk = "develop-1088" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-484" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From d77c682e677339291d68e444bd426f49a0f7f8e4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Jun 2025 17:31:17 +0500 Subject: [PATCH 61/99] Updated on 2026-08-14 --- .../provider/ProviderChooseCrypto.kt | 117 +++++++++------- .../provider/entity/ProviderChooseUM.kt | 20 ++- .../SwapChooseProviderComponent.kt | 61 +++++++++ .../di/SwapChooseProviderModule.kt | 20 +++ .../SwapChooseProviderBottomSheetContent.kt | 16 +++ .../model/SwapChooseProviderModel.kt | 46 +++++++ .../SwapProviderListItemConverter.kt | 85 ++++++++++++ .../ui/SwapChooseProviderBottomSheet.kt | 124 +++++++++++++++++ .../ui/SwapChooseProviderContent.kt | 128 ++++++++++++++++++ .../SwapChooseProviderContentPreview.kt | 90 ++++++++++++ .../swap/v2/impl/common/entity/SwapQuoteUM.kt | 46 +++++++ 11 files changed, 700 insertions(+), 53 deletions(-) create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/di/SwapChooseProviderModule.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt index 1083b56912..4130c95004 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt @@ -54,7 +54,7 @@ fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit .fillMaxWidth() .padding(all = 12.dp), ) { - val (iconRef, titleRef, badgeRef, infoRef, labelRef) = createRefs() + val (iconRef, titleRef, extraRef, infoRef, labelRef) = createRefs() val titleEndRef = createStartBarrier(infoRef, labelRef) IconContent( @@ -72,19 +72,19 @@ fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit start.linkTo(iconRef.end, 12.dp) end.linkTo(titleEndRef, 4.dp) top.linkTo(parent.top) - bottom.linkTo(badgeRef.top) + bottom.linkTo(extraRef.top) width = Dimension.fillToConstraints }, ) - BadgeContent( + ExtraContent( providerChooseUM = providerChooseUM, - modifier = Modifier.constrainAs(badgeRef) { + modifier = Modifier.constrainAs(extraRef) { start.linkTo(iconRef.end, 12.dp) end.linkTo(labelRef.start, 4.dp) - top.linkTo(titleRef.bottom, 4.dp) + top.linkTo(titleRef.bottom) bottom.linkTo(parent.bottom) width = Dimension.fillToConstraints - visibility = if (providerChooseUM.badgeList.isEmpty()) { + visibility = if (providerChooseUM.extraUM is ProviderChooseUM.ExtraUM.Empty) { Visibility.Gone } else { Visibility.Visible @@ -165,16 +165,36 @@ private fun InfoContent(text: TextReference, modifier: Modifier = Modifier) { } @Composable -private fun BadgeContent(providerChooseUM: ProviderChooseUM, modifier: Modifier = Modifier) { +private fun ExtraContent(providerChooseUM: ProviderChooseUM, modifier: Modifier = Modifier) { Row( horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier, ) { - providerChooseUM.badgeList.fastForEach { badge -> - Badge( - iconRes = badge.iconRes, - text = badge.text, + when (val extraUM = providerChooseUM.extraUM) { + is ProviderChooseUM.ExtraUM.Action -> Text( + text = extraUM.text.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = 4.dp) + .clip(RoundedCornerShape(4.dp)) + .background(TangemTheme.colors.button.secondary) + .padding(horizontal = 6.dp, vertical = 1.dp), ) + is ProviderChooseUM.ExtraUM.Badges -> extraUM.badgeList.fastForEach { badge -> + Badge( + iconRes = badge.iconRes, + text = badge.text, + modifier = Modifier.padding(top = 4.dp), + ) + } + is ProviderChooseUM.ExtraUM.Error -> Text( + text = extraUM.text.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(top = 6.dp), + ) + else -> Unit } } } @@ -219,18 +239,20 @@ private class ProviderChooseCryptoPreviewProvider : PreviewParameterProvider, + val extraUM: ExtraUM, val labelUM: LabelUM?, ) { @Immutable @@ -36,4 +36,20 @@ data class ProviderChooseUM( val text: TextReference, ) : LabelUM() } + + @Immutable + sealed class ExtraUM { + data object Empty : ExtraUM() + data class Badges( + val badgeList: ImmutableList, + ) : ExtraUM() + + data class Action( + val text: TextReference, + ) : ExtraUM() + + data class Error( + val text: TextReference, + ) : ExtraUM() + } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt new file mode 100644 index 0000000000..744a8a3c91 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt @@ -0,0 +1,61 @@ +package com.tangem.features.swap.v2.impl.chooseprovider + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.swap.v2.impl.chooseprovider.model.SwapChooseProviderModel +import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderBottomSheet +import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import kotlinx.collections.immutable.ImmutableList + +internal class SwapChooseProviderComponent( + context: AppComponentContext, + private val params: Params, +) : ComposableBottomSheetComponent, AppComponentContext by context { + + private val model: SwapChooseProviderModel = getOrCreateModel(params = params) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state = model.uiState.collectAsStateWithLifecycle() + + val bottomSheetConfig = remember(key1 = this) { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + + SwapChooseProviderBottomSheet(config = bottomSheetConfig) { + SwapChooseProviderContent( + providerList = state.value.providerList, + onProviderClick = model::onProviderClick, + ) + } + } + + data class Params( + val cryptoCurrency: CryptoCurrency, + val selectedProvider: ExpressProvider, + val providers: ImmutableList, + val callback: ModelCallback, + val onDismiss: () -> Unit, + ) + + interface ModelCallback { + fun onProviderResult(quoteUM: SwapQuoteUM) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/di/SwapChooseProviderModule.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/di/SwapChooseProviderModule.kt new file mode 100644 index 0000000000..db602af2e5 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/di/SwapChooseProviderModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.swap.v2.impl.chooseprovider.model.SwapChooseProviderModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@InstallIn(ModelComponent::class) +@Module +internal interface SwapChooseProviderModule { + + @Binds + @IntoMap + @ClassKey(SwapChooseProviderModel::class) + fun provideSwapChooseProviderModel(impl: SwapChooseProviderModel): Model +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt new file mode 100644 index 0000000000..ad5eb47e18 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt @@ -0,0 +1,16 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.entity + +import com.tangem.core.ui.components.provider.entity.ProviderChooseUM +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import kotlinx.collections.immutable.ImmutableList + +internal data class SwapChooseProviderBottomSheetContent( + val providerList: ImmutableList, + val selectedProvider: ExpressProvider, +) + +internal data class SwapProviderListItem( + val providerUM: ProviderChooseUM, + val quote: SwapQuoteUM, +) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt new file mode 100644 index 0000000000..a4a4613f7f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt @@ -0,0 +1,46 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.model + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent +import com.tangem.features.swap.v2.impl.chooseprovider.model.converter.SwapProviderListItemConverter +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class SwapChooseProviderModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params: SwapChooseProviderComponent.Params = paramsContainer.require() + + val uiState: StateFlow + field: MutableStateFlow = MutableStateFlow(getInitialState()) + + private val swapProviderListItemConverter by lazy(LazyThreadSafetyMode.NONE) { + SwapProviderListItemConverter( + cryptoCurrency = params.cryptoCurrency, + selectedProvider = params.selectedProvider, + ) + } + + fun onProviderClick(quoteUM: SwapQuoteUM) { + params.callback.onProviderResult(quoteUM) + } + + private fun getInitialState(): SwapChooseProviderBottomSheetContent { + return SwapChooseProviderBottomSheetContent( + providerList = swapProviderListItemConverter.convertList(params.providers) + .filterNotNull() + .toPersistentList(), + selectedProvider = params.selectedProvider, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt new file mode 100644 index 0000000000..75234f816b --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt @@ -0,0 +1,85 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.model.converter + +import com.tangem.core.ui.components.audits.AuditLabelUM +import com.tangem.core.ui.components.provider.entity.ProviderChooseUM +import com.tangem.core.ui.components.provider.entity.ProviderChooseUM.LabelUM +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.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderListItem +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.converter.Converter + +internal class SwapProviderListItemConverter( + private val cryptoCurrency: CryptoCurrency, + private val selectedProvider: ExpressProvider, +) : Converter { + override fun convert(value: SwapQuoteUM): SwapProviderListItem? { + val provider = value.provider ?: return null + + return SwapProviderListItem( + providerUM = ProviderChooseUM( + title = stringReference(provider.name), + subtitle = stringReference(provider.type.typeName), + infoText = when (value) { + is SwapQuoteUM.Content -> value.quoteAmountValue + else -> TextReference.EMPTY + }, + iconUrl = provider.imageLarge, + isSelected = provider.providerId == selectedProvider.providerId, + extraUM = getExtras(value = value), + labelUM = getLabel(value = value), + ), + quote = value, + ) + } + + private fun getExtras(value: SwapQuoteUM) = when (value) { + is SwapQuoteUM.Allowance -> ProviderChooseUM.ExtraUM.Action( + text = resourceReference(R.string.express_provider_permission_needed), + ) + is SwapQuoteUM.Error -> { + ProviderChooseUM.ExtraUM.Error( + when (val error = value.expressError) { + is ExpressError.AmountError.TooSmallError -> resourceReference( + id = R.string.express_provider_min_amount, + formatArgs = wrappedList( + error.amount.format { crypto(cryptoCurrency) }, + ), + ) + is ExpressError.AmountError.NotEnoughAllowanceError, + is ExpressError.AmountError.TooBigError, + -> resourceReference( + id = R.string.express_provider_max_amount, + formatArgs = wrappedList( + error.amount.format { crypto(cryptoCurrency) }, + ), + ) + else -> TextReference.EMPTY + }, + ) + } + SwapQuoteUM.Empty, + SwapQuoteUM.Loading, + is SwapQuoteUM.Content, + -> ProviderChooseUM.ExtraUM.Empty + } + + private fun getLabel(value: SwapQuoteUM) = when (val diff = (value as? SwapQuoteUM.Content)?.diffPercent) { + SwapQuoteUM.Content.DifferencePercent.Best -> LabelUM.Info( + AuditLabelUM( + text = resourceReference(R.string.express_provider_best_rate), + type = AuditLabelUM.Type.Info, + ), + ) + is SwapQuoteUM.Content.DifferencePercent.Diff -> LabelUM.Text(diff.percent) + else -> null + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt new file mode 100644 index 0000000000..1b2b8d2e04 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt @@ -0,0 +1,124 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.provider.ProviderChooseCrypto +import com.tangem.core.ui.extensions.conditional +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderListItem +import com.tangem.features.swap.v2.impl.chooseprovider.ui.preview.SwapChooseProviderContentPreview +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun SwapChooseProviderBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { + TangemModalBottomSheet( + config = config, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.express_choose_provider), + endIconRes = R.drawable.ic_close_24, + onEndClick = config.onDismissRequest, + ) + }, + ) { + content() + } +} + +@Composable +internal fun SwapChooseProviderContent( + providerList: ImmutableList, + onProviderClick: (SwapQuoteUM) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(horizontal = 13.dp), + ) { + providerList.fastForEachIndexed { index, provider -> + ProviderChooseCrypto( + providerChooseUM = provider.providerUM, + onClick = { onProviderClick(provider.quote) }, + modifier = modifier + .conditional(index == 0) { padding(top = 6.dp) }, + ) + } + Icon( + painter = painterResource(id = R.drawable.ic_lightning_16), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.padding( + top = 16.dp, + start = 3.dp, + end = 3.dp, + ), + ) + Text( + text = stringResourceSafe(R.string.express_more_providers_soon), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.icon.informative, + modifier = Modifier + .padding( + top = 4.dp, + bottom = 24.dp, + start = 3.dp, + end = 3.dp, + ), + textAlign = TextAlign.Center, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun SwapChooseProviderContent_Preview( + @PreviewParameter(ProviderChooseCryptoPreviewProvider::class) params: SwapChooseProviderBottomSheetContent, +) { + TangemThemePreview { + SwapChooseProviderBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ), + ) { + SwapChooseProviderContent( + providerList = params.providerList, + onProviderClick = {}, + ) + } + } +} + +private class ProviderChooseCryptoPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SwapChooseProviderContentPreview.state, + ) +} +// endregion \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt new file mode 100644 index 0000000000..43c2d3c8f0 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt @@ -0,0 +1,128 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.ui + +import android.content.res.Configuration +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.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.features.swap.v2.impl.R + +@Composable +fun SwapChooseProviderContent(expressProvider: ExpressProvider?, onClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + onClick = onClick, + ), + ) { + HorizontalDivider( + thickness = 0.5.dp, + color = TangemTheme.colors.stroke.primary, + modifier = Modifier.padding(horizontal = 12.dp), + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding(12.dp), + ) { + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_exchange_horizontal_24), + ), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + Text( + text = stringResourceSafe(R.string.express_provider), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(start = 8.dp), + ) + SpacerWMax() + SubcomposeAsyncImage( + modifier = modifier + .size(20.dp) + .clip(RoundedCornerShape(4.dp)) + .background(TangemColorPalette.Light1), + model = ImageRequest.Builder(context = LocalContext.current) + .data(expressProvider?.imageLarge) + .crossfade(enable = true) + .allowHardware(false) + .build(), + contentDescription = null, + ) + Text( + text = expressProvider?.name.orEmpty(), // todo provider error + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(start = 6.dp), + ) + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_chevron_24), + ), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + modifier = Modifier.padding(start = 4.dp), + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun SwapChooseProviderContent_Preview() { + TangemThemePreview { + Box( + modifier = Modifier.background(TangemTheme.colors.background.tertiary), + ) { + SwapChooseProviderContent( + expressProvider = ExpressProvider( + providerId = "changelly", + rateTypes = listOf(ExpressRateType.Fixed), + name = "Changelly", + type = ExpressProviderType.CEX, + imageLarge = "", + termsOfUse = "", + privacyPolicy = "", + isRecommended = false, + slippage = null, + ), + onClick = {}, + ) + } + } +} +// endregion \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt new file mode 100644 index 0000000000..6558e68f96 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -0,0 +1,90 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.ui.preview + +import com.tangem.core.ui.components.audits.AuditLabelUM +import com.tangem.core.ui.components.provider.entity.ProviderChooseUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderListItem +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +internal object SwapChooseProviderContentPreview { + + private val provider1 = ExpressProvider( + providerId = "changenow", + rateTypes = listOf(ExpressRateType.Float), + name = "ChangeNow", + type = ExpressProviderType.CEX, + imageLarge = "", + termsOfUse = "", + privacyPolicy = "", + isRecommended = true, + slippage = BigDecimal.ZERO, + ) + private val provider2 = provider1.copy( + providerId = "changelly", + name = "Changelly", + ) + + private val quote1 = SwapQuoteUM.Content( + provider = provider1, + quoteAmount = "123".toBigDecimal(), + quoteAmountValue = stringReference("123"), + diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + ) + + private val quote2 = SwapQuoteUM.Content( + provider = provider2, + quoteAmount = "13.12".toBigDecimal(), + quoteAmountValue = stringReference("13.12"), + diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, + ) + + val state = SwapChooseProviderBottomSheetContent( + providerList = persistentListOf( + SwapProviderListItem( + providerUM = ProviderChooseUM( + title = stringReference(provider1.name), + subtitle = stringReference(provider1.type.typeName), + infoText = stringReference("1800 POL"), + iconUrl = "", + isSelected = true, + extraUM = ProviderChooseUM.ExtraUM.Empty, + labelUM = ProviderChooseUM.LabelUM.Info( + AuditLabelUM( + text = resourceReference(R.string.express_provider_best_rate), + type = AuditLabelUM.Type.Info, + ), + ), + ), + quote = quote1, + ), + SwapProviderListItem( + providerUM = ProviderChooseUM( + title = stringReference(provider2.name), + subtitle = stringReference(provider2.type.typeName), + infoText = stringReference("1800 POL"), + iconUrl = "", + isSelected = false, + extraUM = ProviderChooseUM.ExtraUM.Action( + text = resourceReference(R.string.express_provider_permission_needed), + ), + labelUM = ProviderChooseUM.LabelUM.Info( + AuditLabelUM( + text = resourceReference(R.string.express_provider_best_rate), + type = AuditLabelUM.Type.Info, + ), + ), + ), + quote = quote2, + ), + ), + selectedProvider = provider1, + ) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt new file mode 100644 index 0000000000..0b4abfd3c3 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt @@ -0,0 +1,46 @@ +package com.tangem.features.swap.v2.impl.common.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.express.models.ExpressProvider +import java.math.BigDecimal + +@Immutable +internal sealed class SwapQuoteUM { + + abstract val provider: ExpressProvider? + + data object Empty : SwapQuoteUM() { + override val provider = null + } + + data object Loading : SwapQuoteUM() { + override val provider = null + } + + data class Error( + override val provider: ExpressProvider, + val expressError: ExpressError, + ) : SwapQuoteUM() + + data class Allowance( + override val provider: ExpressProvider, + val allowanceContract: String, + ) : SwapQuoteUM() + + data class Content( + override val provider: ExpressProvider, + val quoteAmount: BigDecimal, + val quoteAmountValue: TextReference, + val diffPercent: DifferencePercent, + ) : SwapQuoteUM() { + sealed class DifferencePercent { + data object Empty : DifferencePercent() + data object Best : DifferencePercent() + data class Diff( + val percent: TextReference, + ) : DifferencePercent() + } + } +} \ No newline at end of file From 04c90d11db481415d2d34c0bc3a062709c9ef393 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Jun 2025 12:56:37 +0500 Subject: [PATCH 62/99] Updated on 2026-08-14 --- .../com/tangem/tap/routing/utils/ChildFactory.kt | 13 +++++++++++++ .../kotlin/com/tangem/common/routing/AppRoute.kt | 10 ++++++++++ .../com/tangem/domain/swap/models/SwapCurrencies.kt | 9 ++++++++- features/swap-v2/impl/build.gradle.kts | 1 + .../chooseprovider/model/SwapChooseProviderModel.kt | 6 +++--- 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 7008e3f4ec..4c5089eeb5 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -10,6 +10,7 @@ import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent @@ -78,6 +79,7 @@ internal class ChildFactory @Inject constructor( private val nftComponentFactory: NFTComponent.Factory, private val nftSendComponentFactory: NFTSendComponent.Factory, private val usedeskComponentFactory: UsedeskComponent.Factory, + private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, private val testerRouter: TesterRouter, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, ) { @@ -407,6 +409,17 @@ internal class ChildFactory @Inject constructor( componentFactory = usedeskComponentFactory, ) } + is AppRoute.ChooseManagedTokens -> { + createComponentChild( + context = context, + params = ChooseManagedTokensComponent.Params( + userWalletId = route.userWalletId, + initialCurrency = route.initialCurrency, + source = ChooseManagedTokensComponent.Source.valueOf(route.source.name), + ), + componentFactory = chooseManagedTokensComponentFactory, + ) + } } } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 4b7523311c..bfa14feda5 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -127,6 +127,16 @@ sealed class AppRoute(val path: String) : Route { } } + data class ChooseManagedTokens( + val userWalletId: UserWalletId, + val initialCurrency: CryptoCurrency, + val source: Source, + ) : AppRoute(path = "/$source/choose_managed_tokens/$userWalletId/${initialCurrency.id.value}") { + enum class Source { + SendViaSwap, + } + } + @Serializable data class WalletConnectSessions(val userWalletId: UserWalletId) : AppRoute(path = "/wallet_connect_sessions") diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt index ef559c44e9..78630c8eb6 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -12,7 +12,14 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus data class SwapCurrencies( val fromGroup: SwapCurrenciesGroup, val toGroup: SwapCurrenciesGroup, -) +) { + companion object { + val EMPTY = SwapCurrencies( + fromGroup = SwapCurrenciesGroup(emptyList(), emptyList(), false), + toGroup = SwapCurrenciesGroup(emptyList(), emptyList(), false), + ) + } +} /** * Return swap group depending on [swapDirection] diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index 456c959e50..817039a569 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(projects.domain.manageTokens) implementation(projects.domain.transaction.models) implementation(projects.domain.transaction) + implementation(projects.domain.legacy) /** Compose */ implementation(deps.compose.foundation) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt index a4a4613f7f..52fe541a8d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt @@ -21,9 +21,6 @@ internal class SwapChooseProviderModel @Inject constructor( private val params: SwapChooseProviderComponent.Params = paramsContainer.require() - val uiState: StateFlow - field: MutableStateFlow = MutableStateFlow(getInitialState()) - private val swapProviderListItemConverter by lazy(LazyThreadSafetyMode.NONE) { SwapProviderListItemConverter( cryptoCurrency = params.cryptoCurrency, @@ -31,6 +28,9 @@ internal class SwapChooseProviderModel @Inject constructor( ) } + val uiState: StateFlow + field: MutableStateFlow = MutableStateFlow(getInitialState()) + fun onProviderClick(quoteUM: SwapQuoteUM) { params.callback.onProviderResult(quoteUM) } From f6d9c354688d7c640f4e20d2618062ccba55787d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Jun 2025 12:57:08 +0500 Subject: [PATCH 63/99] Updated on 2026-08-14 --- .../v2/impl/amount/entity/SwapAmountUM.kt | 93 +++++++++++++++++++ .../amount/model/SwapAmountClickIntents.kt | 10 ++ .../ui/preview/SwapAmountClickIntentsStub.kt | 20 ++++ .../common/SwapNavigationModelCallback.kt | 7 ++ .../v2/impl/common/entity/NavigationUM.kt | 22 +++++ .../features/swap/v2/impl/swap/SwapRoute.kt | 23 +++++ 6 files changed, 175 insertions(+) create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapNavigationModelCallback.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/NavigationUM.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/swap/SwapRoute.kt diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt new file mode 100644 index 0000000000..532fc81270 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -0,0 +1,93 @@ +package com.tangem.features.swap.v2.impl.amount.entity + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class SwapAmountUM { + + abstract val isPrimaryButtonEnabled: Boolean + abstract val primaryAmount: SwapAmountFieldUM + abstract val secondaryAmount: SwapAmountFieldUM + abstract val selectedAmountType: SwapAmountType + + data object Empty : SwapAmountUM() { + override val isPrimaryButtonEnabled = false + override val selectedAmountType = SwapAmountType.From + override val primaryAmount = SwapAmountFieldUM.Empty(SwapAmountType.From) + override val secondaryAmount = SwapAmountFieldUM.Empty(SwapAmountType.To) + } + + data class Content( + override val isPrimaryButtonEnabled: Boolean, + + // two amount fields + override val primaryAmount: SwapAmountFieldUM, + override val secondaryAmount: SwapAmountFieldUM, + override val selectedAmountType: SwapAmountType, + val primaryCryptoCurrencyStatus: CryptoCurrencyStatus?, + val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?, + + // selected swap route + val swapDirection: SwapDirection, + val swapRateType: ExpressRateType, + + // swap models + val swapCurrencies: SwapCurrencies, + val swapQuotes: ImmutableList, + val selectedQuote: SwapQuoteUM, + + // extra data + val appCurrency: AppCurrency?, + ) : SwapAmountUM() +} + +@Immutable +sealed class SwapAmountFieldUM { + abstract val amountType: SwapAmountType + abstract val amountField: AmountState + + data class Empty( + override val amountType: SwapAmountType, + ) : SwapAmountFieldUM() { + override val amountField: AmountState = AmountState.Empty(isPrimaryButtonEnabled = false) + } + + data class Loading( + override val amountType: SwapAmountType, + ) : SwapAmountFieldUM() { + override val amountField: AmountState = AmountState.Empty(isPrimaryButtonEnabled = false) + } + + data class Content( + override val amountType: SwapAmountType, + override val amountField: AmountState, + val priceImpact: TextReference?, + val title: TextReference, + val subtitle: TextReference, + val subtitleEllipsis: TextEllipsis, + val isClickEnabled: Boolean, + ) : SwapAmountFieldUM() +} + +@Immutable +sealed class PriceImpactUM { + + data object Empty : PriceImpactUM() + + data class Value(val value: Float) : PriceImpactUM() +} + +enum class SwapAmountType { + From, + To, +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt new file mode 100644 index 0000000000..f9c92b77a1 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt @@ -0,0 +1,10 @@ +package com.tangem.features.swap.v2.impl.amount.model + +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType + +internal interface SwapAmountClickIntents : AmountScreenClickIntents { + + fun onExpandEditField(selectedAmountType: SwapAmountType) + fun onSelectTokenClick() +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt new file mode 100644 index 0000000000..98bcb53665 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt @@ -0,0 +1,20 @@ +package com.tangem.features.swap.v2.impl.amount.ui.preview + +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountClickIntents + +internal object SwapAmountClickIntentsStub : SwapAmountClickIntents { + override fun onExpandEditField(selectedAmountType: SwapAmountType) {} + + override fun onSelectTokenClick() {} + + override fun onAmountValueChange(value: String) {} + + override fun onAmountPasteTriggerDismiss() {} + + override fun onMaxValueClick() {} + + override fun onCurrencyChangeClick(isFiat: Boolean) {} + + override fun onAmountNext() {} +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapNavigationModelCallback.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapNavigationModelCallback.kt new file mode 100644 index 0000000000..2dfd5a4775 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapNavigationModelCallback.kt @@ -0,0 +1,7 @@ +package com.tangem.features.swap.v2.impl.common + +import com.tangem.features.swap.v2.impl.common.entity.NavigationUM + +internal interface SwapNavigationModelCallback { + fun onNavigationResult(navigationUM: NavigationUM) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/NavigationUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/NavigationUM.kt new file mode 100644 index 0000000000..b491576333 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/NavigationUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.swap.v2.impl.common.entity + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class NavigationUM { + data class Content( + val title: TextReference, + val subtitle: TextReference?, + @DrawableRes val backIconRes: Int, + val backIconClick: () -> Unit, + @DrawableRes val additionalIconRes: Int? = null, + val additionalIconClick: (() -> Unit)? = null, + val primaryButton: NavigationButton, + val secondaryPairButtonsUM: Pair? = null, + ) : NavigationUM() + + data object Empty : NavigationUM() +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/swap/SwapRoute.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/swap/SwapRoute.kt new file mode 100644 index 0000000000..2d7da88a11 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/swap/SwapRoute.kt @@ -0,0 +1,23 @@ +package com.tangem.features.swap.v2.impl.swap + +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +internal sealed class SwapRoute : Route { + + abstract val isEditMode: Boolean + + data object Empty : SwapRoute() { + override val isEditMode: Boolean = false + } + + @Serializable + data object Confirm : SwapRoute() { + override val isEditMode: Boolean = true + } + + @Serializable + data class Amount( + override val isEditMode: Boolean, + ) : SwapRoute() +} \ No newline at end of file From 16004fe75a0e9dfb720cfc001626034375683dad Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Jun 2025 12:57:29 +0500 Subject: [PATCH 64/99] Updated on 2026-08-14 --- .../impl/amount/SwapAmountComponentParams.kt | 48 ++ .../v2/impl/amount/model/SwapAmountModel.kt | 498 ++++++++++++++++++ .../impl/amount/model/SwapAmountQuoteUtils.kt | 60 +++ .../converter/SwapAmountFieldConverter.kt | 90 ++++ .../SwapAmountReadyStateConverter.kt | 58 ++ .../SwapAmountChangeCurrencyTransformer.kt | 34 ++ .../SwapAmountPasteTransformer.kt | 27 + .../SwapAmountSelectQuoteTransformer.kt | 60 +++ .../SwapAmountSetQuotesTransformer.kt | 172 ++++++ .../SwapAmountValueChangeTransformer.kt | 47 ++ .../SwapAmountValueMaxTransformer.kt | 46 ++ .../SwapQuoteLoadingStateTransformer.kt | 19 + .../v2/impl/amount/ui/SwapAmountContent.kt | 441 ++++++++++++++++ 13 files changed, 1600 insertions(+) create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountReadyStateConverter.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPasteTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt new file mode 100644 index 0000000000..d7a15f42f7 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt @@ -0,0 +1,48 @@ +package com.tangem.features.swap.v2.impl.amount + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.swap.SwapRoute +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +internal sealed class SwapAmountComponentParams { + + abstract val amountUM: SwapAmountUM + abstract val analyticsCategoryName: String + abstract val userWallet: UserWallet + abstract val appCurrency: AppCurrency + abstract val swapDirection: SwapDirection + abstract val isBalanceHidingFlow: StateFlow + + data class AmountParams( + override val amountUM: SwapAmountUM, + override val analyticsCategoryName: String, + override val userWallet: UserWallet, + override val isBalanceHidingFlow: StateFlow, + override val appCurrency: AppCurrency, + override val swapDirection: SwapDirection, + val primaryCryptoCurrency: CryptoCurrency, + val secondaryCryptoCurrency: CryptoCurrency?, + val callback: SwapAmountComponent.ModelCallback, + val currentRoute: Flow, + val onBackClick: () -> Unit, + val onNextClick: () -> Unit, + ) : SwapAmountComponentParams() + + data class AmountBlockParams( + override val amountUM: SwapAmountUM, + override val analyticsCategoryName: String, + override val userWallet: UserWallet, + override val isBalanceHidingFlow: StateFlow, + override val appCurrency: AppCurrency, + override val swapDirection: SwapDirection, + val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, + val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?, + val blockClickEnableFlow: StateFlow, + ) : SwapAmountComponentParams() +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt new file mode 100644 index 0000000000..9d188d6cb3 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -0,0 +1,498 @@ +package com.tangem.features.swap.v2.impl.amount.model + +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapQuoteModel +import com.tangem.domain.swap.usecase.GetSwapPairsUseCase +import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase +import com.tangem.domain.swap.usecase.SelectInitialPairUseCase +import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase +import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountReadyStateConverter +import com.tangem.features.swap.v2.impl.amount.model.transformers.* +import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent +import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.SwapChooseTokenNetworkListener +import com.tangem.features.swap.v2.impl.common.entity.NavigationUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject +import kotlin.properties.Delegates +import com.tangem.utils.transformer.update as transformerUpdate + +@Suppress("LargeClass", "LongParameterList") +@ModelScoped +internal class SwapAmountModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val getSwapPairsUseCase: GetSwapPairsUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase, + private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, + private val selectInitialPairUseCase: SelectInitialPairUseCase, + private val getSwapQuoteUseCase: GetSwapQuoteUseCase, + private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener, + private val getAllowanceUseCase: GetAllowanceUseCase, + private val appRouter: AppRouter, +) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback { + + private val params: SwapAmountComponentParams = paramsContainer.require() + private val swapDirection = params.swapDirection + private val appCurrency = params.appCurrency + private var userWallet = params.userWallet + + private var primaryCryptoCurrency: CryptoCurrency by Delegates.notNull() + private var secondaryCryptoCurrency: CryptoCurrency? = null + + private var primaryCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var secondaryCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + + private var primaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull() + private var secondaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull() + private var primaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull() + private var secondaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull() + + val uiState: StateFlow + field = MutableStateFlow(params.amountUM) + + init { + when (params) { + is SwapAmountComponentParams.AmountParams -> { + primaryCryptoCurrency = params.primaryCryptoCurrency + secondaryCryptoCurrency = params.secondaryCryptoCurrency + } + is SwapAmountComponentParams.AmountBlockParams -> { + primaryCryptoCurrency = params.primaryCryptoCurrencyStatus.currency + secondaryCryptoCurrency = params.secondaryCryptoCurrencyStatus?.currency + primaryCryptoCurrencyStatus = params.primaryCryptoCurrencyStatus + params.secondaryCryptoCurrencyStatus?.let { + secondaryCryptoCurrencyStatus = it + } + } + } + + initialState() + configAmountNavigation() + + observeChooseSelectToken() + // todo observe balance hiding flow + } + + fun updateState(amountUM: SwapAmountUM) { + uiState.update { amountUM } + } + + override fun onProviderResult(quoteUM: SwapQuoteUM) { + uiState.transformerUpdate( + SwapAmountSelectQuoteTransformer( + quoteUM = quoteUM, + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, + secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + ), + ) + } + + override fun onExpandEditField(selectedAmountType: SwapAmountType) { + uiState.update { amountUM -> + if (amountUM !is SwapAmountUM.Content) return + amountUM.copy( + selectedAmountType = selectedAmountType, + ) + } + } + + override fun onAmountValueChange(value: String) { + uiState.transformerUpdate( + SwapAmountValueChangeTransformer( + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + value = value, + ), + ) + loadQuotes() + } + + override fun onAmountPasteTriggerDismiss() { + uiState.transformerUpdate(SwapAmountPasteTransformer) + } + + override fun onMaxValueClick() { + uiState.transformerUpdate( + SwapAmountValueMaxTransformer( + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + ), + ) + loadQuotes() + } + + override fun onCurrencyChangeClick(isFiat: Boolean) { + uiState.transformerUpdate( + SwapAmountChangeCurrencyTransformer( + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + isFiatSelected = isFiat, + ), + ) + } + + override fun onAmountNext() { + saveResult() + } + + override fun onSelectTokenClick() { + appRouter.push( + AppRoute.ChooseManagedTokens( + userWalletId = userWallet.walletId, + initialCurrency = primaryCryptoCurrency, + source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, + ), + ) + } + + private fun observeChooseSelectToken() { + swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow + .onEach { currency -> + uiState.update { amountUM -> + if (amountUM is SwapAmountUM.Content) { + amountUM.copy( + isPrimaryButtonEnabled = false, + secondaryAmount = SwapAmountFieldUM.Loading( + amountType = SwapAmountType.To, + ), + ) + } else { + amountUM + } + } + initPairs(primaryCryptoCurrency, currency) + } + .launchIn(modelScope) + } + + private fun initialState() { + if (uiState.value is SwapAmountUM.Empty) { + initPairs( + primaryCryptoCurrency = primaryCryptoCurrency, + secondaryCryptoCurrency = secondaryCryptoCurrency, + ) + uiState.update { + SwapAmountUM.Content( + isPrimaryButtonEnabled = it.isPrimaryButtonEnabled, + primaryAmount = it.primaryAmount, + secondaryAmount = it.secondaryAmount, + swapDirection = swapDirection, + swapCurrencies = SwapCurrencies.EMPTY, + appCurrency = appCurrency, + swapQuotes = persistentListOf(), + swapRateType = ExpressRateType.Float, + selectedAmountType = SwapAmountType.From, + selectedQuote = SwapQuoteUM.Empty, + primaryCryptoCurrencyStatus = null, + secondaryCryptoCurrencyStatus = null, + ) + } + } + } + + private fun initPairs(primaryCryptoCurrency: CryptoCurrency, secondaryCryptoCurrency: CryptoCurrency?) { + modelScope.launch { + val cryptoCurrencyStatusList = getMultiCryptoCurrencyStatusUseCase + .invokeMultiWalletSync(userWallet.walletId) + .getOrElse { emptyList() } + + val primaryStatus = cryptoCurrencyStatusList.firstOrNull { + it.currency.id == primaryCryptoCurrency.id + } + if (primaryStatus == null) { + Timber.e("Failed to get crypto currency status") + // todo error + return@launch + } + + val cryptoCurrencyStatusListExceptPrimary = cryptoCurrencyStatusList.filter { + val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount + val notCustomTokenFilter = !it.currency.isCustom + statusFilter && notCustomTokenFilter + } + + getSwapPairsUseCase( + userWallet = userWallet, + initialCurrency = primaryCryptoCurrency, + cryptoCurrencyStatusList = cryptoCurrencyStatusListExceptPrimary, + ).fold( + ifRight = { swapCurrencies -> + val secondaryStatus = selectInitialPairUseCase( + primaryCryptoCurrency = primaryCryptoCurrency, + secondaryCryptoCurrency = secondaryCryptoCurrency, + userWallet = userWallet, + swapCurrencies = swapCurrencies, + swapDirection = params.swapDirection, + ) + + if (secondaryStatus != null) { + initCurrencies(primaryStatus, secondaryStatus) + uiState.update { + SwapAmountReadyStateConverter( + swapCurrencies = swapCurrencies, + userWallet = userWallet, + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + appCurrency = appCurrency, + swapDirection = swapDirection, + clickIntents = this@SwapAmountModel, + isBalanceHidden = params.isBalanceHidingFlow.value, + ).convert(Unit) + } + } else { + // todo not available currency to swap + } + }, + ifLeft = { + // todo error + }, + ) + } + } + + private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus) { + primaryCryptoCurrencyStatus = primaryStatus + secondaryCryptoCurrencyStatus = secondaryStatus + + primaryMinimumAmountBoundary = EnterAmountBoundary( + amount = getMinimumTransactionAmountSyncUseCase + .invoke( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = primaryStatus, + ).getOrNull().orZero(), + fiatRate = primaryStatus.value.fiatRate, + fiatAmount = primaryStatus.value.fiatAmount, + ) + secondaryMinimumAmountBoundary = EnterAmountBoundary( + amount = getMinimumTransactionAmountSyncUseCase + .invoke( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = secondaryStatus, + ).getOrNull().orZero(), + fiatRate = secondaryStatus.value.fiatRate, + fiatAmount = secondaryStatus.value.fiatAmount, + ) + primaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(primaryStatus) + secondaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(secondaryStatus) + } + + private fun loadQuotes() { + val state = uiState.value as? SwapAmountUM.Content ?: return + + val (fromCryptoCurrency, toCryptoCurrency) = when (state.swapDirection) { + SwapDirection.Direct -> primaryCryptoCurrencyStatus.currency to secondaryCryptoCurrencyStatus.currency + SwapDirection.Reverse -> secondaryCryptoCurrencyStatus.currency to primaryCryptoCurrencyStatus.currency + } + + val fromAmount = when (state.swapDirection) { + SwapDirection.Direct -> state.primaryAmount.amountField + SwapDirection.Reverse -> state.secondaryAmount.amountField + } as? AmountState.Data + + if (fromAmount?.amountTextField?.isError == true) return + + val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value ?: return + + val swapGroups = when (state.swapDirection) { + SwapDirection.Direct -> state.swapCurrencies.toGroup.available + SwapDirection.Reverse -> state.swapCurrencies.fromGroup.available + } + + uiState.transformerUpdate(SwapQuoteLoadingStateTransformer) + + modelScope.launch { + val quotes = swapGroups.firstOrNull { + it.currencyStatus.currency.id == toCryptoCurrency.id + }?.providers?.map { provider -> + async { + getSwapQuoteUseCase( + userWallet = userWallet, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + fromAmount = fromAmountValue, + provider = provider, + ).fold( + ifLeft = { error -> + SwapQuoteUM.Error( + provider = provider, + expressError = error, + ) + }, + ifRight = { quote: SwapQuoteModel -> + convertToSwapProviderUM( + quote = quote, + provider = provider, + differencePercent = DifferencePercent.Empty, + swapDirection = swapDirection, + ) + }, + ) + } + }?.awaitAll().orEmpty() + + uiState.transformerUpdate( + SwapAmountSetQuotesTransformer( + quotes = quotes, + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, + secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + ), + ) + } + } + + private suspend fun convertToSwapProviderUM( + quote: SwapQuoteModel, + provider: ExpressProvider, + differencePercent: DifferencePercent, + swapDirection: SwapDirection, + ): SwapQuoteUM { + // todo swap allowance + val allowanceContract = quote.allowanceContract + return if (allowanceContract != null) { + val allowance = getAllowanceUseCase( + userWalletId = userWallet.walletId, + cryptoCurrency = primaryCryptoCurrencyStatus.currency, + spenderAddress = allowanceContract, + ).getOrNull().orZero() + val isApprovalNeeded = allowance < primaryCryptoCurrencyStatus.value.amount.orZero() + + if (isApprovalNeeded) { + SwapQuoteUM.Allowance( + provider = provider, + allowanceContract = allowanceContract, + ) + } else { + SwapQuoteUM.Content( + provider = provider, + quoteAmount = quote.toTokenAmount, + diffPercent = differencePercent, + quoteAmountValue = stringReference( + quote.toTokenAmount.format { + crypto( + when (swapDirection) { + SwapDirection.Direct -> secondaryCryptoCurrencyStatus.currency + SwapDirection.Reverse -> primaryCryptoCurrencyStatus.currency + }, + ) + }, + ), + ) + } + } else { + SwapQuoteUM.Content( + provider = provider, + quoteAmount = quote.toTokenAmount, + diffPercent = differencePercent, + quoteAmountValue = stringReference( + quote.toTokenAmount.format { + crypto( + when (swapDirection) { + SwapDirection.Direct -> secondaryCryptoCurrencyStatus.currency + SwapDirection.Reverse -> primaryCryptoCurrencyStatus.currency + }, + ) + }, + ), + ) + } + } + + private fun saveResult() { + val params = params as? SwapAmountComponentParams.AmountParams ?: return + params.callback.onAmountResult(uiState.value) + } + + private fun configAmountNavigation() { + val params = params as? SwapAmountComponentParams.AmountParams ?: return + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).onEach { (state, route) -> + params.callback.onNavigationResult( + NavigationUM.Content( + title = resourceReference(R.string.common_swap), + subtitle = null, + backIconRes = if (route.isEditMode) { + R.drawable.ic_back_24 + } else { + R.drawable.ic_close_24 + }, + backIconClick = { + // if (!route.isEditMode) { + // todo analytics + // analyticsEventHandler.send( + // CommonSendAnalyticEvents.CloseButtonClicked( + // categoryName = params.analyticsCategoryName, + // source = SendScreenSource.Address, + // isFromSummary = false, + // isValid = state.isPrimaryButtonEnabled, + // ), + // ) + // } + params.onBackClick() + }, + primaryButton = NavigationButton( + textReference = if (route.isEditMode) { + resourceReference(R.string.common_continue) + } else { + resourceReference(R.string.common_next) + }, + isEnabled = state.isPrimaryButtonEnabled, + onClick = { + saveResult() + params.onNextClick() + }, + ), + ), + ) + }.launchIn(modelScope) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt new file mode 100644 index 0000000000..bef7ed2108 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -0,0 +1,60 @@ +package com.tangem.features.swap.v2.impl.amount.model + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.utils.extensions.isZero +import java.math.BigDecimal +import java.math.RoundingMode + +internal object SwapAmountQuoteUtils { + + fun calculatePriceImpact( + fromTokenAmount: BigDecimal, + toTokenAmount: BigDecimal, + swapDirection: SwapDirection, + primaryCryptoCurrencyStatus: CryptoCurrencyStatus, + secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + ): TextReference? { + val (fromRate, toRate) = if (swapDirection == SwapDirection.Direct) { + primaryCryptoCurrencyStatus.value.fiatRate to secondaryCryptoCurrencyStatus.value.fiatRate + } else { + secondaryCryptoCurrencyStatus.value.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate + } + + val fromTokenFiatValue = fromTokenAmount.multiply(fromRate) + val toTokenFiatValue = toTokenAmount.multiply(toRate) + + // Check for zero division + if (fromTokenFiatValue.isZero() || toTokenFiatValue.isZero()) return null + + val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP) + + return stringReference("$(-${value.format { percent(withoutSign = false) }})").takeIf { + value > 0.1.toBigDecimal() + } + } + + fun SwapAmountUM.updateAmount( + onPrimaryAmount: SwapAmountFieldUM.Content.() -> SwapAmountFieldUM, + onSecondaryAmount: SwapAmountFieldUM.Content.() -> SwapAmountFieldUM, + ): SwapAmountUM { + if (this !is SwapAmountUM.Content) return this + + return if ( + selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct + ) { + val amountFieldUM = primaryAmount as? SwapAmountFieldUM.Content ?: return this + copy(primaryAmount = amountFieldUM.onPrimaryAmount()) + } else { + val amountFieldUM = secondaryAmount as? SwapAmountFieldUM.Content ?: return this + copy(secondaryAmount = amountFieldUM.onSecondaryAmount()) + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt new file mode 100644 index 0000000000..0b70b34750 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -0,0 +1,90 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.amountScreen.converters.AmountStateConverterV2 +import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter +import com.tangem.common.ui.amountScreen.models.AmountParameters +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType + +internal class SwapAmountFieldConverter( + private val swapDirection: SwapDirection, + private val isBalanceHidden: Boolean, + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: AmountScreenClickIntents, +) { + + private val iconStateConverter = CryptoCurrencyToIconStateConverter() + private val maxEnterAmountConverter = MaxEnterAmountConverter() + + fun convert(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus): SwapAmountFieldUM { + return SwapAmountFieldUM.Content( + amountType = selectedType, + title = stringReference(cryptoCurrencyStatus.currency.name), + subtitle = getSubtitle(selectedType = selectedType, cryptoCurrencyStatus = cryptoCurrencyStatus), + subtitleEllipsis = getSubtitleEllipsis( + selectedType = selectedType, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ), + priceImpact = null, + isClickEnabled = selectedType.isViewingField(), + amountField = AmountStateConverterV2( + clickIntents = clickIntents, + appCurrency = appCurrency, + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus), + iconStateConverter = iconStateConverter, + isRedesignEnabled = true, + ).convert( + AmountParameters( + title = combinedReference( + resourceReference(R.string.send_from_wallet_android), + stringReference(" "), + stringReference(userWallet.name), + ), + value = "", + ), + ), + ) + } + + private fun getSubtitle(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when { + selectedType.isEnteringField() -> resourceReference( + R.string.common_balance, + wrappedList( + cryptoCurrencyStatus.value.amount.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + }, + ), + ).orMaskWithStars(isBalanceHidden) + selectedType.isViewingField() -> resourceReference(R.string.send_with_swap_recipient_amount_text) + else -> TextReference.Companion.EMPTY + } + + private fun getSubtitleEllipsis(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when { + selectedType.isEnteringField() -> TextEllipsis.OffsetEnd(cryptoCurrencyStatus.currency.symbol.length) + selectedType.isViewingField() -> TextEllipsis.End + else -> TextEllipsis.End + } + + private fun SwapAmountType.isEnteringField(): Boolean { + return this == SwapAmountType.From && swapDirection == SwapDirection.Direct || + this == SwapAmountType.To && swapDirection == SwapDirection.Reverse + } + + private fun SwapAmountType.isViewingField(): Boolean { + return this == SwapAmountType.From && swapDirection == SwapDirection.Reverse || + this == SwapAmountType.To && swapDirection == SwapDirection.Direct + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountReadyStateConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountReadyStateConverter.kt new file mode 100644 index 0000000000..7de7bab116 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountReadyStateConverter.kt @@ -0,0 +1,58 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf + +@Suppress("LongParameterList") +internal class SwapAmountReadyStateConverter( + private val userWallet: UserWallet, + private val swapCurrencies: SwapCurrencies, + private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val appCurrency: AppCurrency, + private val clickIntents: AmountScreenClickIntents, + private val swapDirection: SwapDirection, + private val isBalanceHidden: Boolean, +) : Converter { + + private val amountFieldConverter = SwapAmountFieldConverter( + swapDirection = swapDirection, + isBalanceHidden = isBalanceHidden, + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ) + + override fun convert(value: Unit): SwapAmountUM { + return SwapAmountUM.Content( + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + primaryAmount = amountFieldConverter.convert( + selectedType = SwapAmountType.From, + cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + ), + secondaryAmount = amountFieldConverter.convert( + selectedType = SwapAmountType.To, + cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + ), + swapCurrencies = swapCurrencies, + swapDirection = swapDirection, + swapQuotes = persistentListOf(), + selectedQuote = SwapQuoteUM.Empty, + selectedAmountType = SwapAmountType.From, + appCurrency = appCurrency, + swapRateType = ExpressRateType.Float, + isPrimaryButtonEnabled = false, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt new file mode 100644 index 0000000000..05c0a9d16b --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt @@ -0,0 +1,34 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount +import com.tangem.utils.transformer.Transformer + +internal class SwapAmountChangeCurrencyTransformer( + private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val isFiatSelected: Boolean, +) : Transformer { + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + return prevState.updateAmount( + onPrimaryAmount = { + copy( + amountField = AmountCurrencyTransformer( + cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + value = isFiatSelected, + ).transform(prevState.primaryAmount.amountField), + ) + }, + onSecondaryAmount = { + copy( + amountField = AmountCurrencyTransformer( + cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + value = isFiatSelected, + ).transform(prevState.secondaryAmount.amountField), + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPasteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPasteTransformer.kt new file mode 100644 index 0000000000..2b6eb14c59 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPasteTransformer.kt @@ -0,0 +1,27 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.converters.AmountPastedTriggerDismissTransformer +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount +import com.tangem.utils.transformer.Transformer + +internal object SwapAmountPasteTransformer : Transformer { + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + return prevState.updateAmount( + onPrimaryAmount = { + copy( + amountField = AmountPastedTriggerDismissTransformer.transform( + prevState = prevState.primaryAmount.amountField, + ), + ) + }, + onSecondaryAmount = { + copy( + amountField = AmountPastedTriggerDismissTransformer.transform( + prevState = prevState.secondaryAmount.amountField, + ), + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt new file mode 100644 index 0000000000..0c11067bfc --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -0,0 +1,60 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.extensions.orZero +import com.tangem.utils.transformer.Transformer + +internal class SwapAmountSelectQuoteTransformer( + private val quoteUM: SwapQuoteUM, + private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val secondaryMaximumAmountBoundary: EnterAmountBoundary, + private val secondaryMinimumAmountBoundary: EnterAmountBoundary, +) : Transformer { + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState + + return prevState.copy( + isPrimaryButtonEnabled = true, + selectedQuote = quoteUM, + secondaryAmount = if ( + prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct + ) { + val secondaryAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content + val fromAmount = (prevState.primaryAmount.amountField as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value.orZero() + val toAmount = (quoteUM as? SwapQuoteUM.Content)?.quoteAmount + val priceImpact = calculatePriceImpact( + swapDirection = prevState.swapDirection, + fromTokenAmount = fromAmount, + toTokenAmount = toAmount.orZero(), + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + ) + + secondaryAmountField?.copy( + priceImpact = priceImpact, + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + maxEnterAmount = secondaryMaximumAmountBoundary, + minimumTransactionAmount = secondaryMinimumAmountBoundary, + value = toAmount?.parseBigDecimal(secondaryCryptoCurrencyStatus.currency.decimals) + .orEmpty(), + ).transform(secondaryAmountField.amountField), + ) ?: prevState.secondaryAmount + } else { + prevState.secondaryAmount + }, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt new file mode 100644 index 0000000000..ac39b1417e --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -0,0 +1,172 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +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.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent +import com.tangem.utils.StringsSigns +import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.orZero +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class SwapAmountSetQuotesTransformer( + private val quotes: List, + private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val secondaryMaximumAmountBoundary: EnterAmountBoundary, + private val secondaryMinimumAmountBoundary: EnterAmountBoundary, +) : Transformer { + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState + + val fromAmount = when (prevState.swapDirection) { + SwapDirection.Direct -> prevState.primaryAmount.amountField + SwapDirection.Reverse -> prevState.secondaryAmount.amountField + } as? AmountState.Data + val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value.orZero() + + val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) + val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty + + return prevState.copy( + isPrimaryButtonEnabled = bestQuote is SwapQuoteUM.Content, + swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote), + selectedQuote = bestQuote, + primaryAmount = if ( + prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct + ) { + val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content + val amountField = swapAmountField?.amountField as? AmountState.Data + + val amountError = (bestQuote as? SwapQuoteUM.Error)?.expressError.getAmountError() + + if (amountField?.amountTextField?.isError == true) { + prevState.primaryAmount + } else { + swapAmountField?.copy( + amountField = amountField?.copy( + amountTextField = amountField.amountTextField.copy( + error = amountError ?: TextReference.EMPTY, + isError = amountError != null, + ), + ) ?: swapAmountField.amountField, + ) ?: prevState.primaryAmount + } + } else { + prevState.primaryAmount + }, + secondaryAmount = if ( + prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct + ) { + val amountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content + val toAmount = (bestQuote as? SwapQuoteUM.Content)?.quoteAmount + + val priceImpact = calculatePriceImpact( + swapDirection = prevState.swapDirection, + fromTokenAmount = fromAmountValue, + toTokenAmount = toAmount.orZero(), + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + ) + + amountField?.copy( + priceImpact = priceImpact, + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + maxEnterAmount = secondaryMaximumAmountBoundary, + minimumTransactionAmount = secondaryMinimumAmountBoundary, + value = toAmount?.parseBigDecimal(secondaryCryptoCurrencyStatus.currency.decimals).orEmpty(), + ).transform(amountField.amountField), + ) ?: prevState.secondaryAmount + } else { + prevState.secondaryAmount + }, + ) + } + + private fun ExpressError?.getAmountError(): TextReference? = when (this) { + is ExpressError.AmountError.TooSmallError -> resourceReference( + R.string.express_provider_min_amount, + wrappedList(amount.format { crypto(cryptoCurrency = primaryCryptoCurrencyStatus.currency) }), + ) + is ExpressError.AmountError.TooBigError -> resourceReference( + R.string.express_provider_max_amount, + wrappedList(amount.format { crypto(cryptoCurrency = primaryCryptoCurrencyStatus.currency) }), + ) + else -> null + } + + private fun getQuotesWithDiff(sortedQuotes: List, bestQuote: SwapQuoteUM): ImmutableList { + return sortedQuotes.sortedWith(SwapQuotesComparator) + .map { quote -> + if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { + if (quote.provider.providerId == bestQuote.provider.providerId) { + quote.copy(diffPercent = DifferencePercent.Best) + } else { + // current / selected - 1 + val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE + quote.copy( + diffPercent = DifferencePercent.Diff( + percent = stringReference( + if (percent.isPositive()) { + "${StringsSigns.PLUS}${percent.format { percent() }}" + } else { + "${StringsSigns.DASH_SIGN}${percent.format { percent() }}" + }, + ), + ), + ) + } + } else { + quote + } + }.toPersistentList() + } + + private fun findBestQuote(quotes: List): SwapQuoteUM? { + return quotes + .sortedWith(SwapQuotesComparator) + .firstOrNull() + } + + private object SwapQuotesComparator : Comparator { + override fun compare(p0: SwapQuoteUM?, p1: SwapQuoteUM?): Int { + return when { + p0 is SwapQuoteUM.Content && p1 !is SwapQuoteUM.Content -> -1 + p0 !is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> 1 + p0 is SwapQuoteUM.Error && p1 is SwapQuoteUM.Error -> compareErrorQuote(p0 = p0, p1 = p1) + p0 is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> p1.quoteAmount.compareTo(p0.quoteAmount) + else -> 0 + } + } + + private fun compareErrorQuote(p0: SwapQuoteUM.Error, p1: SwapQuoteUM.Error): Int = when { + p0.expressError is ExpressError.AmountError && p1.expressError !is ExpressError.AmountError -> -1 + p0.expressError !is ExpressError.AmountError && p1.expressError is ExpressError.AmountError -> 1 + p0.expressError is ExpressError.AmountError && p1.expressError is ExpressError.AmountError -> { + p0.expressError.amount.compareTo(p1.expressError.amount) + } + else -> 0 + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt new file mode 100644 index 0000000000..59687934ce --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt @@ -0,0 +1,47 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount +import com.tangem.utils.transformer.Transformer + +@Suppress("LongParameterList") +internal class SwapAmountValueChangeTransformer( + private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val primaryMaximumAmountBoundary: EnterAmountBoundary, + private val secondaryMaximumAmountBoundary: EnterAmountBoundary, + private val primaryMinimumAmountBoundary: EnterAmountBoundary, + private val secondaryMinimumAmountBoundary: EnterAmountBoundary, + private val value: String, +) : Transformer { + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState + + return prevState + .updateAmount( + onPrimaryAmount = { + copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = value, + ).transform(prevState.primaryAmount.amountField), + ) + }, + onSecondaryAmount = { + copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + maxEnterAmount = secondaryMaximumAmountBoundary, + minimumTransactionAmount = secondaryMinimumAmountBoundary, + value = value, + ).transform(prevState.secondaryAmount.amountField), + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt new file mode 100644 index 0000000000..8392215305 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt @@ -0,0 +1,46 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.transformer.Transformer + +internal class SwapAmountValueMaxTransformer( + private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val primaryMaximumAmountBoundary: EnterAmountBoundary, + private val secondaryMaximumAmountBoundary: EnterAmountBoundary, + private val primaryMinimumAmountBoundary: EnterAmountBoundary, + private val secondaryMinimumAmountBoundary: EnterAmountBoundary, +) : Transformer { + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState + + return prevState + .copy(selectedQuote = SwapQuoteUM.Loading) + .updateAmount( + onPrimaryAmount = { + copy( + amountField = AmountFieldSetMaxAmountTransformer( + cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + maxAmount = primaryMaximumAmountBoundary, + minAmount = primaryMinimumAmountBoundary, + ).transform(prevState.primaryAmount.amountField), + ) + }, + onSecondaryAmount = { + copy( + amountField = AmountFieldSetMaxAmountTransformer( + cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + maxAmount = secondaryMaximumAmountBoundary, + minAmount = secondaryMinimumAmountBoundary, + ).transform(prevState.secondaryAmount.amountField), + ) + }, + + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt new file mode 100644 index 0000000000..12bcce175c --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.transformer.Transformer + +internal object SwapQuoteLoadingStateTransformer : Transformer { + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState + + return prevState.copy( + selectedQuote = if (prevState.selectedQuote is SwapQuoteUM.Empty) { + SwapQuoteUM.Loading + } else { + prevState.selectedQuote + }, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt new file mode 100644 index 0000000000..671ce82c48 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt @@ -0,0 +1,441 @@ +package com.tangem.features.swap.v2.impl.amount.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +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.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.ui.AmountFieldV2 +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.conditionalCompose +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountClickIntents +import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountClickIntentsStub +import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM + +@Composable +internal fun SwapAmountContent( + amountUM: SwapAmountUM, + clickIntents: SwapAmountClickIntents, + modifier: Modifier = Modifier, +) { + ConstraintLayout( + modifier = modifier, + ) { + val (amountFromRef, amountToRef, middleButtonRef) = createRefs() + SwapAmountBlock( + amountUM = amountUM, + amountFieldUM = amountUM.primaryAmount, + clickIntents = clickIntents, + modifier = Modifier.constrainAs(amountFromRef) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) + SwapAmountBlock( + amountUM = amountUM, + amountFieldUM = amountUM.secondaryAmount, + clickIntents = clickIntents, + modifier = Modifier.constrainAs(amountToRef) { + top.linkTo(amountFromRef.bottom, 8.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) + SwapAmountBlockSeparator( + modifier = Modifier + .constrainAs(middleButtonRef) { + top.linkTo(amountFromRef.bottom) + bottom.linkTo(amountToRef.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) + } +} + +@Composable +private fun SwapAmountBlockSeparator(modifier: Modifier = Modifier) { + // todo add swap type like [Swap, SendViaSwap, SendIncognito] + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .heightIn(max = 28.dp) + .clip(RoundedCornerShape(32.dp)) + .background(TangemTheme.colors.background.secondary) + .padding( + vertical = 6.dp, + horizontal = 12.dp, + ), + ) { + Text( + text = "Convert", + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + VerticalDivider( + thickness = 1.dp, + color = TangemTheme.colors.icon.inactive, + ) + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_close_24), + ), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + } +} + +@Composable +private fun SwapAmountBlock( + amountUM: SwapAmountUM, + amountFieldUM: SwapAmountFieldUM, + clickIntents: SwapAmountClickIntents, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .padding(horizontal = 16.dp) + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors.background.action), + ) { + AnimatedVisibility(amountUM.selectedAmountType == amountFieldUM.amountType) { + Box { + SwapAmountEditBlock( + amountFieldUM = amountFieldUM, + modifier = Modifier, + onValueChange = clickIntents::onAmountValueChange, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, + onCurrencyChange = clickIntents::onCurrencyChangeClick, + ) + HorizontalDivider( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 16.dp), + thickness = 0.5.dp, + color = TangemTheme.colors.stroke.primary, + ) + } + } + SwapAmountInfo( + amountUM = amountUM, + amountFieldUM = amountFieldUM, + onExpandEditField = clickIntents::onExpandEditField, + onSelectTokenClick = clickIntents::onSelectTokenClick, + onMaxAmountClick = clickIntents::onMaxValueClick, + ) + } +} + +@Composable +private fun SwapAmountEditBlock( + amountFieldUM: SwapAmountFieldUM, + onValueChange: (String) -> Unit, + onValuePastedTriggerDismiss: () -> Unit, + onCurrencyChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier + .padding(vertical = 48.dp), + ) { + if (amountFieldUM.amountField !is AmountState.Data) { + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(60.dp), + ) + } else { + Text( + text = (amountFieldUM.amountField as AmountState.Data).title.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + AmountFieldV2( + amountUM = amountFieldUM.amountField, + onValueChange = onValueChange, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, + onCurrencyChange = onCurrencyChange, + modifier = Modifier, + ) + } +} + +@Composable +private fun SwapAmountInfo( + amountUM: SwapAmountUM, + amountFieldUM: SwapAmountFieldUM, + onExpandEditField: (SwapAmountType) -> Unit, + onMaxAmountClick: () -> Unit, + onSelectTokenClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val tokenIconState = (amountFieldUM.amountField as? AmountState.Data)?.tokenIconState ?: CurrencyIconState.Loading + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + enabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true, + onClick = { + if ((amountUM as? SwapAmountUM.Content)?.swapRateType == ExpressRateType.Fixed) { + onExpandEditField(amountFieldUM.amountType) + } else { + onSelectTokenClick() + } + }, + ), + ) { + CurrencyIcon( + state = tokenIconState, + shouldDisplayNetwork = true, + modifier = Modifier.padding( + start = 16.dp, + top = 16.dp, + bottom = 16.dp, + ), + ) + SwapAmountInfoMain(amountFieldUM = amountFieldUM) + SpacerWMax() + AnimatedContent( + amountUM, + ) { wrappedAmountUM -> + if (wrappedAmountUM is SwapAmountUM.Content) { + SwapAmountInfoExtra( + amountUM = wrappedAmountUM, + amountFieldUM = amountFieldUM, + onMaxAmountClick = onMaxAmountClick, + onSelectTokenClick = onSelectTokenClick, + ) + } else { + RectangleShimmer() + } + } + } +} + +@Composable +private fun SwapAmountInfoMain(amountFieldUM: SwapAmountFieldUM, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = amountFieldUM !is SwapAmountFieldUM.Content, + modifier = modifier, + ) { isContent -> + if (isContent) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + TextShimmer( + style = TangemTheme.typography.subtitle2, + modifier = Modifier.width(56.dp), + ) + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(72.dp), + ) + } + } else { + val amountFieldUM = amountFieldUM as SwapAmountFieldUM.Content + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = amountFieldUM.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + ) + EllipsisText( + text = amountFieldUM.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ellipsis = amountFieldUM.subtitleEllipsis, + ) + } + } + } +} + +@Composable +private fun SwapAmountInfoExtra( + amountUM: SwapAmountUM.Content, + amountFieldUM: SwapAmountFieldUM, + onMaxAmountClick: () -> Unit, + onSelectTokenClick: () -> Unit, +) { + when (amountFieldUM.amountType) { + SwapAmountType.From -> when (amountUM.swapDirection) { + SwapDirection.Direct -> { + AnimatedVisibility( + visible = amountUM.selectedAmountType == amountFieldUM.amountType, + enter = fadeIn(), + exit = fadeOut(), + ) { + AmountMaxButton(onMaxAmountClick) + } + } + SwapDirection.Reverse -> { + SwapAmountInfoQuote( + quoteUM = amountUM.selectedQuote, + swapRateType = amountUM.swapRateType, + onSelectTokenClick = + onSelectTokenClick, + ) + } + } + + SwapAmountType.To -> when (amountUM.swapDirection) { + SwapDirection.Direct -> { + SwapAmountInfoQuote( + quoteUM = amountUM.selectedQuote, + swapRateType = amountUM.swapRateType, + onSelectTokenClick = onSelectTokenClick, + ) + } + SwapDirection.Reverse -> { + AnimatedVisibility( + visible = amountUM.selectedAmountType == amountFieldUM.amountType, + enter = fadeIn(), + exit = fadeOut(), + ) { + AmountMaxButton(onMaxAmountClick) + } + } + } + } +} + +@Composable +private fun AmountMaxButton(onMaxAmountClick: () -> Unit) { + SecondaryButton( + text = stringResourceSafe(R.string.send_max_amount), + size = TangemButtonSize.Small, + onClick = onMaxAmountClick, + modifier = Modifier.padding(end = 16.dp), + ) +} + +@Composable +private fun SwapAmountInfoQuote(quoteUM: SwapQuoteUM, swapRateType: ExpressRateType, onSelectTokenClick: () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.conditionalCompose( + condition = swapRateType == ExpressRateType.Fixed, + modifier = { + clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + onClick = onSelectTokenClick, + ) + }, + ), + ) { + when (quoteUM) { + is SwapQuoteUM.Content -> EllipsisText( + text = quoteUM.quoteAmountValue.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(end = 2.dp), + ) + + is SwapQuoteUM.Error, + is SwapQuoteUM.Empty, + -> Box(modifier = Modifier.padding(start = 16.dp)) + + is SwapQuoteUM.Loading -> CircularProgressIndicator( + color = TangemTheme.colors.icon.inactive, + modifier = Modifier + .padding(end = 4.dp) + .size(20.dp), + ) + + is SwapQuoteUM.Allowance -> Text( + text = "ALLOWANCE NOT IMPLEMENTED", + ) + } + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_chevron_24), + ), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + modifier = Modifier + .padding( + end = 16.dp, + top = 24.dp, + bottom = 24.dp, + ) + .size(24.dp), + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun SwapAmountContent_Preview( + @PreviewParameter(SwapAmountContentPreviewProvider::class) params: SwapAmountUM, +) { + TangemThemePreview { + SwapAmountContent( + amountUM = params, + modifier = Modifier, + clickIntents = SwapAmountClickIntentsStub, + ) + } +} + +private class SwapAmountContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SwapAmountContentPreview.emptyState, + SwapAmountContentPreview.defaultState, + ) +} +// endregion \ No newline at end of file From ebb5b5830ef8123e5263c90975e1e45b082b24a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Jun 2025 18:56:25 +0900 Subject: [PATCH 65/99] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 1be17abe62..d33eab3225 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1088" +tangemBlockchainSdk = "develop-1093" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-484" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From d1c13037e07cec9fcdc5213f17e16b955563c55d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Jun 2025 15:05:52 +0300 Subject: [PATCH 66/99] Updated on 2026-08-14 --- .../datasource/api/common/config/ApiConfig.kt | 4 +- .../config/{TangemVisa.kt => TangemPay.kt} | 2 +- .../TangemVisaApi.kt => pay/TangemPayApi.kt} | 35 +++++----- .../request/ActivationByCardWalletRequest.kt | 2 +- .../ActivationByCustomerWalletRequest.kt | 2 +- .../models/request/ActivationStatusRequest.kt | 2 +- .../request/ExchangeAccessTokenRequest.kt | 2 +- .../request/GenerateNoneByCardIdRequest.kt | 2 +- .../GenerateNoneByCardWalletRequest.kt | 2 +- .../request/GetAccessTokenByCardIdRequest.kt | 2 +- .../GetAccessTokenByCardWalletRequest.kt | 2 +- .../request/GetCardWalletAcceptanceRequest.kt | 2 +- .../GetCustomerWalletAcceptanceRequest.kt | 2 +- .../request/RefreshTokenByCardIdRequest.kt | 2 +- .../RefreshTokenByCardWalletRequest.kt | 2 +- .../models/request/SetPinCodeRequest.kt | 2 +- .../CardActivationRemoteStateResponse.kt | 2 +- .../models/response/GenerateNonceResponse.kt | 2 +- .../models/response/JWTResponse.kt | 2 +- .../models/response/KycAccessInfoResponse.kt | 15 ++++ .../models/response/VisaCustomerInfo.kt | 2 +- .../models/response/VisaDataToSignResponse.kt | 2 +- .../models/response/VisaErrorResponse.kt | 2 +- .../models/response/VisaTxHistoryResponse.kt | 2 +- .../tangem/datasource/di/ApiConfigsModule.kt | 2 +- .../com/tangem/datasource/di/NetworkModule.kt | 8 +-- .../managers/ProdApiConfigsManagerTest.kt | 6 +- data/visa/build.gradle.kts | 1 + .../tangem/data/pay/DefaultKycRepository.kt | 70 +++++++++++++++++++ .../tangem/data/pay/di/TangemPayDataModule.kt | 18 +++++ .../visa/DefaultVisaActivationRepository.kt | 10 +-- .../data/visa/DefaultVisaAuthRepository.kt | 8 +-- .../tangem/data/visa/DefaultVisaRepository.kt | 6 +- .../VisaActivationStatusConverterWithState.kt | 2 +- .../data/visa/utils/VisaApiRequestMaker.kt | 6 +- .../data/visa/utils/VisaTxDetailsFactory.kt | 2 +- .../visa/utils/VisaTxHistoryItemFactory.kt | 2 +- .../visa/utils/VisaTxHistoryPagingSource.kt | 2 +- .../com/tangem/domain/pay/KycStartInfo.kt | 6 ++ .../domain/pay/repository/KycRepository.kt | 15 ++++ features/kyc/impl/build.gradle.kts | 6 ++ .../features/kyc/DefaultKycComponent.kt | 60 ++++++++++++---- 42 files changed, 246 insertions(+), 80 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/common/config/{TangemVisa.kt => TangemPay.kt} (97%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa/TangemVisaApi.kt => pay/TangemPayApi.kt} (70%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/ActivationByCardWalletRequest.kt (95%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/ActivationByCustomerWalletRequest.kt (90%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/ActivationStatusRequest.kt (82%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/ExchangeAccessTokenRequest.kt (82%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/GenerateNoneByCardIdRequest.kt (85%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/GenerateNoneByCardWalletRequest.kt (86%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/GetAccessTokenByCardIdRequest.kt (86%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/GetAccessTokenByCardWalletRequest.kt (87%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/GetCardWalletAcceptanceRequest.kt (86%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/GetCustomerWalletAcceptanceRequest.kt (87%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/RefreshTokenByCardIdRequest.kt (83%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/RefreshTokenByCardWalletRequest.kt (83%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/request/SetPinCodeRequest.kt (85%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/response/CardActivationRemoteStateResponse.kt (93%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/response/GenerateNonceResponse.kt (86%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/response/JWTResponse.kt (93%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/KycAccessInfoResponse.kt rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/response/VisaCustomerInfo.kt (89%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/response/VisaDataToSignResponse.kt (84%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/response/VisaErrorResponse.kt (84%) rename core/datasource/src/main/java/com/tangem/datasource/api/{visa => pay}/models/response/VisaTxHistoryResponse.kt (98%) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/pay/KycStartInfo.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index f3cc288b25..2c1dcaea5b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -24,7 +24,7 @@ sealed class ApiConfig { Express, TangemTech, StakeKit, - TangemVisa, + TangemPay, Attestation, BlockAid, } @@ -34,7 +34,7 @@ sealed class ApiConfig { is Express -> ID.Express is TangemTech -> ID.TangemTech is StakeKit -> ID.StakeKit - is TangemVisa -> ID.TangemVisa + is TangemPay -> ID.TangemPay is Attestation -> ID.Attestation is BlockAid -> ID.BlockAid } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt similarity index 97% rename from core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt index b46a599b2d..b45492d87b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt @@ -3,7 +3,7 @@ package com.tangem.datasource.api.common.config import com.tangem.utils.ProviderSuspend import com.tangem.utils.version.AppVersionProvider -internal class TangemVisa( +internal class TangemPay( private val appVersionProvider: AppVersionProvider, ) : ApiConfig() { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt similarity index 70% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 5b2755a78a..2749826753 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -1,27 +1,27 @@ -package com.tangem.datasource.api.visa +package com.tangem.datasource.api.pay import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest -import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest -import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest -import com.tangem.datasource.api.visa.models.request.ExchangeAccessTokenRequest -import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardIdRequest -import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardWalletRequest -import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardIdRequest -import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardWalletRequest -import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest -import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest -import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardIdRequest -import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardWalletRequest -import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest -import com.tangem.datasource.api.visa.models.response.* +import com.tangem.datasource.api.pay.models.request.ActivationByCardWalletRequest +import com.tangem.datasource.api.pay.models.request.ActivationByCustomerWalletRequest +import com.tangem.datasource.api.pay.models.request.ActivationStatusRequest +import com.tangem.datasource.api.pay.models.request.ExchangeAccessTokenRequest +import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardIdRequest +import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardWalletRequest +import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardIdRequest +import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardWalletRequest +import com.tangem.datasource.api.pay.models.request.GetCardWalletAcceptanceRequest +import com.tangem.datasource.api.pay.models.request.GetCustomerWalletAcceptanceRequest +import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest +import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest +import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest +import com.tangem.datasource.api.pay.models.response.* import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST import retrofit2.http.Query -interface TangemVisaApi { +interface TangemPayApi { // region: auth @@ -104,4 +104,7 @@ interface TangemVisaApi { @Query("limit") limit: Int, @Query("offset") offset: Int, ): ApiResponse + + @GET("v1/customer/kyc") + suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCardWalletRequest.kt similarity index 95% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCardWalletRequest.kt index c16198f0c6..a5dafd7b32 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCardWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCustomerWalletRequest.kt similarity index 90% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCustomerWalletRequest.kt index ace2090603..731188d92a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCustomerWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationStatusRequest.kt similarity index 82% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationStatusRequest.kt index 7ce0614abc..9f3e017e55 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationStatusRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ExchangeAccessTokenRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ExchangeAccessTokenRequest.kt similarity index 82% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ExchangeAccessTokenRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ExchangeAccessTokenRequest.kt index 63b9fe43d8..164d71fa5c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ExchangeAccessTokenRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ExchangeAccessTokenRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardIdRequest.kt similarity index 85% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardIdRequest.kt index d3aa5eaa50..7ab945b8f8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardIdRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardWalletRequest.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardWalletRequest.kt index 69c2f7a7d5..95d2f8f9bf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardIdRequest.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardIdRequest.kt index c212c81412..380284bab7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardIdRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardWalletRequest.kt similarity index 87% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardWalletRequest.kt index df5d0c3935..50fc683c9b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCardWalletAcceptanceRequest.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCardWalletAcceptanceRequest.kt index dd084fa326..de4cec941b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCardWalletAcceptanceRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCustomerWalletAcceptanceRequest.kt similarity index 87% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCustomerWalletAcceptanceRequest.kt index 3948fc43b3..7ff5c16337 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCustomerWalletAcceptanceRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardIdRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/RefreshTokenByCardIdRequest.kt similarity index 83% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardIdRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/RefreshTokenByCardIdRequest.kt index f94a59a280..50653c8f79 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardIdRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/RefreshTokenByCardIdRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/RefreshTokenByCardWalletRequest.kt similarity index 83% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/RefreshTokenByCardWalletRequest.kt index 108469e1b6..d21bc53edf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/RefreshTokenByCardWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/SetPinCodeRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/SetPinCodeRequest.kt similarity index 85% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/SetPinCodeRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/SetPinCodeRequest.kt index 69e1a5ac08..2266910591 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/SetPinCodeRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/SetPinCodeRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.request +package com.tangem.datasource.api.pay.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardActivationRemoteStateResponse.kt similarity index 93% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardActivationRemoteStateResponse.kt index a34a31a366..ae9c701b06 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardActivationRemoteStateResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.response +package com.tangem.datasource.api.pay.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/GenerateNonceResponse.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/GenerateNonceResponse.kt index 9a79a457b8..958580bc5e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/GenerateNonceResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.response +package com.tangem.datasource.api.pay.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/JWTResponse.kt similarity index 93% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/JWTResponse.kt index 0380074d02..c28a882ae9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/JWTResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.response +package com.tangem.datasource.api.pay.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/KycAccessInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/KycAccessInfoResponse.kt new file mode 100644 index 0000000000..dea8079161 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/KycAccessInfoResponse.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class KycAccessInfoResponse( + @Json(name = "result") val result: Result, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "token") val token: String, + @Json(name = "locale") val locale: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaCustomerInfo.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaCustomerInfo.kt similarity index 89% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaCustomerInfo.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaCustomerInfo.kt index 70d5f6ac94..b6a227f62f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaCustomerInfo.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaCustomerInfo.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.response +package com.tangem.datasource.api.pay.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaDataToSignResponse.kt similarity index 84% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaDataToSignResponse.kt index a19d3e7ca4..a35c99ab59 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaDataToSignResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.response +package com.tangem.datasource.api.pay.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaErrorResponse.kt similarity index 84% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaErrorResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaErrorResponse.kt index 49a5050d0c..b211405000 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaErrorResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.response +package com.tangem.datasource.api.pay.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaTxHistoryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaTxHistoryResponse.kt similarity index 98% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaTxHistoryResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaTxHistoryResponse.kt index 7c4d7d0a22..aea17cc4c1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaTxHistoryResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaTxHistoryResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.visa.models.response +package com.tangem.datasource.api.pay.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 8afffb0a2a..b1761922d3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -53,7 +53,7 @@ internal object ApiConfigsModule { @Provides @IntoSet - fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider) + fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPay(appVersionProvider) @Provides @IntoSet diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 1110e9f745..4122660756 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -17,7 +17,7 @@ import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApiV2 -import com.tangem.datasource.api.visa.TangemVisaApi +import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.utils.* @@ -206,9 +206,9 @@ internal object NetworkModule { analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, appLogsStore: AppLogsStore, - ): TangemVisaApi { - return createApi( - id = ApiConfig.ID.TangemVisa, + ): TangemPayApi { + return createApi( + id = ApiConfig.ID.TangemPay, moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 4570f390fc..fe329835cd 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -37,7 +37,7 @@ private val API_CONFIGS = setOf( Express(configManager, expressAuthProvider, appVersionProvider, appInfoProvider), TangemTech(appVersionProvider, appAuthProvider, appInfoProvider), StakeKit(stakeKitAuthProvider), - TangemVisa(appVersionProvider), + TangemPay(appVersionProvider), ) /** @@ -88,7 +88,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { is Express -> createExpressModel() is TangemTech -> createTangemTechModel() is StakeKit -> createStakeKitModel() - is TangemVisa -> createVisaModel() + is TangemPay -> createVisaModel() is Attestation -> createAttestationModel() is BlockAid -> createBlockAidSdkModel() } @@ -182,7 +182,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { private fun createVisaModel(): Model { return Model( - id = ApiConfig.ID.TangemVisa, + id = ApiConfig.ID.TangemPay, expected = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, baseUrl = "[REDACTED_ENV_URL]", diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index f8999494d2..6740dd9dde 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { /** Project - Data */ implementation(projects.core.datasource) + implementation(projects.core.error) implementation(projects.data.common) /** Project - Domain */ diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt new file mode 100644 index 0000000000..8df8da505d --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt @@ -0,0 +1,70 @@ +package com.tangem.data.pay + +import arrow.core.Either +import com.squareup.moshi.Moshi +import com.tangem.core.error.UniversalError +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.domain.pay.KycStartInfo +import com.tangem.domain.pay.repository.KycRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.withContext + +@Suppress("UnusedPrivateMember") +class DefaultKycRepository @AssistedInject constructor( + @Assisted userWalletId: UserWalletId, + @NetworkMoshi moshi: Moshi, + private val tangemPayApi: TangemPayApi, + private val dispatcherProvider: CoroutineDispatcherProvider, +) : KycRepository { + + private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) + + override suspend fun getKycStartInfo(): Either = withContext(dispatcherProvider.io) { + val authTokenForSpecificWallet = "get from userWalletId" + + request { + tangemPayApi.getKycAccess( + authHeader = authTokenForSpecificWallet, + ).getOrThrow().result + }.map { + KycStartInfo( + token = it.token, + locale = it.locale, + ) + } + } + + private suspend fun request(requestBlock: suspend () -> T): Either { + return runCatching { + Either.Right(requestBlock()) + }.getOrElse { responseError -> + if (responseError is ApiResponseError.HttpException && + responseError.errorBody != null + ) { + return runCatching { + visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode + }.map { + Either.Left(VisaApiError.fromBackendError(it)) + }.getOrElse { + Either.Left(VisaApiError.UnknownWithoutCode) + } + } + + return Either.Left(VisaApiError.UnknownWithoutCode) + } + } + + @AssistedFactory + interface Factory : KycRepository.Factory { + override fun create(userWalletId: UserWalletId): DefaultKycRepository + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt new file mode 100644 index 0000000000..d0de7d5c31 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -0,0 +1,18 @@ +package com.tangem.data.pay.di + +import com.tangem.data.pay.DefaultKycRepository +import com.tangem.domain.pay.repository.KycRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface TangemPayDataModule { + + @Binds + @Singleton + fun bindKycRepositoryFactory(factory: DefaultKycRepository.Factory): KycRepository.Factory +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index 40c5d49fe6..8d10a750ff 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -10,9 +10,9 @@ import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.visa.TangemVisaApi -import com.tangem.datasource.api.visa.models.request.* -import com.tangem.datasource.api.visa.models.response.VisaErrorResponseJsonAdapter +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.request.* +import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.visa.error.VisaApiError @@ -29,7 +29,7 @@ import kotlinx.coroutines.withContext internal class DefaultVisaActivationRepository @AssistedInject constructor( @Assisted private val visaCardId: VisaCardId, @NetworkMoshi private val moshi: Moshi, - private val visaApi: TangemVisaApi, + private val visaApi: TangemPayApi, private val dispatcherProvider: CoroutineDispatcherProvider, private val visaAuthTokenStorage: VisaAuthTokenStorage, private val visaAuthRepository: VisaAuthRepository, @@ -162,7 +162,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( } override suspend fun getPinCodeRsaEncryptionPublicKey(): String { - val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemVisa).environment + val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemPay).environment val rsaPublicKey = visaLibLoader.getOrCreateConfig().rsaPublicKey return when (env) { ApiEnvironment.DEV, diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index b0f2b5e284..c9c07624e5 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -4,9 +4,9 @@ import arrow.core.Either import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.visa.TangemVisaApi -import com.tangem.datasource.api.visa.models.request.* -import com.tangem.datasource.api.visa.models.response.VisaErrorResponseJsonAdapter +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.request.* +import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter import com.tangem.datasource.di.NetworkMoshi import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.VisaAuthChallenge @@ -21,7 +21,7 @@ import javax.inject.Inject @Suppress("UnusedPrivateMember") internal class DefaultVisaAuthRepository @Inject constructor( @NetworkMoshi private val moshi: Moshi, - private val visaAuthApi: TangemVisaApi, + private val visaAuthApi: TangemPayApi, private val dispatchers: CoroutineDispatcherProvider, ) : VisaAuthRepository { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 09eaf90d10..55d8eae097 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -14,8 +14,8 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.visa.config.VisaLibLoader import com.tangem.data.visa.utils.* -import com.tangem.datasource.api.visa.TangemVisaApi -import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.visa.model.VisaCurrency @@ -43,7 +43,7 @@ internal class DefaultVisaRepository @Inject constructor( private val userWalletsStore: UserWalletsStore, private val dispatchers: CoroutineDispatcherProvider, private val visaApiRequestMaker: VisaApiRequestMaker, - private val visaApi: TangemVisaApi, + private val visaApi: TangemPayApi, private val visaCurrencyFactory: VisaCurrencyFactory, ) : VisaRepository { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt index cc00430f80..09128b4324 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt @@ -1,6 +1,6 @@ package com.tangem.data.visa.converter -import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse +import com.tangem.datasource.api.pay.models.response.CardActivationRemoteStateResponse import com.tangem.domain.visa.model.VisaActivationOrderInfo import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.utils.converter.Converter diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt index 147f558037..792ad89620 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt @@ -5,8 +5,8 @@ import com.tangem.data.visa.model.AccessCodeData import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.visa.TangemVisaApi -import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardWalletRequest +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.visa.exception.RefreshTokenExpiredException @@ -25,7 +25,7 @@ typealias VisaAuthorizationHeader = String internal class VisaApiRequestMaker @Inject constructor( private val userWalletsStore: UserWalletsStore, - private val visaAuthApi: TangemVisaApi, + private val visaAuthApi: TangemPayApi, private val accessCodeDataConverter: AccessCodeDataConverter, private val dispatcherProvider: CoroutineDispatcherProvider, ) { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt index 8f9b41148f..b33a0cafb9 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt @@ -2,7 +2,7 @@ package com.tangem.data.visa.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.externallinkprovider.TxExploreState -import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse import com.tangem.domain.visa.model.VisaTxDetails internal class VisaTxDetailsFactory { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt index 75a8c4eefe..d7be726e36 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt @@ -1,6 +1,6 @@ package com.tangem.data.visa.utils -import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse import com.tangem.domain.visa.model.VisaTxHistoryItem internal class VisaTxHistoryItemFactory { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt index 4393d4f372..c37c9f9762 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt @@ -3,7 +3,7 @@ package com.tangem.data.visa.utils import androidx.paging.PagingSource import androidx.paging.PagingState import com.tangem.data.common.cache.CacheRegistry -import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.domain.wallets.models.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/KycStartInfo.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/KycStartInfo.kt new file mode 100644 index 0000000000..e568e679f3 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/KycStartInfo.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.pay + +data class KycStartInfo( + val token: String, + val locale: String, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt new file mode 100644 index 0000000000..58380aabb0 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.pay.repository + +import arrow.core.Either +import com.tangem.core.error.UniversalError +import com.tangem.domain.pay.KycStartInfo +import com.tangem.domain.wallets.models.UserWalletId + +interface KycRepository { + + suspend fun getKycStartInfo(): Either + + interface Factory { + fun create(userWalletId: UserWalletId): KycRepository + } +} \ No newline at end of file diff --git a/features/kyc/impl/build.gradle.kts b/features/kyc/impl/build.gradle.kts index 86e6e641fb..1bf8fc66f2 100644 --- a/features/kyc/impl/build.gradle.kts +++ b/features/kyc/impl/build.gradle.kts @@ -15,6 +15,10 @@ dependencies { /** Api */ implementation(projects.features.kyc.api) + /** Domain */ + implementation(projects.domain.visa) + implementation(projects.domain.wallets.models) + /** Core modules */ implementation(projects.core.configToggles) implementation(projects.core.analytics) @@ -25,6 +29,7 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.navigation) implementation(projects.core.datasource) + implementation(projects.core.error) /** Common */ implementation(projects.common.ui) @@ -47,6 +52,7 @@ dependencies { implementation(deps.timber) implementation(deps.firebase.crashlytics) implementation(deps.sumsub.sdk) + implementation(deps.arrow.core) /** DI */ implementation(deps.hilt.android) diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt index d192cead2d..5494ff060e 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt @@ -1,35 +1,67 @@ package com.tangem.features.kyc import com.sumsub.sns.core.SNSMobileSDK +import com.sumsub.sns.core.data.listener.SNSCompleteHandler import com.sumsub.sns.core.data.listener.TokenExpirationHandler +import com.sumsub.sns.core.data.model.SNSCompletionResult +import com.sumsub.sns.core.data.model.SNSSDKState +import com.sumsub.sns.core.theme.SNSTheme +import com.sumsub.sns.core.theme.SNSThemeMetric +import com.sumsub.sns.core.theme.colors +import com.sumsub.sns.core.theme.metrics import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.pay.repository.KycRepository +import com.tangem.domain.wallets.models.UserWalletId import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import java.util.Locale class DefaultKycComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, + private val kycRepositoryFactory: KycRepository.Factory, ) : KycComponent, AppComponentContext by appComponentContext { + private val kycRepository = kycRepositoryFactory.create(UserWalletId("0FFFFF")) + override fun launch() { - val accessToken = "123456" + componentScope.launch { + // val startInfo = kycRepository.getKycStartInfo().getOrNull() ?: return@launch - val tokenExpirationHandler = object : TokenExpirationHandler { - override fun onTokenExpired(): String? { - // Access token expired - // get a new one and pass it to the callback to re-initiate the SDK - val newToken = "..." // get a new token from your backend - return newToken + val tokenExpirationHandler = object : TokenExpirationHandler { + override fun onTokenExpired(): String? { + val newToken = runBlocking { kycRepository.getKycStartInfo().getOrNull()?.token } + return newToken + } } + + val snsSdk = SNSMobileSDK.Builder(activity) + .withAccessToken( + "TODO", + onTokenExpiration = tokenExpirationHandler, + ) + .withTheme( + SNSTheme { + colors { + } + metrics { + this.screenHeaderAlignment = SNSThemeMetric.TextAlignment.CENTER + } + }, + ) + .withLocale(Locale("en")) + .withCompleteHandler( + object : SNSCompleteHandler { + override fun onComplete(result: SNSCompletionResult, state: SNSSDKState) { + } + }, + ) + .build() + + snsSdk.launch() } - - val snsSdk = SNSMobileSDK.Builder(activity) - .withAccessToken(accessToken, onTokenExpiration = tokenExpirationHandler) - .withLocale(Locale("en")) - .build() - - snsSdk.launch() } @AssistedFactory From 6626b53394fee71cc58b467c1acf4fede94742cf Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Jun 2025 11:27:48 +0400 Subject: [PATCH 67/99] Updated on 2026-08-14 --- .../ethereum/WcEthSendTransactionUseCase.kt | 34 ++++++++++++------- .../ethereum/WcEthSignTransactionUseCase.kt | 32 +++++++++-------- .../network/ethereum/WcEthTxHelper.kt | 3 +- .../walletconnect/model/WcApprovedAmount.kt | 9 +++++ .../usecase/method/WcTransactionUseCase.kt | 8 ++--- .../model/WcSendTransactionModel.kt | 14 +++++--- .../WcSendAndReceiveBlockAidUiConverter.kt | 6 ++-- .../blockaid/WcSpendAllowanceUMConverter.kt | 32 +++++++++-------- 8 files changed, 85 insertions(+), 53 deletions(-) create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcApprovedAmount.kt diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index ccce4e7bad..4b80dbfbd4 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData -import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Amount as BlockchainAmount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex @@ -15,7 +15,9 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.tokens.model.Amount import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.usecase.method.* import dagger.assisted.Assisted @@ -35,7 +37,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( WcApproval, WcMutableFee { - private var approvalAmount: Amount? = null + private var approvalAmount: WcApprovedAmount? = null private var dAppFee = WcEthTxHelper.getDAppFee( network = context.network, txParams = method.transaction, @@ -53,13 +55,19 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result) ?: return@map BlockAidTransactionCheck.Result.Plain(result) val tokenInfo = amount.tokenInfo - if (!amount.isUnlimited) { - this@WcEthSendTransactionUseCase.approvalAmount = Amount( - currencySymbol = tokenInfo.symbol, - decimals = tokenInfo.decimals, - value = amount.approvedAmount, - ) - } + this@WcEthSendTransactionUseCase.approvalAmount = WcApprovedAmount( + amount = if (!amount.isUnlimited) { + Amount( + currencySymbol = tokenInfo.symbol, + decimals = tokenInfo.decimals, + value = amount.approvedAmount, + ) + } else { + null + }, + logoUrl = tokenInfo.logoUrl, + chainId = tokenInfo.chainId, + ) BlockAidTransactionCheck.Result.Approval( result = result, approval = this, @@ -90,7 +98,9 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( val extras = uncompiled.extras as EthereumTransactionExtras val callData = ApprovalERC20TokenCallData( spenderAddress = uncompiled.sourceAddress, - amount = action.amount, + amount = action.amount?.amount?.let { + BlockchainAmount(currencySymbol = it.currencySymbol, decimals = it.decimals, value = it.value) + }, ) approvalAmount = action.amount dAppFee = null @@ -119,11 +129,11 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( emitAll(delegate.invoke(transactionData)) } - override fun getAmount(): Amount? { + override fun getAmount(): WcApprovedAmount? { return approvalAmount } - override fun updateAmount(amount: Amount?) { + override fun updateAmount(amount: WcApprovedAmount?) { middleAction(WcEthTxAction.UpdateApprovalAmount(amount)) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index ed984c971c..b86988cbae 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData -import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Amount as BlockchainAmount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex @@ -15,13 +15,11 @@ import com.tangem.data.walletconnect.sign.SignCollector import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate +import com.tangem.domain.tokens.model.Amount import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.domain.walletconnect.model.WcEthMethod -import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck -import com.tangem.domain.walletconnect.usecase.method.WcApproval -import com.tangem.domain.walletconnect.usecase.method.WcMutableFee -import com.tangem.domain.walletconnect.usecase.method.WcSignState -import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase +import com.tangem.domain.walletconnect.usecase.method.* import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -43,7 +41,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( WcApproval, WcMutableFee { - private var approvalAmount: Amount? = null + private var approvalAmount: WcApprovedAmount? = null private var dAppFee = WcEthTxHelper.getDAppFee( network = context.network, txParams = method.transaction, @@ -61,10 +59,14 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( ?: return@map BlockAidTransactionCheck.Result.Plain(result) val tokenInfo = amount.tokenInfo if (!amount.isUnlimited) { - approvalAmount = Amount( - currencySymbol = tokenInfo.symbol, - decimals = tokenInfo.decimals, - value = amount.approvedAmount, + approvalAmount = WcApprovedAmount( + amount = Amount( + currencySymbol = tokenInfo.symbol, + decimals = tokenInfo.decimals, + value = amount.approvedAmount, + ), + logoUrl = tokenInfo.logoUrl, + chainId = tokenInfo.chainId, ) } BlockAidTransactionCheck.Result.Approval( @@ -98,7 +100,9 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( val extras = uncompiled.extras as EthereumTransactionExtras val callData = ApprovalERC20TokenCallData( spenderAddress = uncompiled.sourceAddress, - amount = action.amount, + amount = action.amount?.amount?.let { + BlockchainAmount(currencySymbol = it.currencySymbol, decimals = it.decimals, value = it.value) + }, ) approvalAmount = action.amount dAppFee = null @@ -127,11 +131,11 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( return dAppFee } - override fun getAmount(): Amount? { + override fun getAmount(): WcApprovedAmount? { return approvalAmount } - override fun updateAmount(amount: Amount?) { + override fun updateAmount(amount: WcApprovedAmount?) { middleAction(WcEthTxAction.UpdateApprovalAmount(amount)) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt index a2fc3d4224..268633a213 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt @@ -16,6 +16,7 @@ import com.tangem.blockchain.extensions.hexToBigDecimal import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.extensions.hexToBytes import com.tangem.domain.models.network.Network +import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.domain.walletconnect.model.WcEthTransactionParams import java.math.BigDecimal @@ -81,5 +82,5 @@ internal object WcEthTxHelper { sealed interface WcEthTxAction { data class UpdateFee(val fee: Fee) : WcEthTxAction - data class UpdateApprovalAmount(val amount: Amount?) : WcEthTxAction + data class UpdateApprovalAmount(val amount: WcApprovedAmount?) : WcEthTxAction } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcApprovedAmount.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcApprovedAmount.kt new file mode 100644 index 0000000000..5b7dc55b23 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcApprovedAmount.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.tokens.model.Amount + +data class WcApprovedAmount( + val amount: Amount?, + val chainId: Int?, + val logoUrl: String?, +) \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt index c4410c3a7f..acc4686c4d 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt @@ -1,8 +1,8 @@ package com.tangem.domain.walletconnect.usecase.method -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.walletconnect.model.WcApprovedAmount /** * Base UseCase for wc methods with single TransactionData @@ -40,10 +40,10 @@ interface WcMutableFee { /** * Wc Approval Method - * If [Amount] is null it means unlimited + * If [WcApprovedAmount.amount] is null it means unlimited * [updateAmount] triggered a new [TransactionData] emit */ interface WcApproval { - fun getAmount(): Amount? - fun updateAmount(amount: Amount?) + fun getAmount(): WcApprovedAmount? + fun updateAmount(amount: WcApprovedAmount?) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 3f9908da90..4cde43d227 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -105,11 +105,11 @@ internal class WcSendTransactionModel @Inject constructor( this@WcSendTransactionModel.signState = signState val isSecurityCheckContent = securityCheck is Lce.Content - var isApprovalMethod = isSecurityCheckContent && + val isApprovalMethod = isSecurityCheckContent && securityCheck.content is BlockAidTransactionCheck.Result.Approval wcApproval = useCase as? WcApproval sign = { useCase.sign() } - buildUiState(securityCheck, useCase, signState) + buildUiState(securityCheck, useCase, signState, isApprovalMethod) } } else -> unknownMethodRunnable() @@ -144,10 +144,14 @@ internal class WcSendTransactionModel @Inject constructor( securityCheck: Lce, useCase: WcSignUseCase<*>, signState: WcSignState<*>, + isApproval: Boolean, ) { val blockAidState = when (securityCheck) { is Lce.Content -> blockAidUiConverter.convert( - WcSendAndReceiveBlockAidUiConverter.Input(securityCheck.content.result, wcApproval?.getAmount()), + WcSendAndReceiveBlockAidUiConverter.Input( + securityCheck.content.result, + if (isApproval) wcApproval?.getAmount() else null, + ), ) is Lce.Error -> WcSendReceiveTransactionCheckResultsUM(isLoading = false) is Lce.Loading -> WcSendReceiveTransactionCheckResultsUM(isLoading = true) @@ -188,9 +192,9 @@ internal class WcSendTransactionModel @Inject constructor( } fun onClickDoneCustomAllowance(value: BigDecimal, isUnlimited: Boolean) { - val maxValue = if (isUnlimited) Double.MAX_VALUE.toBigDecimal() else value + val newValue = if (isUnlimited) null else value wcApproval?.getAmount()?.let { currentAmount -> - wcApproval?.updateAmount(currentAmount.copy(maxValue = maxValue)) + wcApproval?.updateAmount(currentAmount.copy(amount = currentAmount.amount?.copy(value = newValue))) } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index 14d3abede0..aa9eb45eb3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -5,10 +5,10 @@ import com.domain.blockaid.models.transaction.SimulationResult import com.domain.blockaid.models.transaction.ValidationResult import com.domain.blockaid.models.transaction.simultation.AmountInfo import com.domain.blockaid.models.transaction.simultation.SimulationData -import com.tangem.blockchain.common.Amount import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM @@ -86,7 +86,7 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( when (data) { is SimulationData.SendAndReceive -> null is SimulationData.Approve -> value.approvedAmount?.let { - data.approvedAmounts.map { spendAllowanceUMConverter.convert(it) }.firstOrNull() + spendAllowanceUMConverter.convert(it) } } }, @@ -95,7 +95,7 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( data class Input( val result: CheckTransactionResult, - val approvedAmount: Amount?, + val approvedAmount: WcApprovedAmount?, ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt index 96d797734c..4f7bf34268 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt @@ -1,25 +1,29 @@ package com.tangem.features.walletconnect.transaction.ui.blockaid -import com.domain.blockaid.models.transaction.simultation.ApprovedAmount import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.utils.converter.Converter import javax.inject.Inject -internal class WcSpendAllowanceUMConverter @Inject constructor() : Converter { +internal class WcSpendAllowanceUMConverter @Inject constructor() : Converter { - override fun convert(value: ApprovedAmount) = WcSpendAllowanceUM( - amountValue = value.approvedAmount, - amountText = if (value.isUnlimited) { - TextReference.Res(R.string.wc_common_unlimited) - } else { - TextReference.Str(value.approvedAmount.amountText()) - }, - isUnlimited = value.isUnlimited, - tokenSymbol = value.tokenInfo.symbol, - tokenImageUrl = value.tokenInfo.logoUrl, - networkIconRes = value.tokenInfo.chainId?.toString()?.let { getActiveIconRes(it) }, - ) + override fun convert(value: WcApprovedAmount): WcSpendAllowanceUM { + val amount = value.amount?.value ?: 0.0.toBigDecimal() + val isUnlimited = value.amount?.value == null + return WcSpendAllowanceUM( + amountValue = amount, + amountText = if (value.amount?.value == null) { + TextReference.Res(R.string.wc_common_unlimited) + } else { + TextReference.Str(amount.amountText()) + }, + isUnlimited = isUnlimited, + tokenSymbol = value.amount?.currencySymbol ?: "", + tokenImageUrl = value.logoUrl, + networkIconRes = value.chainId?.toString()?.let { getActiveIconRes(it) }, + ) + } } \ No newline at end of file From 80595f8381465cdfcbd4b96739f69b0a0ad4bcff Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Jun 2025 12:44:41 +0300 Subject: [PATCH 68/99] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 97949a7daf..92b3c84211 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -32,7 +32,7 @@ compose-runtime = "1.7.4" compose-foundation = "1.7.4" compose-material = "1.7.4" compose-material3 = "1.3.1" -compose-constraint = "1.1.1" +compose-constraint = "1.0.1" compose-navigation = "2.7.7" compose-accompanist = "0.30.1" compose-paging = "3.2.1" From ebebd1d3874254f03be68a3c8fec6403cc2816c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Jun 2025 09:55:08 +0000 Subject: [PATCH 69/99] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 292e53e595..d33eab3225 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.25.0-1099" +tangemBlockchainSdk = "develop-1093" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.25.0-486" +tangemCardSdk = "develop-484" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 4218aea2bd53d7387331d61f9951cafc8dafcf67 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Jun 2025 15:27:09 +0500 Subject: [PATCH 70/99] Updated on 2026-08-14 --- .../common/ui/notifications/Notifications.kt | 47 +++++++++++++++++++ features/send-v2/api/build.gradle.kts | 5 ++ .../v2/api/SendNotificationsComponent.kt} | 43 +++++++++-------- .../features/send/v2/di/SendFeatureModule.kt | 8 ++++ .../v2/send/confirm/SendConfirmComponent.kt | 9 ++-- .../v2/send/confirm/model/SendConfirmModel.kt | 2 +- .../v2/send/confirm/ui/SendConfirmContent.kt | 7 +-- .../confirm/NFTSendConfirmComponent.kt | 10 ++-- .../confirm/model/NFTSendConfirmModel.kt | 2 +- .../confirm/ui/NFTSendConfirmContent.kt | 4 +- .../DefaultSendNotificationsComponent.kt | 45 ++++++++++++++++++ .../NotificationsUpdateTrigger.kt | 2 +- .../notifications/model/NotificationData.kt | 15 ------ .../notifications/model/NotificationsModel.kt | 5 +- 14 files changed, 148 insertions(+), 56 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt rename features/send-v2/{impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt => api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt} (51%) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/DefaultSendNotificationsComponent.kt delete mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt new file mode 100644 index 0000000000..68a3f77292 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt @@ -0,0 +1,47 @@ +package com.tangem.common.ui.notifications + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList + +fun LazyListScope.notifications( + notifications: ImmutableList, + modifier: Modifier = Modifier, + hasPaddingAbove: Boolean = false, + isClickDisabled: Boolean = false, +) { + itemsIndexed( + items = notifications, + key = { _, item -> item::class.java }, + contentType = { _, item -> item::class.java }, + itemContent = { i, item -> + val topPadding = if (i == 0 && hasPaddingAbove) 0.dp else 12.dp + Notification( + config = item.config, + modifier = modifier + .padding(top = topPadding) + .animateItem(), + containerColor = when (item) { + is NotificationUM.Error.TokenExceedsBalance, + is NotificationUM.Warning.NetworkFeeUnreachable, + is NotificationUM.Warning.HighFeeError, + -> TangemTheme.colors.background.action + else -> TangemTheme.colors.button.disabled + }, + iconTint = when (item) { + is NotificationUM.Error.TokenExceedsBalance, + is NotificationUM.Warning, + -> null + is NotificationUM.Error -> TangemTheme.colors.icon.warning + is NotificationUM.Info -> TangemTheme.colors.icon.accent + }, + isEnabled = !isClickDisabled, + ) + }, + ) +} \ No newline at end of file diff --git a/features/send-v2/api/build.gradle.kts b/features/send-v2/api/build.gradle.kts index 43576f6348..6a1ce98877 100644 --- a/features/send-v2/api/build.gradle.kts +++ b/features/send-v2/api/build.gradle.kts @@ -13,6 +13,9 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) + /** Common */ + implementation(projects.common.ui) + /** Domain models */ api(projects.domain.models) implementation(projects.domain.appCurrency.models) @@ -23,10 +26,12 @@ dependencies { /** Compose */ implementation(deps.compose.runtime) + implementation(deps.compose.foundation) /** Tangem */ implementation(tangemDeps.blockchain) /** Other */ implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt similarity index 51% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt index 4f163c1a61..25f8e2bf6f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt @@ -1,41 +1,28 @@ -package com.tangem.features.send.v2.subcomponents.notifications +package com.tangem.features.send.v2.api import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.Modifier +import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.send.v2.subcomponents.notifications -import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData -import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationsModel import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.StateFlow +import java.math.BigDecimal -internal class NotificationsComponent( - appComponentContext: AppComponentContext, - params: Params, -) : AppComponentContext by appComponentContext { +interface SendNotificationsComponent { - private val model: NotificationsModel = getOrCreateModel(params) - - val state: StateFlow> = model.uiState + val state: StateFlow> fun LazyListScope.content( state: ImmutableList, modifier: Modifier = Modifier, hasPaddingAbove: Boolean = false, isClickDisabled: Boolean = false, - ) { - notifications( - notifications = state, - modifier = modifier, - hasPaddingAbove = hasPaddingAbove, - isClickDisabled = isClickDisabled, - ) - } + ) data class Params( val analyticsCategoryName: String, @@ -44,5 +31,17 @@ internal class NotificationsComponent( val feeCryptoCurrencyStatus: CryptoCurrencyStatus, val appCurrency: AppCurrency, val notificationData: NotificationData, - ) + ) { + data class NotificationData( + val destinationAddress: String, + val memo: String?, + val amountValue: BigDecimal, + val reduceAmountBy: BigDecimal, + val isIgnoreReduce: Boolean, + val fee: Fee?, + val feeError: GetFeeError?, + ) + } + + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt index 3c2f9bcf9f..0cde15afdd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt @@ -5,8 +5,10 @@ import com.tangem.features.send.v2.DefaultSendFeatureToggles import com.tangem.features.send.v2.api.NFTSendComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.send.DefaultSendComponent import com.tangem.features.send.v2.sendnft.DefaultNFTSendComponent +import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent import dagger.Binds import dagger.Module import dagger.Provides @@ -35,4 +37,10 @@ internal interface SendFeatureModuleBinds { @Binds @Singleton fun provideNFTSendComponentFactory(impl: DefaultNFTSendComponent.Factory): NFTSendComponent.Factory + + @Binds + @Singleton + fun provideNotificationComponentFactory( + impl: DefaultSendNotificationsComponent.Factory, + ): SendNotificationsComponent.Factory } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index 9a34d59c65..0145507ff8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -14,6 +14,8 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.api.SendNotificationsComponent +import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.ui.state.ConfirmUM @@ -26,8 +28,7 @@ import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBloc import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams -import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent -import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData +import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent import com.tangem.utils.extensions.orZero import kotlinx.coroutines.flow.* @@ -89,9 +90,9 @@ internal class SendConfirmComponent( onClick = model::showEditFee, ) - private val notificationsComponent = NotificationsComponent( + private val notificationsComponent = DefaultSendNotificationsComponent( appComponentContext = child("sendConfirmNotifications"), - params = NotificationsComponent.Params( + params = SendNotificationsComponent.Params( analyticsCategoryName = params.analyticsCategoryName, userWalletId = params.userWallet.walletId, cryptoCurrencyStatus = params.cryptoCurrencyStatus, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 29a3e1e93d..d9d8e350dd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -32,6 +32,7 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.requireColdWallet +import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.SendConfirmAlertFactory @@ -55,7 +56,6 @@ import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubt import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger -import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.stripZeroPlainString diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index a6162f90f2..649af62424 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -6,7 +6,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM @@ -27,7 +28,7 @@ import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.notifications -import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent +import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent import kotlinx.collections.immutable.ImmutableList private const val BLOCKS_KEY = "BLOCKS_KEY" @@ -39,7 +40,7 @@ internal fun SendConfirmContent( destinationBlockComponent: SendDestinationBlockComponent, amountBlockComponent: SendAmountBlockComponent, feeBlockComponent: SendFeeBlockComponent, - notificationsComponent: NotificationsComponent, + notificationsComponent: DefaultSendNotificationsComponent, notificationsUM: ImmutableList, ) { val confirmUM = sendUM.confirmUM as? ConfirmUM.Content diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt index 486dd72606..8eb01e8547 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt @@ -16,6 +16,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.ui.state.ConfirmUM @@ -26,8 +27,7 @@ import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBloc import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams -import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent -import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData +import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent import kotlinx.coroutines.flow.* import java.math.BigDecimal @@ -83,15 +83,15 @@ internal class NFTSendConfirmComponent( ), ) - private val notificationsComponent = NotificationsComponent( + private val notificationsComponent = DefaultSendNotificationsComponent( appComponentContext = child("NFTSendConfirmNotifications"), - params = NotificationsComponent.Params( + params = SendNotificationsComponent.Params( analyticsCategoryName = params.analyticsCategoryName, userWalletId = params.userWallet.walletId, cryptoCurrencyStatus = params.cryptoCurrencyStatus, feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, appCurrency = params.appCurrency, - notificationData = NotificationData( + notificationData = SendNotificationsComponent.Params.NotificationData( destinationAddress = model.confirmData.enteredDestination.orEmpty(), memo = model.confirmData.enteredMemo, amountValue = BigDecimal.ZERO, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 68dce3b0dc..e9121d6a4c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -26,6 +26,7 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.features.nft.entity.NFTSendSuccessTrigger +import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.SendConfirmAlertFactory @@ -47,7 +48,6 @@ import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger -import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.utils.transformer.update diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt index 2d41666e61..22e963cff2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -28,7 +28,7 @@ import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.notifications -import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent +import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent import kotlinx.collections.immutable.ImmutableList private const val BLOCKS_KEY = "BLOCKS_KEY" @@ -40,7 +40,7 @@ internal fun NFTSendConfirmContent( destinationBlockComponent: SendDestinationBlockComponent, nftDetailsBlockComponent: NFTDetailsBlockComponent, feeBlockComponent: SendFeeBlockComponent, - notificationsComponent: NotificationsComponent, + notificationsComponent: DefaultSendNotificationsComponent, notificationsUM: ImmutableList, ) { val confirmUM = nftSendUM.confirmUM as? ConfirmUM.Content diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/DefaultSendNotificationsComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/DefaultSendNotificationsComponent.kt new file mode 100644 index 0000000000..2a5d8b171c --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/DefaultSendNotificationsComponent.kt @@ -0,0 +1,45 @@ +package com.tangem.features.send.v2.subcomponents.notifications + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.send.v2.api.SendNotificationsComponent +import com.tangem.features.send.v2.api.SendNotificationsComponent.Params +import com.tangem.features.send.v2.subcomponents.notifications +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationsModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.StateFlow + +internal class DefaultSendNotificationsComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Params, +) : SendNotificationsComponent, AppComponentContext by appComponentContext { + + private val model: NotificationsModel = getOrCreateModel(params) + + override val state: StateFlow> = model.uiState + + override fun LazyListScope.content( + state: ImmutableList, + modifier: Modifier, + hasPaddingAbove: Boolean, + isClickDisabled: Boolean, + ) { + notifications( + notifications = state, + modifier = modifier, + hasPaddingAbove = hasPaddingAbove, + isClickDisabled = isClickDisabled, + ) + } + + @AssistedFactory + interface Factory : SendNotificationsComponent.Factory { + override fun create(context: AppComponentContext, params: Params): DefaultSendNotificationsComponent + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt index bdcf4b4076..ed7123bbcd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt @@ -1,6 +1,6 @@ package com.tangem.features.send.v2.subcomponents.notifications -import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData +import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt deleted file mode 100644 index bf7e440869..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.features.send.v2.subcomponents.notifications.model - -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.domain.transaction.error.GetFeeError -import java.math.BigDecimal - -data class NotificationData( - val destinationAddress: String, - val memo: String?, - val amountValue: BigDecimal, - val reduceAmountBy: BigDecimal, - val isIgnoreReduce: Boolean, - val fee: Fee?, - val feeError: GetFeeError?, -) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index c5cda3887b..726be9421e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -35,12 +35,13 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount +import com.tangem.features.send.v2.api.SendNotificationsComponent +import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger import com.tangem.features.send.v2.subcomponents.fee.SendFeeData import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount import com.tangem.features.send.v2.subcomponents.fee.model.checkFeeCoverage -import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger import com.tangem.features.send.v2.subcomponents.notifications.analytics.NotificationsAnalyticEvents import com.tangem.lib.crypto.BlockchainUtils @@ -77,7 +78,7 @@ internal class NotificationsModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { - private val params: NotificationsComponent.Params = paramsContainer.require() + private val params: SendNotificationsComponent.Params = paramsContainer.require() private val analyticsCategoryName = params.analyticsCategoryName private val userWalletId = params.userWalletId From 19341c536e8180223e96a389b7e26e3ffdf1216a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Jun 2025 12:58:13 +0500 Subject: [PATCH 71/99] Updated on 2026-08-14 --- .../v2/impl/amount/SwapAmountComponent.kt | 42 +++++++++++ .../impl/amount/SwapAmountComponentParams.kt | 2 - .../v2/impl/amount/di/SwapAmountModule.kt | 20 +++++ .../v2/impl/amount/model/SwapAmountModel.kt | 4 +- .../ui/preview/SwapAmountContentPreview.kt | 73 +++++++++++++++++++ .../common/SwapNavigationModelCallback.kt | 2 + 6 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/di/SwapAmountModule.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt new file mode 100644 index 0000000000..aeb060452e --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.swap.v2.impl.amount + +import androidx.compose.foundation.background +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel +import com.tangem.features.swap.v2.impl.amount.ui.SwapAmountContent +import com.tangem.features.swap.v2.impl.common.SwapNavigationModelCallback +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class SwapAmountComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: SwapAmountComponentParams.AmountParams, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SwapAmountModel = getOrCreateModel(params = params) + + fun updateState(amountUM: SwapAmountUM) = model.updateState(amountUM) + + @Composable + override fun Content(modifier: Modifier) { + val amountUM by model.uiState.collectAsStateWithLifecycle() + + SwapAmountContent( + amountUM = amountUM, + modifier = Modifier.background(TangemTheme.colors.background.tertiary), + clickIntents = model, + ) + } + + interface ModelCallback : SwapNavigationModelCallback { + fun onAmountResult(amountUM: SwapAmountUM) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt index d7a15f42f7..b482191a87 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt @@ -30,8 +30,6 @@ internal sealed class SwapAmountComponentParams { val secondaryCryptoCurrency: CryptoCurrency?, val callback: SwapAmountComponent.ModelCallback, val currentRoute: Flow, - val onBackClick: () -> Unit, - val onNextClick: () -> Unit, ) : SwapAmountComponentParams() data class AmountBlockParams( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/di/SwapAmountModule.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/di/SwapAmountModule.kt new file mode 100644 index 0000000000..c80a8ee734 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/di/SwapAmountModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.swap.v2.impl.amount.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@InstallIn(ModelComponent::class) +@Module +internal interface SwapAmountModule { + + @Binds + @IntoMap + @ClassKey(SwapAmountModel::class) + fun provideSwapAmountModel(impl: SwapAmountModel): Model +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 9d188d6cb3..a7610c26f1 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -477,7 +477,7 @@ internal class SwapAmountModel @Inject constructor( // ), // ) // } - params.onBackClick() + params.callback.onBackClick() }, primaryButton = NavigationButton( textReference = if (route.isEditMode) { @@ -488,7 +488,7 @@ internal class SwapAmountModel @Inject constructor( isEnabled = state.isPrimaryButtonEnabled, onClick = { saveResult() - params.onNextClick() + params.callback.onNextClick() }, ), ), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt new file mode 100644 index 0000000000..6db113e583 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -0,0 +1,73 @@ +package com.tangem.features.swap.v2.impl.amount.ui.preview + +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import kotlinx.collections.immutable.persistentListOf + +internal data object SwapAmountContentPreview { + + val emptyState = SwapAmountUM.Content( + isPrimaryButtonEnabled = false, + primaryAmount = SwapAmountFieldUM.Empty( + amountType = SwapAmountType.From, + ), + secondaryAmount = SwapAmountFieldUM.Empty( + amountType = SwapAmountType.To, + ), + swapDirection = SwapDirection.Direct, + selectedAmountType = SwapAmountType.From, + swapCurrencies = SwapCurrencies.EMPTY, + swapQuotes = persistentListOf(), + selectedQuote = SwapQuoteUM.Empty, + primaryCryptoCurrencyStatus = null, + secondaryCryptoCurrencyStatus = null, + swapRateType = ExpressRateType.Float, + appCurrency = AppCurrency.Default, + ) + + val defaultState = SwapAmountUM.Content( + primaryAmount = SwapAmountFieldUM.Content( + amountType = SwapAmountType.From, + amountField = AmountStatePreviewData.amountState.copy( + availableBalance = stringReference("Balance: 100 BTC"), + ), + title = stringReference("Tether"), + subtitle = stringReference("Balance: 100 BTC"), + priceImpact = null, + isClickEnabled = false, + subtitleEllipsis = TextEllipsis.OffsetEnd(3), + ), + secondaryAmount = SwapAmountFieldUM.Content( + amountType = SwapAmountType.To, + amountField = AmountStatePreviewData.amountState.copy( + title = stringReference("Amount to receive"), + availableBalance = TextReference.EMPTY, + ), + title = stringReference("Shiba Inu"), + priceImpact = stringReference("(-10%)"), + subtitle = TextReference.EMPTY, + isClickEnabled = false, + subtitleEllipsis = TextEllipsis.OffsetEnd(3), + ), + appCurrency = AppCurrency.Default, + swapDirection = SwapDirection.Direct, + selectedAmountType = SwapAmountType.From, + swapCurrencies = SwapCurrencies.EMPTY, + swapQuotes = persistentListOf(), + selectedQuote = SwapQuoteUM.Empty, + primaryCryptoCurrencyStatus = null, + secondaryCryptoCurrencyStatus = null, + swapRateType = ExpressRateType.Float, + isPrimaryButtonEnabled = true, + ) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapNavigationModelCallback.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapNavigationModelCallback.kt index 2dfd5a4775..1f0c84d635 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapNavigationModelCallback.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapNavigationModelCallback.kt @@ -4,4 +4,6 @@ import com.tangem.features.swap.v2.impl.common.entity.NavigationUM internal interface SwapNavigationModelCallback { fun onNavigationResult(navigationUM: NavigationUM) + fun onBackClick() + fun onNextClick() } \ No newline at end of file From 1c53e890af59579bdbf1c6441abc62979abbe1b4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Jun 2025 16:00:50 +0300 Subject: [PATCH 72/99] Updated on 2026-08-14 --- tangem-android-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tangem-android-tools b/tangem-android-tools index 034ad04fdf..bc4cd43085 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 034ad04fdf0810e338cb8898f50554874cec96d7 +Subproject commit bc4cd430853ca794614b8d5163c9b28b9ca26112 From c1b16658405f49ea681f5e8ba35e37a6196544cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Jun 2025 16:02:18 +0300 Subject: [PATCH 73/99] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d33eab3225..e18a2ec5cf 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1093" +tangemBlockchainSdk = "develop-1097" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-484" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From ac4179be91fb38722ba59e4f5c90c40cd28a6dc3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Jun 2025 18:08:39 +0500 Subject: [PATCH 74/99] Updated on 2026-08-14 --- .../ui/CreateWalletSelectionContent.kt | 3 +- .../hotwallet/AddExistingWalletComponent.kt | 8 + .../root/AddExistingWalletModel.kt | 25 ++ .../root/DefaultAddExistingWalletComponent.kt | 72 ++++++ .../root/entity/AddExistingWalletUM.kt | 5 + .../routing/AddExistingWalletChildFactory.kt | 25 ++ .../root/routing/AddExistingWalletRoute.kt | 10 + .../root/ui/AddExistingWalletContent.kt | 37 +++ .../start/AddExistingWalletStartComponent.kt | 37 +++ .../start/AddExistingWalletStartModel.kt | 29 +++ .../start/entity/AddExistingWalletStartUM.kt | 8 + .../start/ui/AddExistingWalletStartContent.kt | 214 ++++++++++++++++++ 12 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/AddExistingWalletComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/entity/AddExistingWalletUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 4c66af5a94..e5ffea8e68 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -157,9 +157,10 @@ private fun WalletBlock( .padding(16.dp) .clickable(onClick = onClick), ) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row { Text( modifier = Modifier + .weight(1f) .padding(end = 8.dp), text = title, style = TangemTheme.typography.subtitle1, diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/AddExistingWalletComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/AddExistingWalletComponent.kt new file mode 100644 index 0000000000..f9bf2edf1e --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/AddExistingWalletComponent.kt @@ -0,0 +1,8 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface AddExistingWalletComponent : ComposableContentComponent { + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt new file mode 100644 index 0000000000..abbb917a03 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt @@ -0,0 +1,25 @@ +package com.tangem.features.hotwallet.addexistingwallet.root + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject + +@ModelScoped +internal class AddExistingWalletModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks() + + inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks { + override fun onBackClick() { + // TODO implement in [REDACTED_TASK_KEY] + } + + override fun onImportPhraseClick() { + // TODO implement in [REDACTED_TASK_KEY] + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt new file mode 100644 index 0000000000..4ef6395750 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt @@ -0,0 +1,72 @@ +package com.tangem.features.hotwallet.addexistingwallet.root + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.AddExistingWalletComponent +import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletChildFactory +import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.addexistingwallet.root.ui.AddExistingWalletContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultAddExistingWalletComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: Unit, + addExistingWalletChildFactory: AddExistingWalletChildFactory, +) : AddExistingWalletComponent, AppComponentContext by appComponentContext { + + private val model: AddExistingWalletModel = getOrCreateModel(params) + + private val stackNavigation = StackNavigation() + private val startRoute = AddExistingWalletRoute.Start + + private val innerStack = childStack( + key = "addExistingWalletInnerStack", + source = stackNavigation, + serializer = null, + initialConfiguration = startRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + addExistingWalletChildFactory.createChild( + route = configuration, + childContext = childByContext(factoryContext), + model = model, + ) + }, + ) + + @Composable + override fun Content(modifier: Modifier) { + val stackState by innerStack.subscribeAsState() + + BackHandler(onBack = ::onChildBack) + AddExistingWalletContent( + stackState = stackState, + ) + } + + private fun onChildBack() { + val isEmptyStack = innerStack.value.backStack.isEmpty() + + if (isEmptyStack) { + router.pop() + } else { + stackNavigation.pop() + } + } + + @AssistedFactory + interface Factory : AddExistingWalletComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultAddExistingWalletComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/entity/AddExistingWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/entity/AddExistingWalletUM.kt new file mode 100644 index 0000000000..8030308588 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/entity/AddExistingWalletUM.kt @@ -0,0 +1,5 @@ +package com.tangem.features.hotwallet.addexistingwallet.root.entity + +internal data class AddExistingWalletUM( + val onBackClick: () -> Unit, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt new file mode 100644 index 0000000000..b9079208c6 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt @@ -0,0 +1,25 @@ +package com.tangem.features.hotwallet.addexistingwallet.root.routing + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel +import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent +import javax.inject.Inject + +internal class AddExistingWalletChildFactory @Inject constructor() { + + fun createChild( + route: AddExistingWalletRoute, + childContext: AppComponentContext, + model: AddExistingWalletModel, + ): ComposableContentComponent { + return when (route) { + is AddExistingWalletRoute.Start -> AddExistingWalletStartComponent( + context = childContext, + params = AddExistingWalletStartComponent.Params( + callbacks = model.addExistingWalletStartModelCallbacks, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt new file mode 100644 index 0000000000..56ca941809 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt @@ -0,0 +1,10 @@ +package com.tangem.features.hotwallet.addexistingwallet.root.routing + +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +internal sealed class AddExistingWalletRoute : Route { + + @Serializable + object Start : AddExistingWalletRoute() +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt new file mode 100644 index 0000000000..9d3367cbc7 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.hotwallet.addexistingwallet.root.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute + +@Composable +internal fun AddExistingWalletContent(stackState: ChildStack) { + Column( + modifier = Modifier.Companion + .background(color = TangemTheme.colors.background.primary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.Companion.CenterHorizontally, + ) { + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.weight(1f), + ) { + it.instance.Content(Modifier.weight(1f)) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartComponent.kt new file mode 100644 index 0000000000..42e01eaf3b --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.hotwallet.addexistingwallet.start + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.addexistingwallet.start.ui.AddExistingWalletStartContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class AddExistingWalletStartComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { + private val model: AddExistingWalletStartModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + AddExistingWalletStartContent( + state = state, + modifier = modifier, + ) + } + + interface ModelCallbacks { + fun onBackClick() + fun onImportPhraseClick() + } + + data class Params( + val callbacks: ModelCallbacks, + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt new file mode 100644 index 0000000000..58ea450355 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt @@ -0,0 +1,29 @@ +package com.tangem.features.hotwallet.addexistingwallet.start + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class AddExistingWalletStartModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params: AddExistingWalletStartComponent.Params = paramsContainer.require() + + internal val uiState: StateFlow + field = MutableStateFlow( + AddExistingWalletStartUM( + onBackClick = params.callbacks::onBackClick, + onImportPhraseClick = params.callbacks::onImportPhraseClick, + onScanCardClick = { /* [REDACTED_TODO_COMMENT] */ }, + onBuyCardClick = { /* [REDACTED_TODO_COMMENT] */ }, + ), + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt new file mode 100644 index 0000000000..f898f9c1b7 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.hotwallet.addexistingwallet.start.entity + +internal data class AddExistingWalletStartUM( + val onBackClick: () -> Unit, + val onImportPhraseClick: () -> Unit, + val onScanCardClick: () -> Unit, + val onBuyCardClick: () -> Unit, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt new file mode 100644 index 0000000000..a0a2ea6abe --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt @@ -0,0 +1,214 @@ +package com.tangem.features.hotwallet.addexistingwallet.start.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.extensions.conditional +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modifier: Modifier = Modifier) { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .systemBarsPadding(), + ) { + TangemTopAppBar( + modifier = modifier + .statusBarsPadding(), + startButton = TopAppBarButtonUM.Back(state.onBackClick), + title = "Add existing wallet", + ) + Column( + modifier = Modifier + .weight(1f) + .padding( + start = 16.dp, + top = 24.dp, + end = 16.dp, + ), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + // TODO [REDACTED_TASK_KEY] update and extract this string + text = "Import wallet", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + OptionBlock( + modifier = Modifier + .padding(top = 24.dp), + // TODO [REDACTED_TASK_KEY] update and extract this string + title = "Import a recovery phrase", + // TODO [REDACTED_TASK_KEY] update and extract this string + description = "Your 12-word seed phrase to recover your wallet", + badge = null, + onClick = state.onImportPhraseClick, + enabled = true, + ) + OptionBlock( + // TODO [REDACTED_TASK_KEY] update and extract this string + title = "Scan a Tangem Wallet", + // TODO [REDACTED_TASK_KEY] update and extract this string + description = "Physical cards that securely store your crypto offline.", + badge = null, + onClick = state.onScanCardClick, + enabled = true, + ) + OptionBlock( + // TODO [REDACTED_TASK_KEY] update and extract this string + title = "Import from Google Drive", + // TODO [REDACTED_TASK_KEY] update and extract this string + description = "Recover an existing wallet stored in your Google Drive backup", + badge = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.field.focused, + shape = TangemTheme.shapes.roundedCorners8, + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + // TODO [REDACTED_TASK_KEY] update and extract this string + text = "Coming Soon", + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + } + }, + onClick = null, + enabled = false, + ) + } + BuyTangemWalletBlock( + onScanClick = state.onBuyCardClick, + ) + } +} + +@Composable +private fun OptionBlock( + title: String, + description: String, + badge: (@Composable () -> Unit)?, + onClick: (() -> Unit)?, + enabled: Boolean, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(top = 8.dp) + .background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(16.dp) + .conditional(onClick != null) { + onClick?.let { clickableSingle(onClick = it) } ?: Modifier + }, + ) { + Row { + Text( + modifier = Modifier + .weight(1f) + .padding(end = 8.dp), + text = title, + style = TangemTheme.typography.subtitle1, + color = if (enabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.secondary + }, + ) + badge?.invoke() + } + Text( + modifier = Modifier + .padding(top = 4.dp), + text = description, + style = TangemTheme.typography.body2, + color = if (enabled) { + TangemTheme.colors.text.tertiary + } else { + TangemTheme.colors.text.disabled + }, + ) + } +} + +@Composable +private fun BuyTangemWalletBlock(onScanClick: () -> Unit, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(16.dp) + .background( + color = TangemTheme.colors.field.primary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding( + horizontal = 20.dp, + vertical = 16.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier + .weight(1f) + .padding(end = 16.dp), + // TODO [REDACTED_TASK_KEY] update and extract this string + text = "Want to purchase a Tangem Wallet?", + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + ) + PrimaryButton( + modifier = Modifier + .wrapContentWidth(), + // TODO [REDACTED_TASK_KEY] update and extract this string + text = "Buy Now", + onClick = onScanClick, + size = TangemButtonSize.RoundedAction, + colors = TangemButtonsDefaults.secondaryButtonColors, + enabled = true, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewCreateWalletContent() { + TangemThemePreview { + AddExistingWalletStartContent( + state = AddExistingWalletStartUM( + onBackClick = {}, + onImportPhraseClick = {}, + onScanCardClick = {}, + onBuyCardClick = {}, + ), + ) + } +} \ No newline at end of file From b3bf232b1226ea74b197a146bf64342eab05e215 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Jun 2025 16:13:40 +0300 Subject: [PATCH 75/99] Updated on 2026-08-14 --- .../transaction/model/WcSendTransactionModel.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 4cde43d227..20b19996a3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -14,7 +14,6 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.network.Network import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase @@ -132,11 +131,9 @@ internal class WcSendTransactionModel @Inject constructor( network: Network, ): Either { return getNetworkCoinUseCase.invokeSync( - userWalletId = userWallet.walletId, + userWallet = userWallet, networkId = network.id, derivationPath = network.derivationPath, - isSingleWalletWithTokens = userWallet is UserWallet.Cold && - userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), ) } From 983b9b253d1ab9842620253caeea9674f427696e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Jun 2025 13:43:33 +0400 Subject: [PATCH 76/99] Updated on 2026-08-14 --- .../data/staking/DefaultStakingRepository.kt | 4 +- .../multi/DefaultMultiYieldBalanceFetcher.kt | 2 +- .../DefaultSingleYieldBalanceProducer.kt | 14 +- .../data/staking/utils/StakingIdFactory.kt | 16 +- .../DefaultMultiYieldBalanceFetcherTest.kt | 38 ++-- .../DefaultSingleYieldBalanceProducerTest.kt | 16 +- .../staking/utils/StakingIdFactoryTest.kt | 188 ++++++++++++++++++ 7 files changed, 224 insertions(+), 54 deletions(-) create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index d6ce77cad7..4c145ce44c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -502,7 +502,7 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): YieldBalance { - val stakingId = stakingIdFactory.createForDefault( + val stakingId = stakingIdFactory.create( userWalletId = userWalletId, currencyId = cryptoCurrency.id, network = cryptoCurrency.network, @@ -639,7 +639,7 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrencies: List, ): YieldBalanceList { - val stakingIds = cryptoCurrencies.flatMap { + val stakingIds = cryptoCurrencies.mapNotNull { stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt index 8c21c90072..ce8be6ef59 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt @@ -93,7 +93,7 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( private suspend fun getStakingIds(params: MultiYieldBalanceFetcher.Params) = either { val stakingIds = catch( block = { - params.currencyIdWithNetworkMap.flatMapTo(hashSetOf()) { (currencyId, network) -> + params.currencyIdWithNetworkMap.mapNotNullTo(hashSetOf()) { (currencyId, network) -> stakingIdFactory.create( userWalletId = params.userWalletId, currencyId = currencyId, diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt index 5b6c543361..887db9af5f 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt @@ -39,26 +39,24 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( ) } - private var stakingIds: Set? = null + private var stakingId: StakingID? = null override fun produce(): Flow { return multiYieldBalanceSupplier( params = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId), ) .mapNotNull { balances -> - val currentStakingIds = getStakingIds().ifEmpty { - return@mapNotNull YieldBalance.Unsupported - } + val currentStakingId = getStakingId() ?: return@mapNotNull YieldBalance.Unsupported - balances.firstOrNull { currentStakingIds.contains(it.getStakingId()) } + balances.firstOrNull { it.getStakingId() == currentStakingId } ?: YieldBalance.Unsupported } .distinctUntilChanged() .flowOn(dispatchers.default) } - private suspend fun getStakingIds(): Set { - val saved = stakingIds + private suspend fun getStakingId(): StakingID? { + val saved = stakingId if (saved != null) return saved @@ -67,7 +65,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( currencyId = params.currencyId, network = params.network, ) - .also { stakingIds = it } + .also { stakingId = it } } @AssistedFactory diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt index 4936f9f866..d1b760b601 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt @@ -21,21 +21,7 @@ internal class StakingIdFactory @Inject constructor( private val walletManagersFacade: WalletManagersFacade, ) { - suspend fun create(userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network): Set { - val addresses = walletManagersFacade.getAddresses(userWalletId = userWalletId, network = network) - - val integrationId = createIntegrationId(currencyId) ?: return emptySet() - - return addresses.mapTo(hashSetOf()) { address -> - StakingID(integrationId = integrationId, address = address.value) - } - } - - suspend fun createForDefault( - userWalletId: UserWalletId, - currencyId: CryptoCurrency.ID, - network: Network, - ): StakingID? { + suspend fun create(userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network): StakingID? { val address = walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) val integrationId = createIntegrationId(currencyId) diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt index d1ad78ad56..ea30e3f16c 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt @@ -60,8 +60,8 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) @@ -102,8 +102,8 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(tonId)) @@ -152,7 +152,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { - stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + stakingIdFactory.create(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) @@ -183,7 +183,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { - stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + stakingIdFactory.create(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) @@ -199,15 +199,15 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if stakingIdFactory returns empty list`() = runTest { + fun `fetch yields balances failure if stakingIdFactory returns null`() = runTest { // Arrange val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns emptySet() - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns emptySet() + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns null + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns null // Actual val actual = fetcher.invoke(params) @@ -242,8 +242,8 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs @@ -281,8 +281,8 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList() coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs @@ -320,8 +320,8 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf( @@ -364,8 +364,8 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1"))) @@ -411,8 +411,8 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt index 1cc5bdd3de..b05ad2e885 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt @@ -54,7 +54,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns expected - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId val actual = producer.produce() @@ -74,7 +74,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns expected - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId val actual = producer.produceWithFallback() @@ -109,7 +109,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns expected - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId val actual = producer.produceWithFallback() @@ -168,7 +168,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val fallbackStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = null) Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus)) - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId innerFlow.emit(value = true) @@ -188,7 +188,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns yieldBalancesFlow - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId val actual = producer.produce() @@ -203,14 +203,14 @@ internal class DefaultSingleYieldBalanceProducerTest { } @Test - fun `test if wallet manager facade returns empty set`() = runTest { + fun `test if wallet manager facade returns null`() = runTest { val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() val yieldBalancesFlow = flowOf(setOf(balance)) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns yieldBalancesFlow - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns emptySet() + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns null val actual = producer.produce() @@ -235,7 +235,5 @@ internal class DefaultSingleYieldBalanceProducerTest { integrationId = "solana-sol-native-multivalidator-staking", address = "0x1", ) - - val stakingIds = setOf(tonId) } } \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt new file mode 100644 index 0000000000..f455fd431b --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt @@ -0,0 +1,188 @@ +package com.tangem.data.staking.utils + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class StakingIdFactoryTest { + + private val walletManagersFacade: WalletManagersFacade = mockk() + private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade) + + @BeforeEach + fun resetMocks() { + clearMocks(walletManagersFacade) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreateIntegrationId { + + @ParameterizedTest + @ProvideTestModels + fun createIntegrationId(model: CreateIntegrationIdModel) { + // Act + val actual = factory.createIntegrationId(currencyId = model.currencyId) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + CreateIntegrationIdModel( + currencyId = createCurrencyId(blockchain = Blockchain.TON), + expected = "ton-ton-chorus-one-pools-staking", + ), + CreateIntegrationIdModel( + currencyId = createCurrencyId(blockchain = Blockchain.Solana), + expected = "solana-sol-native-multivalidator-staking", + ), + CreateIntegrationIdModel( + currencyId = createCurrencyId(blockchain = Blockchain.Cosmos), + expected = "cosmos-atom-native-staking", + ), + CreateIntegrationIdModel( + currencyId = createCurrencyId(blockchain = Blockchain.Tron), + expected = "tron-trx-native-staking", + ), + CreateIntegrationIdModel( + currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-token⚓"), + expected = "ethereum-matic-native-staking", + ), + CreateIntegrationIdModel( + currencyId = createCurrencyId(blockchain = Blockchain.BSC), + expected = "bsc-bnb-native-staking", + ), + CreateIntegrationIdModel( + currencyId = createCurrencyId(blockchain = Blockchain.Cardano), + expected = "cardano-ada-native-staking", + ), + CreateIntegrationIdModel( + currencyId = createCurrencyId(blockchain = Blockchain.Bitcoin), + expected = null, + ), + ) + } + + data class CreateIntegrationIdModel(val currencyId: CryptoCurrency.ID, val expected: String?) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Create { + + private val defaultAddress = "address" + + @Test + fun `create returns null if address is null`() = runTest { + // Arrange + val userWalletId = UserWalletId(stringValue = "011") + val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON) + + coEvery { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = currency.network) + } returns null + + // Act + val actual = factory.create( + userWalletId = userWalletId, + currencyId = currency.id, + network = currency.network, + ) + + // Assert + val expected = null + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = currency.network) + } + } + + @ParameterizedTest + @ProvideTestModels + fun create(model: CreateModel) = runTest { + // Arrange + val userWalletId = UserWalletId(stringValue = "011") + val network = MockCryptoCurrencyFactory().createCoin(Blockchain.TON).network + + coEvery { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + } returns defaultAddress + + // Act + val actual = factory.create(userWalletId = userWalletId, currencyId = model.currencyId, network = network) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + + coVerify(exactly = 1) { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + } + } + + private fun provideTestModels() = listOf( + CreateModel( + currencyId = createCurrencyId(blockchain = Blockchain.TON), + expected = createStakingId(integrationId = "ton-ton-chorus-one-pools-staking"), + ), + CreateModel( + currencyId = createCurrencyId(blockchain = Blockchain.Solana), + expected = createStakingId(integrationId = "solana-sol-native-multivalidator-staking"), + ), + CreateModel( + currencyId = createCurrencyId(blockchain = Blockchain.Cosmos), + expected = createStakingId(integrationId = "cosmos-atom-native-staking"), + ), + CreateModel( + currencyId = createCurrencyId(blockchain = Blockchain.Tron), + expected = createStakingId(integrationId = "tron-trx-native-staking"), + ), + CreateModel( + currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-token⚓"), + expected = createStakingId(integrationId = "ethereum-matic-native-staking"), + ), + CreateModel( + currencyId = createCurrencyId(blockchain = Blockchain.BSC), + expected = createStakingId(integrationId = "bsc-bnb-native-staking"), + ), + CreateModel( + currencyId = createCurrencyId(blockchain = Blockchain.Cardano), + expected = createStakingId(integrationId = "cardano-ada-native-staking"), + ), + CreateModel( + currencyId = createCurrencyId(blockchain = Blockchain.Bitcoin), + expected = null, + ), + ) + + private fun createStakingId(integrationId: String): StakingID { + return StakingID(integrationId = integrationId, address = defaultAddress) + } + } + + data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: StakingID?) + + private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID { + return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}⟩${blockchain.toCoinId()}⚓") + } +} \ No newline at end of file From 0ab1c7507a37e707d5001038743951b1a47ca96e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Jun 2025 18:48:33 +0400 Subject: [PATCH 77/99] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 3 + .../java/com/tangem/tap/TangemApplication.kt | 7 +- .../domain/scanCard/LegacyScanProcessor.kt | 11 +- .../domain/tasks/product/DerivationsFinder.kt | 12 +- .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../local/preferences/PreferencesKeys.kt | 2 - .../token/DefaultUserTokensResponseStore.kt | 26 +++- .../local/token/UserTokensResponseStore.kt | 5 + .../data/common/currency/UserTokensSaver.kt | 14 +- .../tangem/data/common/di/DataCommonModule.kt | 5 +- .../common/currency/UserTokensSaverTest.kt | 136 ++++++++---------- .../DefaultCustomTokensRepository.kt | 17 +-- .../DefaultManageTokensRepository.kt | 10 +- .../managetokens/di/ManageTokensDataModule.kt | 10 +- .../data/onramp/DefaultHotCryptoRepository.kt | 6 +- .../tangem/data/onramp/di/OnrampDataModule.kt | 9 +- .../tangem/data/tokens/di/TokensDataModule.kt | 5 +- .../repository/DefaultCurrenciesRepository.kt | 101 ++++++------- 18 files changed, 188 insertions(+), 193 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index a005dd97cb..aaeed5140b 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -21,6 +21,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -145,4 +146,6 @@ interface ApplicationEntryPoint { fun getColdUserWalletBuilderFactory(): ColdUserWalletBuilder.Factory fun getApiConfigsManager(): ApiConfigsManager + + fun getUserTokensResponseStore(): UserTokensResponseStore } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 2be2d0310a..621f3eed14 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -38,6 +38,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.utils.NetworkLogsSaveInterceptor import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase @@ -232,6 +233,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val apiConfigsManager: ApiConfigsManager get() = entryPoint.getApiConfigsManager() + private val userTokensResponseStore: UserTokensResponseStore + get() = entryPoint.getUserTokensResponseStore() + // endregion private val appScope = MainScope() @@ -320,7 +324,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat } derivationsFinder = DerivationsFinder( - appPreferencesStore = appPreferencesStore, + userTokensResponseStore = userTokensResponseStore, dispatchers = dispatchers, ) @@ -370,6 +374,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat uiMessageSender = uiMessageSender, onlineCardVerifier = onlineCardVerifier, coldUserWalletBuilderFactory = coldUserWalletBuilderFactory, + userTokensResponseStore = userTokensResponseStore, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 29665865af..5ce4d8f38e 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -18,9 +18,6 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.message.dialog.Dialogs -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.feedback.models.FeedbackEmailType @@ -258,10 +255,10 @@ internal class LegacyScanProcessor @Inject constructor( onSuccess() return } - val appPrefStoreStore = store.inject(DaggerGraphState::appPreferencesStore) - val tokens = appPrefStoreStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), - ) + + val userTokensResponseStore = store.inject(DaggerGraphState::userTokensResponseStore) + val tokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + if (scanResponse.card.isAccessCodeSet && tokens == null) { store.dispatchDialogShow( AppDialog.WalletAlreadyWasUsedDialog( diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt index 6fcad66476..6fde516a55 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt @@ -5,10 +5,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.models.scan.CardDTO @@ -25,7 +22,7 @@ internal data class BlockchainToDerive( // FIXME: May be move to DI, currently unnecessary internal class DerivationsFinder( - private val appPreferencesStore: AppPreferencesStore, + private val userTokensResponseStore: UserTokensResponseStore, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -67,10 +64,7 @@ internal class DerivationsFinder( } private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet { - val responseTokens = appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), - ) - ?.tokens + val responseTokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)?.tokens ?: return hashSetOf() return responseTokens.asSequence() diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 526d65968d..e7817d2245 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository @@ -79,4 +80,5 @@ data class DaggerGraphState( val onlineCardVerifier: OnlineCardVerifier? = null, val cardArworksProvider: CardArtworksProvider? = null, val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null, + val userTokensResponseStore: UserTokensResponseStore? = null, ) : StateType \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 5e4f9577d1..874a358083 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -151,8 +151,6 @@ object PreferencesKeys { fun getShouldShowInitialPermissionScreen(permission: String) = booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission") // endregion - - fun getUserTokensKey(userWalletId: String) = stringPreferencesKey(name = "user_tokens_$userWalletId") } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt index c04690b7ce..6e25e1c446 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt @@ -1,10 +1,13 @@ package com.tangem.datasource.local.token +import androidx.datastore.preferences.core.stringPreferencesKey import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow /** * Default implementation of [UserTokensResponseStore] @@ -17,9 +20,24 @@ internal class DefaultUserTokensResponseStore( private val appPreferencesStore: AppPreferencesStore, ) : UserTokensResponseStore { - override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? { - return appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + override fun get(userWalletId: UserWalletId): Flow { + return appPreferencesStore.getObject( + key = createPreferencesKey(userWalletId = userWalletId.stringValue), ) } + + override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = createPreferencesKey(userWalletId = userWalletId.stringValue), + ) + } + + override suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse) { + appPreferencesStore.storeObject( + key = createPreferencesKey(userWalletId = userWalletId.stringValue), + value = response, + ) + } + + private fun createPreferencesKey(userWalletId: String) = stringPreferencesKey(name = "user_tokens_$userWalletId") } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt index df802c591c..6fd03be9a2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow /** * Store of [UserTokensResponse] @@ -10,6 +11,10 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface UserTokensResponseStore { + fun get(userWalletId: UserWalletId): Flow + /** Get [UserTokensResponse] synchronously by [userWalletId] or null */ suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? + + suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse) } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index de9781bffc..487b96f427 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -4,17 +4,15 @@ import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.tokens.UserTokensBackwardCompatibility import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.storeObject +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber -class UserTokensSaver constructor( +class UserTokensSaver( private val tangemTechApi: TangemTechApi, - private val appPreferencesStore: AppPreferencesStore, + private val userTokensResponseStore: UserTokensResponseStore, private val dispatchers: CoroutineDispatcherProvider, private val userTokensResponseAddressesEnricher: UserTokensResponseAddressesEnricher, ) { @@ -31,10 +29,8 @@ class UserTokensSaver constructor( } else { compatibleUserTokensResponse } - appPreferencesStore.storeObject( - key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), - value = enrichedUserTokensResponse, - ) + + userTokensResponseStore.store(userWalletId = userWalletId, response = enrichedUserTokensResponse) } suspend fun storeAndPush(userWalletId: UserWalletId, response: UserTokensResponse) { diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index 6ceeb800e9..c8c834d85c 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -8,7 +8,6 @@ import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.quote.DefaultQuotesFetcher import com.tangem.data.common.quote.QuotesFetcher import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.demo.DemoConfig @@ -61,13 +60,13 @@ internal object DataCommonModule { @Singleton fun provideUserTokensSaver( tangemTechApi: TangemTechApi, - appPreferencesStore: AppPreferencesStore, + userTokensResponseStore: UserTokensResponseStore, dispatchers: CoroutineDispatcherProvider, enricher: UserTokensResponseAddressesEnricher, ): UserTokensSaver { return UserTokensSaver( tangemTechApi = tangemTechApi, - appPreferencesStore = appPreferencesStore, + userTokensResponseStore = userTokensResponseStore, dispatchers = dispatchers, userTokensResponseAddressesEnricher = enricher, ) diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index 8a7e2e922a..caeb9bd3f9 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -1,51 +1,35 @@ package com.tangem.data.common.currency -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class UserTokensSaverTest { private val tangemTechApi: TangemTechApi = mockk() - private val dataStore: DataStore = mockk(relaxed = true) - private val appPreferenceStore = AppPreferencesStore( - moshi = Moshi.Builder().build(), + private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxed = true) + private val enricher: UserTokensResponseAddressesEnricher = mockk() + + private val userTokensSaver: UserTokensSaver = UserTokensSaver( + tangemTechApi = tangemTechApi, + userTokensResponseStore = userTokensResponseStore, + userTokensResponseAddressesEnricher = enricher, dispatchers = TestingCoroutineDispatcherProvider(), - preferencesDataStore = dataStore, ) - private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider() - private val userTokensResponseAddressesEnricher: UserTokensResponseAddressesEnricher = mockk() - private lateinit var userTokensSaver: UserTokensSaver - - @Before - fun setup() { - coEvery { dataStore.data } returns flowOf(mockk(relaxed = true)) - userTokensSaver = UserTokensSaver( - tangemTechApi = tangemTechApi, - appPreferencesStore = appPreferenceStore, - dispatchers = dispatchers, - userTokensResponseAddressesEnricher = userTokensResponseAddressesEnricher, - ) - } - - @After - fun tearDown() { - clearAllMocks() + @BeforeEach + fun resetMocks() { + clearMocks(tangemTechApi, userTokensResponseStore, enricher) } @Test @@ -58,57 +42,30 @@ class UserTokensSaverTest { sort = UserTokensResponse.SortType.BALANCE, tokens = emptyList(), ) + val enrichedResponse = UserTokensResponse( version = 0, group = UserTokensResponse.GroupType.NETWORK, - sort = UserTokensResponse.SortType.BALANCE, + sort = UserTokensResponse.SortType.MANUAL, tokens = emptyList(), ) - coEvery { userTokensResponseAddressesEnricher(userWalletId, response) } returns enrichedResponse - coEvery { dataStore.updateData(any()) } returns mockk(relaxed = true) + + coEvery { enricher(userWalletId, response) } returns enrichedResponse // WHEN userTokensSaver.store(userWalletId, response) // THEN - coVerify { - dataStore.updateData(any()) + coVerifyOrder { + enricher(userWalletId, response) + userTokensResponseStore.store(userWalletId, enrichedResponse) } - coVerify(exactly = 0) { + + coVerify(inverse = true) { tangemTechApi.saveUserTokens(any(), any()) } } - @Test - fun `GIVEN user wallet id and response WHEN storeAndPush THEN should store and push enriched response`() = runTest { - // GIVEN - val userWalletId = UserWalletId("1234567890abcdef") - val response = UserTokensResponse( - version = 0, - group = UserTokensResponse.GroupType.NETWORK, - sort = UserTokensResponse.SortType.BALANCE, - tokens = emptyList(), - ) - val enrichedResponse = UserTokensResponse( - version = 0, - group = UserTokensResponse.GroupType.NETWORK, - sort = UserTokensResponse.SortType.BALANCE, - tokens = emptyList(), - ) - coEvery { userTokensResponseAddressesEnricher(userWalletId, response) } returns enrichedResponse - coEvery { dataStore.updateData(any()) } returns mockk(relaxed = true) - coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns ApiResponse.Success(Unit) - - // WHEN - userTokensSaver.storeAndPush(userWalletId, response) - - // THEN - coVerify { - dataStore.updateData(any()) - tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) - } - } - @Test fun `GIVEN user wallet id and response WHEN push AND api call fails THEN should log error and call onFailSend`() = runTest { @@ -123,14 +80,13 @@ class UserTokensSaverTest { val enrichedResponse = UserTokensResponse( version = 0, group = UserTokensResponse.GroupType.NETWORK, - sort = UserTokensResponse.SortType.BALANCE, + sort = UserTokensResponse.SortType.MANUAL, tokens = emptyList(), ) val error = ApiResponseError.UnknownException(Exception("API Error")) var onFailSendCalled = false - coEvery { userTokensResponseAddressesEnricher(userWalletId, response) } returns enrichedResponse - coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns - ApiResponse.Error(error) as ApiResponse + coEvery { enricher(userWalletId, response) } returns enrichedResponse + coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse // WHEN userTokensSaver.push( @@ -140,12 +96,42 @@ class UserTokensSaverTest { ) // THEN - coVerify { + coVerifyOrder { + enricher(userWalletId, response) tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) } - coVerify(exactly = 0) { - dataStore.updateData(any()) - } + assert(onFailSendCalled) { "onFailSend callback should be called when API call fails" } } + + @Test + fun `GIVEN user wallet id and response WHEN storeAndPush THEN should store and push enriched response`() = runTest { + // GIVEN + val userWalletId = UserWalletId("1234567890abcdef") + val response = UserTokensResponse( + version = 0, + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = emptyList(), + ) + val enrichedResponse = UserTokensResponse( + version = 0, + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = emptyList(), + ) + coEvery { enricher(userWalletId, response) } returns enrichedResponse + coEvery { + tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + } returns ApiResponse.Success(Unit) + + // WHEN + userTokensSaver.storeAndPush(userWalletId, response) + + // THEN + coVerifyOrder { + enricher(userWalletId, response) + tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + } + } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 9064c8fb08..49054eb06e 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -12,10 +12,7 @@ import com.tangem.data.common.network.NetworkFactory import com.tangem.data.managetokens.utils.TokenAddressesConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.extensions.canHandleBlockchain import com.tangem.domain.common.extensions.supportedBlockchains @@ -35,7 +32,7 @@ import kotlinx.coroutines.withContext internal class DefaultCustomTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, - private val appPreferencesStore: AppPreferencesStore, + private val userTokensResponseStore: UserTokensResponseStore, private val walletManagersFacade: WalletManagersFacade, private val excludedBlockchains: ExcludedBlockchains, private val dispatchers: CoroutineDispatcherProvider, @@ -71,9 +68,8 @@ internal class DefaultCustomTokensRepository( contractAddress: String?, ): Boolean { return withContext(dispatchers.io) { - val storedCurrencies = appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), - ) + val storedCurrencies = userTokensResponseStore.getSyncOrNull(userWalletId) + requireNotNull(storedCurrencies) { "User tokens not found for user wallet [$userWalletId] while checking if currency is not added" } @@ -230,9 +226,8 @@ internal class DefaultCustomTokensRepository( contractAddress = currency.contractAddress, ) } - val storedCurrencies = appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), - ) + val storedCurrencies = userTokensResponseStore.getSyncOrNull(userWalletId) + requireNotNull(storedCurrencies) { "User tokens not found for user wallet [$userWalletId] while removing currency" } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 7b4aef4179..55f8660e21 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -17,9 +17,7 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.config.testnet.TestnetTokensStorage -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.extensions.canHandleBlockchain @@ -46,7 +44,7 @@ internal class DefaultManageTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, - private val appPreferencesStore: AppPreferencesStore, + private val userTokensResponseStore: UserTokensResponseStore, private val testnetTokensStorage: TestnetTokensStorage, private val excludedBlockchains: ExcludedBlockchains, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, @@ -251,9 +249,7 @@ internal class DefaultManageTokensRepository( } private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { - return appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(key.stringValue), - ) + return userTokensResponseStore.getSyncOrNull(userWalletId = key) } override suspend fun checkCurrencyUnsupportedState( diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 1e3cc8eafe..87943d1e88 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -9,7 +9,7 @@ import com.tangem.data.managetokens.DefaultManageTokensRepository import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.testnet.TestnetTokensStorage -import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -31,7 +31,7 @@ internal object ManageTokensDataModule { tangemTechApi: TangemTechApi, userWalletsStore: UserWalletsStore, manageTokensUpdateFetcher: ManageTokensUpdateFetcher, - appPreferencesStore: AppPreferencesStore, + userTokensResponseStore: UserTokensResponseStore, testnetTokensStorage: TestnetTokensStorage, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, @@ -42,7 +42,7 @@ internal object ManageTokensDataModule { tangemTechApi = tangemTechApi, userWalletsStore = userWalletsStore, manageTokensUpdateFetcher = manageTokensUpdateFetcher, - appPreferencesStore = appPreferencesStore, + userTokensResponseStore = userTokensResponseStore, testnetTokensStorage = testnetTokensStorage, excludedBlockchains = excludedBlockchains, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, @@ -56,7 +56,7 @@ internal object ManageTokensDataModule { fun provideCustomTokensRepository( tangemTechApi: TangemTechApi, userWalletsStore: UserWalletsStore, - appPreferencesStore: AppPreferencesStore, + userTokensResponseStore: UserTokensResponseStore, walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, @@ -66,7 +66,7 @@ internal object ManageTokensDataModule { return DefaultCustomTokensRepository( tangemTechApi = tangemTechApi, userWalletsStore = userWalletsStore, - appPreferencesStore = appPreferencesStore, + userTokensResponseStore = userTokensResponseStore, walletManagersFacade = walletManagersFacade, excludedBlockchains = excludedBlockchains, dispatchers = dispatchers, diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index e0d1ec226d..4df4a7a45c 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -18,6 +18,7 @@ import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.extensions.canHandleBlockchain import com.tangem.domain.common.extensions.canHandleToken @@ -58,6 +59,7 @@ internal class DefaultHotCryptoRepository( private val userWalletsStore: UserWalletsStore, private val tangemTechApi: TangemTechApi, private val appPreferencesStore: AppPreferencesStore, + private val userTokensResponseStore: UserTokensResponseStore, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, ) : HotCryptoRepository { @@ -103,9 +105,7 @@ internal class DefaultHotCryptoRepository( private fun getWalletsWithTokensFlow(): Flow>> { return userWalletsStore.userWallets.flatMapLatest { userWallets -> val flows = userWallets.map { userWallet -> - appPreferencesStore.getObject( - key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), - ) + userTokensResponseStore.get(userWalletId = userWallet.walletId) .map { userWallet to it?.tokens.orEmpty() } } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index 3ecf2df253..9adfb5cc75 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -8,7 +8,6 @@ import com.tangem.data.onramp.DefaultOnrampErrorResolver import com.tangem.data.onramp.DefaultOnrampRepository import com.tangem.data.onramp.DefaultOnrampTransactionRepository import com.tangem.data.onramp.converters.error.OnrampErrorConverter -import com.tangem.domain.onramp.repositories.LegacyTopUpRepository import com.tangem.data.onramp.legacy.MercuryoTopUpRepository import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse @@ -24,11 +23,9 @@ import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.onramp.repositories.HotCryptoRepository -import com.tangem.domain.onramp.repositories.OnrampErrorResolver -import com.tangem.domain.onramp.repositories.OnrampRepository -import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.onramp.repositories.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -104,6 +101,7 @@ internal object OnrampDataModule { appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, analyticsEventHandler: AnalyticsEventHandler, + userTokensResponseStore: UserTokensResponseStore, ): HotCryptoRepository { return DefaultHotCryptoRepository( excludedBlockchains = excludedBlockchains, @@ -113,6 +111,7 @@ internal object OnrampDataModule { appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, analyticsEventHandler = analyticsEventHandler, + userTokensResponseStore = userTokensResponseStore, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index eece90d80a..64bcd2722c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -10,6 +10,7 @@ import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckReposi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository @@ -30,7 +31,7 @@ internal object TokensDataModule { @Singleton fun provideCurrenciesRepository( tangemTechApi: TangemTechApi, - appPreferencesStore: AppPreferencesStore, + userTokensResponseStore: UserTokensResponseStore, userWalletsStore: UserWalletsStore, walletManagersFacade: WalletManagersFacade, cacheRegistry: CacheRegistry, @@ -45,7 +46,7 @@ internal object TokensDataModule { userWalletsStore = userWalletsStore, walletManagersFacade = walletManagersFacade, cacheRegistry = cacheRegistry, - appPreferencesStore = appPreferencesStore, + userTokensResponseStore = userTokensResponseStore, expressServiceLoader = expressServiceLoader, dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index ce4c171722..cd903d6cbb 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -14,10 +14,7 @@ import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.error.DataError @@ -28,11 +25,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.isLocked -import com.tangem.domain.wallets.models.isMultiCurrency -import com.tangem.domain.wallets.models.requireColdWallet +import com.tangem.domain.wallets.models.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -45,11 +38,11 @@ internal class DefaultCurrenciesRepository( private val userWalletsStore: UserWalletsStore, private val walletManagersFacade: WalletManagersFacade, private val cacheRegistry: CacheRegistry, - private val appPreferencesStore: AppPreferencesStore, private val expressServiceLoader: ExpressServiceLoader, private val dispatchers: CoroutineDispatcherProvider, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val userTokensSaver: UserTokensSaver, + private val userTokensResponseStore: UserTokensResponseStore, excludedBlockchains: ExcludedBlockchains, ) : CurrenciesRepository { @@ -96,7 +89,10 @@ internal class DefaultCurrenciesRepository( response = updatedResponse, ) - fetchExpressAssetsByNetworkIds(userWalletId, updatedResponse) + fetchExpressAssetsByNetworkIds( + userWallet = userWalletsStore.getSyncStrict(key = userWalletId), + userTokens = updatedResponse, + ) } } @@ -119,7 +115,11 @@ internal class DefaultCurrenciesRepository( userWalletId = userWalletId, response = updatedResponse, ) - fetchExpressAssetsByNetworkIds(userWalletId, updatedResponse) + + fetchExpressAssetsByNetworkIds( + userWallet = userWalletsStore.getSyncStrict(key = userWalletId), + userTokens = updatedResponse, + ) } } @@ -205,7 +205,7 @@ internal class DefaultCurrenciesRepository( override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return channelFlow { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) if (userWallet.isMultiCurrency) { getMultiCurrencyWalletCurrenciesUpdates(userWalletId).collect(::send) @@ -222,13 +222,19 @@ internal class DefaultCurrenciesRepository( refresh: Boolean, ): CryptoCurrency { return withContext(dispatchers.io) { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard( userWallet.requireColdWallet().scanResponse, ) - fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency), refresh) + + fetchExpressAssetsByNetworkIds( + userWallet = userWallet, + cryptoCurrencies = listOf(currency), + refresh = refresh, + ) + currency } } @@ -238,7 +244,8 @@ internal class DefaultCurrenciesRepository( refresh: Boolean, ): List { return withContext(dispatchers.io) { - val scanResponse = getUserWallet(userWalletId).requireColdWallet().scanResponse + val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val scanResponse = userWallet.requireColdWallet().scanResponse val currencies = if (scanResponse.cardTypesResolver.isSingleWalletWithToken()) { cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = scanResponse) @@ -248,7 +255,12 @@ internal class DefaultCurrenciesRepository( } } - fetchExpressAssetsByNetworkIds(userWalletId, currencies, refresh) + fetchExpressAssetsByNetworkIds( + userWallet = userWallet, + cryptoCurrencies = currencies, + refresh = refresh, + ) + currencies } } @@ -258,7 +270,7 @@ internal class DefaultCurrenciesRepository( id: CryptoCurrency.ID, ): CryptoCurrency { return withContext(dispatchers.io) { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) val currency = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( @@ -266,14 +278,14 @@ internal class DefaultCurrenciesRepository( ) .find { it.id == id } requireNotNull(currency) { "Unable to find currency with provided ID: $id" } - fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency)) + fetchExpressAssetsByNetworkIds(userWallet, listOf(currency)) currency } } override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return channelFlow { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) getMultiCurrencyWalletCurrencies(userWallet) @@ -290,7 +302,7 @@ internal class DefaultCurrenciesRepository( userWalletId: UserWalletId, refresh: Boolean, ): List = withContext(dispatchers.io) { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) fetchTokensIfCacheExpired(userWallet, refresh) @@ -307,7 +319,7 @@ internal class DefaultCurrenciesRepository( override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId) = withContext(dispatchers.io) { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) val storedTokens = requireNotNull( @@ -329,7 +341,7 @@ internal class DefaultCurrenciesRepository( override suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency = withContext(dispatchers.io) { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) val response = requireNotNull( @@ -352,7 +364,7 @@ internal class DefaultCurrenciesRepository( derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin { return withContext(dispatchers.io) { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) @@ -382,7 +394,7 @@ internal class DefaultCurrenciesRepository( override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return channelFlow { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWalletId) @@ -398,7 +410,7 @@ internal class DefaultCurrenciesRepository( override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { return channelFlow { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWalletId) @@ -470,7 +482,7 @@ internal class DefaultCurrenciesRepository( contractAddress: String, networkId: String, ): CryptoCurrency.Token { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) val token = withContext(dispatchers.io) { val foundToken = tangemTechApi.getCoins( contractAddress = contractAddress, @@ -599,7 +611,7 @@ internal class DefaultCurrenciesRepository( userTokensSaver.store(userWalletId, compatibleUserTokensResponse) - fetchExpressAssetsByNetworkIds(userWalletId, compatibleUserTokensResponse) + fetchExpressAssetsByNetworkIds(userWallet, compatibleUserTokensResponse) } private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet.Cold): Boolean { @@ -608,7 +620,7 @@ internal class DefaultCurrenciesRepository( return demoConfig.isDemoCardId(userWallet.cardId) && response == null } - private suspend fun fetchExpressAssetsByNetworkIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) { + private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) { val tokens = userTokens.tokens.map { token -> LeastTokenInfo( contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, @@ -617,12 +629,12 @@ internal class DefaultCurrenciesRepository( } coroutineScope { - launch { expressServiceLoader.update(getUserWallet(userWalletId), tokens) } + launch { expressServiceLoader.update(userWallet, tokens) } } } private suspend fun fetchExpressAssetsByNetworkIds( - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoCurrencies: List, refresh: Boolean = false, ) { @@ -634,11 +646,11 @@ internal class DefaultCurrenciesRepository( ) } cacheRegistry.invokeOnExpire( - key = getAssetsCacheKey(userWalletId), + key = getAssetsCacheKey(userWallet.walletId), skipCache = refresh, block = { coroutineScope { - launch { expressServiceLoader.update(getUserWallet(userWalletId), tokens) } + launch { expressServiceLoader.update(userWallet, tokens) } } }, ) @@ -648,9 +660,8 @@ internal class DefaultCurrenciesRepository( private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse { val userWalletId = userWallet.walletId - val response = appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), - ) ?: createDefaultUserTokensResponse(userWallet) + val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + ?: createDefaultUserTokensResponse(userWallet = userWallet) if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) { Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId") @@ -666,20 +677,14 @@ internal class DefaultCurrenciesRepository( private fun createDefaultUserTokensResponse(userWallet: UserWallet) = userTokensResponseFactory.createUserTokensResponse( currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard( - userWallet.requireColdWallet().scanResponse, + userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] ), isGroupedByNetwork = false, isSortedByBalance = false, ) - private fun getUserWallet(userWalletId: UserWalletId): UserWallet { - return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find a user wallet with provided ID: $userWalletId" - } - } - private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsStore.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected) } @@ -708,14 +713,10 @@ internal class DefaultCurrenciesRepository( private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" private fun getSavedUserTokensResponse(key: UserWalletId): Flow { - return appPreferencesStore - .getObject(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue)) - .filterNotNull() + return userTokensResponseStore.get(userWalletId = key).filterNotNull() } private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { - return appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(key.stringValue), - ) + return userTokensResponseStore.getSyncOrNull(userWalletId = key) } } \ No newline at end of file From d14a5cb87aaecac92606d996cbc8a5dca902cd7c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Jun 2025 16:34:00 +0500 Subject: [PATCH 78/99] Updated on 2026-08-14 --- .../com/tangem/tap/routing/ProxyAppRouter.kt | 1 - .../ui/amountScreen/AmountScreenContent.kt | 4 +- .../field/AmountBoundaryUpdateTransformer.kt | 46 +++++++ .../converters/field/AmountFieldConverter.kt | 2 +- .../ui/amountScreen/models/AmountState.kt | 4 +- .../preview/AmountStatePreviewData.kt | 2 +- .../send/v2/send/DefaultSendComponent.kt | 117 ++++++++++-------- .../v2/send/confirm/SendConfirmComponent.kt | 7 +- .../features/send/v2/send/model/SendModel.kt | 33 +++-- .../amount/SendAmountComponentParams.kt | 18 ++- .../amount/model/SendAmountModel.kt | 101 +++++++++++---- .../destination/model/SendDestinationModel.kt | 18 ++- .../state/StakingStateController.kt | 2 +- 13 files changed, 255 insertions(+), 100 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index 5007213b9b..cdebe55b4a 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -82,7 +82,6 @@ internal class ProxyAppRouter( override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) { if (!isSuccess) { - Timber.tag("ASDASD").d(errorMessage) analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage))) Timber.w(errorMessage) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index bdb765bbbf..823ad1ce3d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -30,8 +30,6 @@ fun AmountScreenContent( clickIntents: AmountScreenClickIntents, modifier: Modifier = Modifier, ) { - if (amountState !is AmountState.Data) return - // Do not put fillMaxSize() in here LazyColumn( modifier = modifier @@ -49,7 +47,7 @@ fun AmountScreenContent( onCurrencyChange = clickIntents::onCurrencyChangeClick, onMaxAmountClick = clickIntents::onMaxValueClick, ) - } else { + } else if (amountState is AmountState.Data) { amountField( amountState = amountState, isBalanceHidden = isBalanceHidden, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt new file mode 100644 index 0000000000..a689b4501a --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt @@ -0,0 +1,46 @@ +package com.tangem.common.ui.amountScreen.converters.field + +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.transformer.Transformer + +/** + * Updates amount boundaries and revalidates current amount state + * + * @property cryptoCurrencyStatus current cryptocurrency status + * @property maxEnterAmount new maximum enter amount boundary + * @property appCurrency current app currency + * @property isRedesignEnabled whether redesign mode is enabled + */ +class AmountBoundaryUpdateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val maxEnterAmount: EnterAmountBoundary, + private val appCurrency: AppCurrency, + private val isRedesignEnabled: Boolean, +) : Transformer { + + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + + val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } + val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } + + val availableBalance = if (isRedesignEnabled) { + resourceReference(R.string.common_balance, wrappedList(crypto)) + } else { + resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)) + } + + return prevState.copy( + availableBalance = availableBalance, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt index ce11f41fce..f19fad6bb4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -100,8 +100,8 @@ class AmountFieldConverterV2( val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) val fiatRate = cryptoCurrencyStatus.value.fiatRate val (fiatValue, fiatDecimal) = when { - fiatRate.isNullOrZero() -> "" to null value.isEmpty() -> "" to BigDecimal.ZERO + fiatRate.isNullOrZero() -> "" to null else -> { val fiatDecimal = fiatRate?.multiply(cryptoDecimal) val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty() diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index b5eab4301d..cc23669f47 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -12,6 +12,7 @@ import java.math.BigDecimal sealed class AmountState { abstract val isPrimaryButtonEnabled: Boolean + abstract val isRedesignEnabled: Boolean /** * @param isPrimaryButtonEnabled indicates if next state button enabled @@ -29,6 +30,7 @@ sealed class AmountState { */ data class Data( override val isPrimaryButtonEnabled: Boolean, + override val isRedesignEnabled: Boolean, val title: TextReference, val availableBalance: TextReference, val tokenName: TextReference, @@ -41,10 +43,10 @@ sealed class AmountState { val isEditingDisabled: Boolean = false, val reduceAmountBy: BigDecimal = BigDecimal.ZERO, val isIgnoreReduce: Boolean = false, - val isRedesignEnabled: Boolean, ) : AmountState() data class Empty( override val isPrimaryButtonEnabled: Boolean = false, + override val isRedesignEnabled: Boolean, ) : AmountState() } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index ab6ead2229..99c11a9a14 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -19,7 +19,7 @@ import java.math.BigDecimal object AmountStatePreviewData { - val emptyState = AmountState.Empty() + val emptyState = AmountState.Empty(isRedesignEnabled = true) val amountState = AmountState.Data( isPrimaryButtonEnabled = false, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index d38c5b42a8..4f091935b8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -19,6 +19,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents @@ -57,14 +58,19 @@ internal class DefaultSendComponent @AssistedInject constructor( popCallback = { onChildBack() }, ) + private val model: SendModel = getOrCreateModel(params = params, router = innerRouter) + private val initialRoute = if (params.amount == null) { - CommonSendRoute.Destination(isEditMode = false) + if (model.uiState.value.isRedesignEnabled) { + CommonSendRoute.Amount(isEditMode = false) + } else { + CommonSendRoute.Destination(isEditMode = false) + } } else { CommonSendRoute.Empty } - private val currentRoute = MutableStateFlow(initialRoute) - private val model: SendModel = getOrCreateModel(params = params, router = innerRouter) + private val currentRoute = MutableStateFlow(initialRoute) private val childStack = childStack( key = "sendInnerStack", @@ -155,9 +161,14 @@ internal class DefaultSendComponent @AssistedInject constructor( callback = model, onBackClick = ::onChildBack, onNextClick = { + val nextRoute = if (model.uiState.value.isRedesignEnabled) { + CommonSendRoute.Confirm + } else { + CommonSendRoute.Amount(isEditMode = false) + } innerRouter.safeNextClick( currentRoute = route, - nextRoute = CommonSendRoute.Amount(isEditMode = false), + nextRoute = nextRoute, popBack = ::onChildBack, childStack = childStack, ) @@ -169,50 +180,54 @@ internal class DefaultSendComponent @AssistedInject constructor( factoryContext: AppComponentContext, route: CommonSendRoute, ): ComposableContentComponent { - val cryptoCurrencyStatus = model.cryptoCurrencyStatus - return if (cryptoCurrencyStatus != null) { - SendAmountComponent( - appComponentContext = factoryContext, - params = SendAmountComponentParams.AmountParams( - state = model.uiState.value.amountUM, - currentRoute = currentRoute.filterIsInstance(), - isBalanceHidingFlow = model.isBalanceHiddenFlow, - analyticsCategoryName = analyticCategoryName, - userWallet = model.userWallet, - appCurrency = model.appCurrency, - cryptoCurrencyStatus = cryptoCurrencyStatus, - callback = model, - predefinedValues = model.predefinedValues, - onBackClick = { - if (route.isEditMode) { + return SendAmountComponent( + appComponentContext = factoryContext, + params = SendAmountComponentParams.AmountParams( + state = model.uiState.value.amountUM, + currentRoute = currentRoute.filterIsInstance(), + isBalanceHidingFlow = model.isBalanceHiddenFlow, + analyticsCategoryName = analyticCategoryName, + appCurrency = model.appCurrency, + userWalletId = params.userWalletId, + cryptoCurrency = params.currency, + cryptoCurrencyStatusFlow = model.cryptoCurrencyStatusFlow, + callback = model, + predefinedValues = model.predefinedValues, + onBackClick = { + if (route.isEditMode) { + onChildBack() + } else { + analyticsEventHandler.send( + CommonSendAnalyticEvents.CloseButtonClicked( + categoryName = analyticCategoryName, + source = SendScreenSource.Amount, + isFromSummary = false, + isValid = model.uiState.value.amountUM.isPrimaryButtonEnabled, + ), + ) + if (model.uiState.value.isRedesignEnabled) { onChildBack() } else { - analyticsEventHandler.send( - CommonSendAnalyticEvents.CloseButtonClicked( - categoryName = analyticCategoryName, - source = SendScreenSource.Amount, - isFromSummary = false, - isValid = model.uiState.value.amountUM.isPrimaryButtonEnabled, - ), - ) router.pop() } - }, - onNextClick = { - innerRouter.safeNextClick( - currentRoute = route, - nextRoute = CommonSendRoute.Confirm, - popBack = ::onChildBack, - childStack = childStack, - ) - }, - isRedesignEnabled = model.uiState.value.isRedesignEnabled, - ), - ) - } else { - model.showAlertError() - getStubComponent() - } + } + }, + onNextClick = { + val nextRoute = if (model.uiState.value.isRedesignEnabled) { + CommonSendRoute.Destination(isEditMode = false) + } else { + CommonSendRoute.Confirm + } + innerRouter.safeNextClick( + currentRoute = route, + nextRoute = nextRoute, + popBack = ::onChildBack, + childStack = childStack, + ) + }, + isRedesignEnabled = model.uiState.value.isRedesignEnabled, + ), + ) } @Suppress("ComplexCondition") @@ -220,8 +235,8 @@ internal class DefaultSendComponent @AssistedInject constructor( val state = model.uiState.value val sendAmount = (state.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value - val cryptoCurrencyStatus = model.cryptoCurrencyStatus - val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus + val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value + val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value return if (sendAmount != null && destinationAddress != null && feeCryptoCurrencyStatus != null && cryptoCurrencyStatus != null @@ -251,10 +266,12 @@ internal class DefaultSendComponent @AssistedInject constructor( } private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableContentComponent { - val cryptoCurrencyStatus = model.cryptoCurrencyStatus - val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus + val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value + val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value - return if (cryptoCurrencyStatus != null && feeCryptoCurrencyStatus != null) { + return if (cryptoCurrencyStatus.value != CryptoCurrencyStatus.Loading && + feeCryptoCurrencyStatus.value != CryptoCurrencyStatus.Loading + ) { SendConfirmComponent( appComponentContext = factoryContext, params = SendConfirmComponent.Params( @@ -265,6 +282,8 @@ internal class DefaultSendComponent @AssistedInject constructor( analyticsCategoryName = analyticCategoryName, cryptoCurrencyStatus = cryptoCurrencyStatus, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + cryptoCurrencyStatusFlow = model.cryptoCurrencyStatusFlow, + feeCryptoCurrencyStatusFlow = model.feeCryptoCurrencyStatusFlow, appCurrency = model.appCurrency, callback = model, predefinedValues = model.predefinedValues, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index 0145507ff8..22b3e7293e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -31,6 +31,7 @@ import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent import com.tangem.utils.extensions.orZero import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.StateFlow internal class SendConfirmComponent( appComponentContext: AppComponentContext, @@ -62,11 +63,13 @@ internal class SendConfirmComponent( state = model.uiState.value.amountUM, analyticsCategoryName = params.analyticsCategoryName, userWallet = params.userWallet, - cryptoCurrencyStatus = params.cryptoCurrencyStatus, appCurrency = params.appCurrency, blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), predefinedValues = params.predefinedValues, isRedesignEnabled = model.uiState.value.isRedesignEnabled, + userWalletId = params.userWallet.walletId, + cryptoCurrency = params.cryptoCurrencyStatus.currency, + cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow, ), onResult = model::onAmountResult, onClick = model::showEditAmount, @@ -145,6 +148,8 @@ internal class SendConfirmComponent( val userWallet: UserWallet, val cryptoCurrencyStatus: CryptoCurrencyStatus, val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + val cryptoCurrencyStatusFlow: StateFlow, + val feeCryptoCurrencyStatusFlow: StateFlow, val appCurrency: AppCurrency, val callback: ModelCallback, val currentRoute: Flow, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index a61ec8f1e6..2ea110f6a2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -92,7 +92,6 @@ internal class SendModel @Inject constructor( ) : Model(), SendComponentCallback { private val params: SendComponent.Params = paramsContainer.require() - private val userWalletId = params.userWalletId private val cryptoCurrency = params.currency private val _uiState = MutableStateFlow(initialState()) @@ -101,9 +100,23 @@ internal class SendModel @Inject constructor( private val _isBalanceHiddenFlow = MutableStateFlow(false) val isBalanceHiddenFlow = _isBalanceHiddenFlow.asStateFlow() + private val _cryptoCurrencyStatusFlow = MutableStateFlow( + CryptoCurrencyStatus( + params.currency, + value = CryptoCurrencyStatus.Loading, + ), + ) + val cryptoCurrencyStatusFlow = _cryptoCurrencyStatusFlow.asStateFlow() + + private val _feeCryptoCurrencyStatusFlow = MutableStateFlow( + CryptoCurrencyStatus( + params.currency, + value = CryptoCurrencyStatus.Loading, + ), + ) + val feeCryptoCurrencyStatusFlow = _feeCryptoCurrencyStatusFlow.asStateFlow() + var userWallet: UserWallet by Delegates.notNull() - var cryptoCurrencyStatus: CryptoCurrencyStatus? = null - var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null var appCurrency: AppCurrency = AppCurrency.Default var predefinedValues: PredefinedValues = PredefinedValues.Empty @@ -279,16 +292,16 @@ internal class SendModel @Inject constructor( ): Flow> { return when { isSingleWalletWithToken -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, + userWalletId = params.userWalletId, currencyId = cryptoCurrency.id, isSingleWalletWithTokens = true, ) isMultiCurrency -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, + userWalletId = params.userWalletId, currencyId = cryptoCurrency.id, isSingleWalletWithTokens = false, ) - else -> getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWalletId) + else -> getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = params.userWalletId) } } @@ -298,7 +311,7 @@ internal class SendModel @Inject constructor( ): CryptoCurrencyStatus { return if (isMultiCurrency) { getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, + userWalletId = params.userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, ).getOrNull() ?: cryptoCurrencyStatus } else { @@ -307,8 +320,8 @@ internal class SendModel @Inject constructor( } private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, feeCurrencyStatus: CryptoCurrencyStatus) { - cryptoCurrencyStatus = currencyStatus - feeCryptoCurrencyStatus = feeCurrencyStatus + _cryptoCurrencyStatusFlow.value = currencyStatus + _feeCryptoCurrencyStatusFlow.value = feeCurrencyStatus if (params.amount != null) { router.replaceAll(CommonSendRoute.Confirm) @@ -360,7 +373,7 @@ internal class SendModel @Inject constructor( } private fun initialState(): SendUM = SendUM( - amountUM = AmountState.Empty(), + amountUM = AmountState.Empty(isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled), destinationUM = SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt index a13dcf3ef1..419dc6ced6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt @@ -2,8 +2,10 @@ package com.tangem.features.send.v2.subcomponents.amount import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback @@ -14,20 +16,22 @@ internal sealed class SendAmountComponentParams { abstract val state: AmountState abstract val analyticsCategoryName: String - abstract val userWallet: UserWallet + abstract val userWalletId: UserWalletId abstract val appCurrency: AppCurrency - abstract val cryptoCurrencyStatus: CryptoCurrencyStatus abstract val predefinedValues: PredefinedValues abstract val isRedesignEnabled: Boolean + abstract val cryptoCurrency: CryptoCurrency + abstract val cryptoCurrencyStatusFlow: StateFlow data class AmountParams( override val state: AmountState, override val analyticsCategoryName: String, - override val userWallet: UserWallet, + override val userWalletId: UserWalletId, override val appCurrency: AppCurrency, - override val cryptoCurrencyStatus: CryptoCurrencyStatus, override val predefinedValues: PredefinedValues, override val isRedesignEnabled: Boolean, + override val cryptoCurrency: CryptoCurrency, + override val cryptoCurrencyStatusFlow: StateFlow, val callback: ModelCallback, val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, @@ -38,11 +42,13 @@ internal sealed class SendAmountComponentParams { data class AmountBlockParams( override val state: AmountState, override val analyticsCategoryName: String, - override val userWallet: UserWallet, override val appCurrency: AppCurrency, - override val cryptoCurrencyStatus: CryptoCurrencyStatus, + override val userWalletId: UserWalletId, override val predefinedValues: PredefinedValues, override val isRedesignEnabled: Boolean, + override val cryptoCurrency: CryptoCurrency, + override val cryptoCurrencyStatusFlow: StateFlow, + val userWallet: UserWallet, val blockClickEnableFlow: StateFlow, ) : SendAmountComponentParams() } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index d60c1b6b81..dc008c3246 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -1,10 +1,13 @@ package com.tangem.features.send.v2.subcomponents.amount.model import androidx.compose.runtime.Stable +import arrow.core.getOrElse import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.converters.* import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer +import com.tangem.common.ui.amountScreen.converters.field.AmountBoundaryUpdateTransformer +import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary @@ -14,9 +17,14 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.ui.state.NavigationUM @@ -36,6 +44,7 @@ import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.properties.Delegates @Suppress("LongParameterList") @Stable @@ -49,33 +58,59 @@ internal class SendAmountModel @Inject constructor( private val sendAmountUpdateQRListener: SendAmountUpdateQRListener, private val analyticsEventHandler: AnalyticsEventHandler, private val sendFeatureToggles: SendFeatureToggles, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, ) : Model(), AmountScreenClickIntents { private val params: SendAmountComponentParams = paramsContainer.require() + private var appCurrency: AppCurrency = AppCurrency.Default + private var userWallet: UserWallet? = null private val _uiState = MutableStateFlow(params.state) val uiState = _uiState.asStateFlow() private val analyticsCategoryName = params.analyticsCategoryName - private val userWallet = params.userWallet - private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + private var cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( + currency = params.cryptoCurrency, + value = CryptoCurrencyStatus.Loading, + ) private var minAmountBoundary: EnterAmountBoundary? = null - private var maxAmountBoundary: EnterAmountBoundary = MaxEnterAmountConverter().convert(cryptoCurrencyStatus) + private var maxAmountBoundary: EnterAmountBoundary by Delegates.notNull() init { configAmountNavigation() - initMinBoundary() - subscribeOnAmountReduceByTriggerUpdates() - subscribeOnAmountReduceToTriggerUpdates() - subscribeOnAmountIgnoreReduceTriggerUpdates() - subscribeOnAmountUpdateQRTriggerUpdates() + initAppCurrency() + subscribeOnCryptoCurrencyStatusFlow() + } + + private fun initAppCurrency() { + modelScope.launch { + userWallet = getUserWalletUseCase(params.userWalletId).getOrNull() + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + } + } + + private fun subscribeOnCryptoCurrencyStatusFlow() { + params.cryptoCurrencyStatusFlow + .onEach { newCryptoCurrencyStatus -> + cryptoCurrencyStatus = newCryptoCurrencyStatus + maxAmountBoundary = MaxEnterAmountConverter().convert(cryptoCurrencyStatus) + if (uiState.value is AmountState.Empty) { + subscribeOnAmountReduceByTriggerUpdates() + subscribeOnAmountReduceToTriggerUpdates() + subscribeOnAmountIgnoreReduceTriggerUpdates() + subscribeOnAmountUpdateQRTriggerUpdates() + } + initMinBoundary() + } + .launchIn(modelScope) } private fun initMinBoundary() { modelScope.launch { minAmountBoundary = getMinimumTransactionAmountSyncUseCase( - userWalletId = userWallet.walletId, + userWalletId = params.userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, ).getOrNull()?.let { EnterAmountBoundary( @@ -84,23 +119,37 @@ internal class SendAmountModel @Inject constructor( ) } - initialState() + if (uiState.value is AmountState.Data) { + _uiState.update( + AmountBoundaryUpdateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = maxAmountBoundary, + appCurrency = appCurrency, + isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, + ), + ) + } else { + initialState() + } } } private fun initialState() { - if (uiState.value is AmountState.Empty) { + if (uiState.value is AmountState.Empty && userWallet != null) { _uiState.update { AmountStateConverterV2( clickIntents = this, - appCurrency = params.appCurrency, + appCurrency = appCurrency, cryptoCurrencyStatus = cryptoCurrencyStatus, maxEnterAmount = maxAmountBoundary, iconStateConverter = CryptoCurrencyToIconStateConverter(), isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, ).convert( AmountParameters( - title = stringReference(userWallet.name), + title = resourceReference( + R.string.send_from_wallet_name, + WrappedList(listOf(userWallet?.name.orEmpty())), // TODO [REDACTED_TASK_KEY] + ), value = "", ), ) @@ -242,7 +291,7 @@ internal class SendAmountModel @Inject constructor( NavigationUM.Content( title = resourceReference(R.string.send_amount_label), subtitle = null, - backIconRes = if (route.isEditMode) { + backIconRes = if (route.isEditMode || params.isRedesignEnabled) { R.drawable.ic_back_24 } else { R.drawable.ic_close_24 @@ -260,15 +309,19 @@ internal class SendAmountModel @Inject constructor( params.onNextClick() }, ), - prevButton = ButtonsUM.PrimaryButtonUM( - text = TextReference.EMPTY, - iconResId = R.drawable.ic_back_24, - isEnabled = true, - onClick = { - saveResult() - params.onBackClick() - }, - ).takeIf { route.isEditMode.not() }, + prevButton = if (params.isRedesignEnabled) { + null + } else { + ButtonsUM.PrimaryButtonUM( + text = TextReference.EMPTY, + iconResId = R.drawable.ic_back_24, + isEnabled = true, + onClick = { + saveResult() + params.onBackClick() + }, + ).takeIf { route.isEditMode.not() } + }, secondaryPairButtonsUM = null, ), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 6bd184f1fc..2cefea736b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -8,6 +8,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.network.CryptoCurrencyAddress import com.tangem.domain.qrscanning.models.SourceType @@ -293,6 +294,7 @@ internal class SendDestinationModel @Inject constructor( params.callback.onDestinationResult(uiState.value) } + @Suppress("LongMethod") private fun configDestinationNavigation() { val params = params as? SendDestinationComponentParams.DestinationParams ?: return combine( @@ -305,7 +307,7 @@ internal class SendDestinationModel @Inject constructor( NavigationUM.Content( title = params.title, subtitle = null, - backIconRes = if (route.isEditMode) { + backIconRes = if (route.isEditMode || isRedesignEnabled) { R.drawable.ic_back_24 } else { R.drawable.ic_close_24 @@ -345,7 +347,19 @@ internal class SendDestinationModel @Inject constructor( params.onNextClick() }, ), - prevButton = null, + prevButton = if (isRedesignEnabled) { + ButtonsUM.PrimaryButtonUM( + text = TextReference.EMPTY, + iconResId = R.drawable.ic_back_24, + isEnabled = true, + onClick = { + saveResult() + params.onBackClick() + }, + ) + } else { + null + }, secondaryPairButtonsUM = null, ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 328bf590dd..ed6fbd3a57 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -78,7 +78,7 @@ internal class StakingStateController @Inject constructor( cryptoCurrencyBlockchainId = "", currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), - amountState = AmountState.Empty(), + amountState = AmountState.Empty(isRedesignEnabled = false), validatorState = StakingStates.ValidatorState.Empty(), rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), confirmationState = StakingStates.ConfirmationState.Empty(), From 58c6eeac8c4db20680546bb6a27de423d43d2183 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Jun 2025 18:04:10 +0500 Subject: [PATCH 79/99] Updated on 2026-08-14 --- .../swap/v2/impl/amount/entity/SwapAmountUM.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index 532fc81270..62b7c86556 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -59,13 +59,19 @@ sealed class SwapAmountFieldUM { data class Empty( override val amountType: SwapAmountType, ) : SwapAmountFieldUM() { - override val amountField: AmountState = AmountState.Empty(isPrimaryButtonEnabled = false) + override val amountField: AmountState = AmountState.Empty( + isPrimaryButtonEnabled = false, + isRedesignEnabled = false, + ) } data class Loading( override val amountType: SwapAmountType, ) : SwapAmountFieldUM() { - override val amountField: AmountState = AmountState.Empty(isPrimaryButtonEnabled = false) + override val amountField: AmountState = AmountState.Empty( + isPrimaryButtonEnabled = false, + isRedesignEnabled = false, + ) } data class Content( From 288bddfe18dc3f3df387675a1bc0949f83651d6d Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 28 Jun 2025 11:55:20 +0400 Subject: [PATCH 80/99] Updated on 2026-08-14 --- .../DefaultCardCryptoCurrencyFactory.kt | 9 +++------ .../currency/ResponseCryptoCurrenciesFactory.kt | 14 +++++--------- .../tangem/data/common/di/DataCommonModule.kt | 7 +++---- .../DefaultCardCryptoCurrencyFactoryTest.kt | 7 ++++++- .../swap/DefaultSwapTransactionRepository.kt | 8 ++++---- .../SavedSwapTransactionConverter.kt | 5 ++--- .../SavedSwapTransactionListConverter.kt | 6 ++---- .../com/tangem/data/swap/di/SwapDataModule.kt | 10 +++++----- .../tangem/data/tokens/di/TokensDataModule.kt | 3 +++ .../repository/DefaultCurrenciesRepository.kt | 17 ++++++++++------- .../swap/DefaultSwapTransactionRepository.kt | 8 +++++--- .../SavedSwapTransactionListConverter.kt | 4 +--- .../tangem/feature/swap/di/SwapDataModule.kt | 5 +++-- 13 files changed, 52 insertions(+), 51 deletions(-) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index a82af53a79..286947afa4 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -31,6 +31,7 @@ internal class DefaultCardCryptoCurrencyFactory( private val excludedBlockchains: ExcludedBlockchains, private val userWalletsStore: UserWalletsStore, private val userTokensResponseStore: UserTokensResponseStore, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ) : CardCryptoCurrencyFactory { private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) } @@ -142,9 +143,7 @@ internal class DefaultCardCryptoCurrencyFactory( val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) ?: return emptyMap() - val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) - - return responseCurrenciesFactory.createCurrencies( + return responseCryptoCurrenciesFactory.createCurrencies( tokens = response.tokens.filter { token -> networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath } }, @@ -160,11 +159,9 @@ internal class DefaultCardCryptoCurrencyFactory( val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) ?: return emptyMap() - val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) - val networkIds = rawIds.map { it.toBlockchain().toNetworkId() } - return responseCurrenciesFactory.createCurrencies( + return responseCryptoCurrenciesFactory.createCurrencies( tokens = response.tokens.filter { token -> token.networkId in networkIds }, scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] ) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt index 1d9b8a5818..b7c3cde6f6 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt @@ -2,7 +2,6 @@ package com.tangem.data.common.currency import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.data.common.network.NetworkFactory @@ -11,11 +10,12 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse import timber.log.Timber +import javax.inject.Inject import com.tangem.blockchain.common.Token as SdkToken -class ResponseCryptoCurrenciesFactory(excludedBlockchains: ExcludedBlockchains) { - - private val networkFactory by lazy(LazyThreadSafetyMode.NONE) { NetworkFactory(excludedBlockchains) } +class ResponseCryptoCurrenciesFactory @Inject constructor( + private val networkFactory: NetworkFactory, +) { fun createCurrency(currencyId: String, response: UserTokensResponse, scanResponse: ScanResponse): CryptoCurrency { return response.tokens @@ -25,11 +25,7 @@ class ResponseCryptoCurrenciesFactory(excludedBlockchains: ExcludedBlockchains) } fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List { - return response.tokens - .asSequence() - .mapNotNull { createCurrency(it, scanResponse) } - .distinctBy { it.id } - .toList() + return createCurrencies(tokens = response.tokens, scanResponse = scanResponse) } fun createCurrencies(tokens: List, scanResponse: ScanResponse): List { diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index c8c834d85c..1e3876029c 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -1,10 +1,7 @@ package com.tangem.data.common.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.DefaultCardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseAddressesEnricher -import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.common.currency.* import com.tangem.data.common.quote.DefaultQuotesFetcher import com.tangem.data.common.quote.QuotesFetcher import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -31,12 +28,14 @@ internal object DataCommonModule { excludedBlockchains: ExcludedBlockchains, userWalletsStore: UserWalletsStore, userTokensResponseStore: UserTokensResponseStore, + responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ): CardCryptoCurrencyFactory { return DefaultCardCryptoCurrencyFactory( demoConfig = DemoConfig(), excludedBlockchains = excludedBlockchains, userWalletsStore = userWalletsStore, userTokensResponseStore = userTokensResponseStore, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, ) } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt index 5b757925b0..0622887743 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt @@ -8,6 +8,7 @@ import com.tangem.common.card.WalletData import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -34,12 +35,16 @@ internal class DefaultCardCryptoCurrencyFactoryTest { private val userWalletsStore: UserWalletsStore = mockk() private val userTokensResponseStore: UserTokensResponseStore = mockk() + private val excludedBlockchains = ExcludedBlockchains() private val factory = DefaultCardCryptoCurrencyFactory( demoConfig = DemoConfig(), - excludedBlockchains = ExcludedBlockchains(), + excludedBlockchains = excludedBlockchains, userWalletsStore = userWalletsStore, userTokensResponseStore = userTokensResponseStore, + responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory( + networkFactory = NetworkFactory(excludedBlockchains = excludedBlockchains), + ), ) private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt index 5329db524a..2ceb2cab7e 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -1,6 +1,6 @@ package com.tangem.data.swap -import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.swap.converter.transaction.SavedSwapStatusConverter import com.tangem.data.swap.converter.transaction.SavedSwapTransactionConverter @@ -31,15 +31,15 @@ import kotlinx.coroutines.withContext internal class DefaultSwapTransactionRepository( private val appPreferencesStore: AppPreferencesStore, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, private val dispatchers: CoroutineDispatcherProvider, - excludedBlockchains: ExcludedBlockchains, ) : SwapTransactionRepository { private val listConverter by lazy(LazyThreadSafetyMode.NONE) { - SavedSwapTransactionListConverter(excludedBlockchains) + SavedSwapTransactionListConverter(responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory) } private val converter by lazy(LazyThreadSafetyMode.NONE) { - SavedSwapTransactionConverter(excludedBlockchains) + SavedSwapTransactionConverter(responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory) } private val savedStatusConverter by lazy(LazyThreadSafetyMode.NONE) { SavedSwapStatusConverter() diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt index ab7b21e61d..78c257249a 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt @@ -1,6 +1,5 @@ package com.tangem.data.swap.converter.transaction -import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.swap.models.SwapStatusDTO import com.tangem.data.swap.models.SwapTransactionDTO @@ -9,9 +8,9 @@ import com.tangem.domain.swap.models.SwapTransactionModel import com.tangem.utils.converter.TwoWayConverter internal class SavedSwapTransactionConverter( - excludedBlockchains: ExcludedBlockchains, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ) : TwoWayConverter { - private val responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) + private val statusConverter by lazy(LazyThreadSafetyMode.NONE) { SavedSwapStatusConverter() } diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt index 176c1eb8f6..393c3c421a 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt @@ -1,6 +1,5 @@ package com.tangem.data.swap.converter.transaction -import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.swap.models.SwapStatusDTO @@ -13,13 +12,12 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.converter.Converter internal class SavedSwapTransactionListConverter( - excludedBlockchains: ExcludedBlockchains, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ) : Converter { - private val responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) private val userTokensResponseFactory = UserTokensResponseFactory() private val savedSwapTransactionConverter by lazy(LazyThreadSafetyMode.NONE) { - SavedSwapTransactionConverter(excludedBlockchains) + SavedSwapTransactionConverter(responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory) } override fun convert(value: SwapTransactionListModel) = SwapTransactionListDTO( diff --git a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt index bd75b9d1ad..486080c4e4 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt @@ -1,7 +1,7 @@ package com.tangem.data.swap.di import com.squareup.moshi.Moshi -import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.express.converter.ExpressErrorConverter import com.tangem.data.swap.DefaultSwapErrorResolver import com.tangem.data.swap.DefaultSwapRepositoryV2 @@ -56,14 +56,14 @@ internal object SwapDataModule { @Provides @Singleton fun provideSwapTransactionRepository( - coroutineDispatcher: CoroutineDispatcherProvider, appPreferencesStore: AppPreferencesStore, - excludedBlockchains: ExcludedBlockchains, + responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, + dispatchers: CoroutineDispatcherProvider, ): SwapTransactionRepository { return DefaultSwapTransactionRepository( appPreferencesStore = appPreferencesStore, - dispatchers = coroutineDispatcher, - excludedBlockchains = excludedBlockchains, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, + dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 64bcd2722c..2cba8e6ef9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -3,6 +3,7 @@ package com.tangem.data.tokens.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.tokens.repository.DefaultCurrenciesRepository import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository @@ -40,6 +41,7 @@ internal object TokensDataModule { excludedBlockchains: ExcludedBlockchains, cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, tokensSaver: UserTokensSaver, + responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ): CurrenciesRepository { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, @@ -52,6 +54,7 @@ internal object TokensDataModule { excludedBlockchains = excludedBlockchains, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, userTokensSaver = tokensSaver, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index cd903d6cbb..efb58c1d8b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -43,11 +43,11 @@ internal class DefaultCurrenciesRepository( private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val userTokensSaver: UserTokensSaver, private val userTokensResponseStore: UserTokensResponseStore, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, excludedBlockchains: ExcludedBlockchains, ) : CurrenciesRepository { private val demoConfig = DemoConfig() - private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) private val userTokensResponseFactory = UserTokensResponseFactory() private val customTokensMerger = CustomTokensMerger( @@ -314,7 +314,7 @@ internal class DefaultCurrenciesRepository( }, ) - responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse) + responseCryptoCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse) } override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId) = @@ -329,7 +329,7 @@ internal class DefaultCurrenciesRepository( }, ) - responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse) + responseCryptoCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse) } override suspend fun getMultiCurrencyWalletCurrency( @@ -351,7 +351,7 @@ internal class DefaultCurrenciesRepository( }, ) - responseCurrenciesFactory.createCurrency( + responseCryptoCurrenciesFactory.createCurrency( currencyId = id, response = response, scanResponse = userWallet.requireColdWallet().scanResponse, @@ -386,7 +386,10 @@ internal class DefaultCurrenciesRepository( it.derivationPath == derivationPath.value } ?: error("Coin in this network $networkId not found") - val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.requireColdWallet().scanResponse) + val coin = responseCryptoCurrenciesFactory.createCurrency( + responseToken = storedCoin, + scanResponse = userWallet.requireColdWallet().scanResponse, + ) coin as? CryptoCurrency.Coin ?: error("Unable to create currency") } @@ -526,7 +529,7 @@ internal class DefaultCurrenciesRepository( getL2CompatibilityTokenComparison(it, currencyRawId.value) } - responseCurrenciesFactory.createCurrencies( + responseCryptoCurrenciesFactory.createCurrencies( response = storedTokens.copy(tokens = filterResponse), scanResponse = userWallet.requireColdWallet().scanResponse, ) @@ -572,7 +575,7 @@ internal class DefaultCurrenciesRepository( private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> - responseCurrenciesFactory.createCurrencies( + responseCryptoCurrenciesFactory.createCurrencies( response = storedTokens, scanResponse = userWallet.requireColdWallet().scanResponse, ) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index 09acf15c65..5633a2c403 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap -import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -24,10 +24,12 @@ import kotlinx.coroutines.withContext internal class DefaultSwapTransactionRepository( private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, - excludedBlockchains: ExcludedBlockchains, + responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ) : SwapTransactionRepository { - private val converter = SavedSwapTransactionListConverter(excludedBlockchains) + private val converter by lazy(LazyThreadSafetyMode.NONE) { + SavedSwapTransactionListConverter(responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory) + } private val userTokensResponseFactory = UserTokensResponseFactory() override suspend fun storeTransaction( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index 9aecea1678..e0b2c21e91 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -1,6 +1,5 @@ package com.tangem.feature.swap.converters -import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.domain.models.currency.CryptoCurrency @@ -13,10 +12,9 @@ import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel import com.tangem.utils.converter.Converter internal class SavedSwapTransactionListConverter( - excludedBlockchains: ExcludedBlockchains, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ) : Converter { - private val responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) private val userTokensResponseFactory = UserTokensResponseFactory() override fun convert(value: SavedSwapTransactionListModel) = SavedSwapTransactionListModelInner( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index eb0e016c58..679015d755 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.di import com.squareup.moshi.Moshi import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier @@ -55,13 +56,13 @@ internal class SwapDataModule { @Singleton fun provideSwapTransactionRepository( appPreferencesStore: AppPreferencesStore, + responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, dispatcherProvider: CoroutineDispatcherProvider, - excludedBlockchains: ExcludedBlockchains, ): SwapTransactionRepository { return DefaultSwapTransactionRepository( appPreferencesStore = appPreferencesStore, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, dispatchers = dispatcherProvider, - excludedBlockchains = excludedBlockchains, ) } From 566c06095d57418b8f7f0624dff892bd57c9884d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 12:51:30 +0500 Subject: [PATCH 81/99] Updated on 2026-08-14 --- .../tangem/features/details/model/UserWalletListModel.kt | 7 +++++-- .../tangem/features/wallet/utils/UserWalletsFetcher.kt | 6 +++++- .../feature/wallet/utils/DefaultUserWalletsFetcher.kt | 9 +++++++-- .../connections/utils/WcUserWalletsFetcher.kt | 6 +++++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 9f4e9c74ab..6af5b54fe3 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -33,8 +33,11 @@ internal class UserWalletListModel @Inject constructor( ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) - private val userWalletsFetcher = userWalletsFetcherFactory - .create(messageSender) { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) } + private val userWalletsFetcher = userWalletsFetcherFactory.create( + messageSender = messageSender, + onlyMultiCurrency = false, + onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) }, + ) val state: MutableStateFlow = MutableStateFlow( value = UserWalletListUM( diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt index 6753ea5dec..361843f8d3 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt @@ -11,6 +11,10 @@ interface UserWalletsFetcher { val userWallets: Flow> interface Factory { - fun create(messageSender: UiMessageSender, onWalletClick: (UserWalletId) -> Unit): UserWalletsFetcher + fun create( + messageSender: UiMessageSender, + onlyMultiCurrency: Boolean, + onWalletClick: (UserWalletId) -> Unit, + ): UserWalletsFetcher } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt index f0b457cbca..a4104329ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt @@ -12,7 +12,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce -import com.tangem.feature.wallet.impl.R import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.core.utils.toLce @@ -22,9 +21,11 @@ import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.feature.wallet.impl.R import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.operations.attestation.ArtworkSize import dagger.assisted.Assisted @@ -43,13 +44,16 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @Assisted private val onWalletClick: (UserWalletId) -> Unit, @Assisted private val messageSender: UiMessageSender, + @Assisted private val onlyMultiCurrency: Boolean, private val getCardImageUseCase: GetCardImageUseCase, ) : UserWalletsFetcher { private var loadedArtworks: HashMap = hashMapOf() + private val walletsFlow = + if (onlyMultiCurrency) getWalletsUseCase().map { it.filter { it.isMultiCurrency } } else getWalletsUseCase() @OptIn(ExperimentalCoroutinesApi::class) - override val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> + override val userWallets: Flow> = walletsFlow.transformLatest { wallets -> val uiModels = UserWalletItemUMConverter(onClick = { onWalletClick(it) }).convertList(wallets) .toImmutableList() @@ -145,6 +149,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( interface Factory : UserWalletsFetcher.Factory { override fun create( messageSender: UiMessageSender, + onlyMultiCurrency: Boolean, onWalletClick: (UserWalletId) -> Unit, ): DefaultUserWalletsFetcher } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt index 93bded7a85..a09f82e47f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt @@ -26,7 +26,11 @@ internal class WcUserWalletsFetcher( private val onWalletSelected: (UserWalletId) -> Unit, ) { - private val userWalletsFetcher = userWalletsFetcherFactory.create(messageSender) { onWalletSelected(it) } + private val userWalletsFetcher = userWalletsFetcherFactory.create( + messageSender = messageSender, + onlyMultiCurrency = true, + onWalletClick = { onWalletSelected(it) }, + ) @OptIn(ExperimentalCoroutinesApi::class) val userWallets: Flow> = userWalletsFetcher.userWallets From 8aa6ce01009be0f89f2a361a7fb16115114db180 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 13:39:08 +0500 Subject: [PATCH 82/99] Updated on 2026-08-14 --- .../send/v2/api/FeeSelectorBlockComponent.kt | 3 + .../send/v2/api/FeeSelectorComponent.kt | 9 +-- .../api/callbacks/FeeSelectorModelCallback.kt | 7 +++ .../send/v2/api/entity}/CustomFeeFieldUM.kt | 2 +- .../send/v2/api}/entity/FeeSelectorUM.kt | 27 +++----- .../send/v2/api/params/FeeSelectorParams.kt | 20 ++++-- .../DefaultFeeSelectorBlockComponent.kt | 28 ++++++--- .../DefaultFeeSelectorComponent.kt | 16 ++--- .../feeselector/model/FeeSelectorIntents.kt | 17 +++++ .../v2/feeselector/model/FeeSelectorModel.kt | 40 ++++++------ .../model/transformers/FeeItemConverter.kt | 9 ++- .../FeeItemSelectedTransformer.kt | 4 +- .../FeeSelectorCustomFieldConverter.kt | 16 ++--- .../FeeSelectorErrorTransformer.kt | 4 +- .../FeeSelectorLoadedTransformer.kt | 25 ++++---- .../ui/FeeSelectorModalBottomSheet.kt | 62 ++++++++++--------- .../fee/model/converters/FeeConverter.kt | 2 +- .../converters/SendFeeCustomFieldConverter.kt | 2 +- .../converters/custom/CustomFeeConverter.kt | 2 +- .../bitcoin/BitcoinCustomFeeConverter.kt | 2 +- .../BaseEthereumCustomFeeConverter.kt | 2 +- .../ethereum/EthereumCustomFeeConverter.kt | 2 +- .../ethereum/EthereumEIPCustomFeeConverter.kt | 2 +- .../EthereumLegacyCustomFeeConverter.kt | 2 +- .../custom/kaspa/KaspaCustomFeeConverter.kt | 2 +- .../v2/subcomponents/fee/ui/SendCustomFee.kt | 2 +- .../fee/ui/state/FeeSelectorUM.kt | 1 + .../v2/subcomponents/fee/ui/state/FeeUM.kt | 1 + .../routing/DefaultWcRoutingComponent.kt | 3 + .../PreviewFeeSelectorBlockComponent.kt | 5 ++ .../WcCommonTransactionComponentDelegate.kt | 3 +- .../components/common/WcNavigationUtils.kt | 18 ++++++ .../send/WcSendTransactionComponent.kt | 15 ++++- .../WcSendTransactionContainerComponent.kt | 3 + .../WcCommonTransactionUMConverter.kt | 3 + .../converter/WcSendTransactionUMConverter.kt | 12 +++- .../entity/common/WcTransactionActionsUM.kt | 1 + .../entity/common/WcTransactionFeeState.kt | 3 +- .../entity/send/WcSendTransactionUM.kt | 2 + .../model/WcSendTransactionModel.kt | 20 +++++- .../transaction/routes/WcTransactionRoutes.kt | 3 + .../ui/common/WcSendTransactionItems.kt | 16 ++++- .../send/WcSendTransactionModalBottomSheet.kt | 2 +- 43 files changed, 274 insertions(+), 146 deletions(-) create mode 100644 features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/callbacks/FeeSelectorModelCallback.kt rename features/send-v2/{impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state => api/src/main/java/com/tangem/features/send/v2/api/entity}/CustomFeeFieldUM.kt (90%) rename features/send-v2/{impl/src/main/java/com/tangem/features/send/v2/feeselector => api/src/main/java/com/tangem/features/send/v2/api}/entity/FeeSelectorUM.kt (58%) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt index adbe917341..5d2744f758 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt @@ -2,9 +2,12 @@ package com.tangem.features.send.v2.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams interface FeeSelectorBlockComponent : ComposableContentComponent { + fun updateState(feeSelectorUM: FeeSelectorUM) + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt index e3a8a20cc7..984f411097 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt @@ -2,11 +2,8 @@ package com.tangem.features.send.v2.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.v2.api.params.FeeSelectorParams -interface FeeSelectorComponent : ComposableContentComponent, ComposableBottomSheetComponent { - data class Params(val appCurrency: AppCurrency) - - interface Factory : ComponentFactory +interface FeeSelectorComponent : ComposableBottomSheetComponent { + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/callbacks/FeeSelectorModelCallback.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/callbacks/FeeSelectorModelCallback.kt new file mode 100644 index 0000000000..fddf1b5f57 --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/callbacks/FeeSelectorModelCallback.kt @@ -0,0 +1,7 @@ +package com.tangem.features.send.v2.api.callbacks + +import com.tangem.features.send.v2.api.entity.FeeSelectorUM + +interface FeeSelectorModelCallback { + fun onFeeResult(feeSelectorUM: FeeSelectorUM) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/CustomFeeFieldUM.kt similarity index 90% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/CustomFeeFieldUM.kt index cb3a73dae0..d080949eb6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/CustomFeeFieldUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.v2.subcomponents.fee.ui.state +package com.tangem.features.send.v2.api.entity import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt similarity index 58% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt index 5d5272f9e0..e71f997e18 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt @@ -1,32 +1,23 @@ -package com.tangem.features.send.v2.feeselector.entity +package com.tangem.features.send.v2.api.entity import androidx.compose.runtime.Immutable import com.tangem.blockchain.common.transaction.Fee import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal import java.math.BigInteger @Immutable -internal sealed class FeeSelectorUM { +sealed class FeeSelectorUM { - abstract val doneButtonConfig: PrimaryButtonConfig + data object Loading : FeeSelectorUM() - data class Loading(private val onDone: () -> Unit) : FeeSelectorUM() { - override val doneButtonConfig = PrimaryButtonConfig(enabled = false, onClick = onDone) - } - - data class Error(val error: GetFeeError, private val onDone: () -> Unit) : FeeSelectorUM() { - override val doneButtonConfig = PrimaryButtonConfig(enabled = false, onClick = onDone) - } + data class Error(val error: GetFeeError) : FeeSelectorUM() data class Content( - override val doneButtonConfig: PrimaryButtonConfig, val feeItems: ImmutableList, - val onFeeSelected: (FeeItem) -> Unit, val selectedFeeItem: FeeItem, val isFeeApproximate: Boolean, val feeFiatRateUM: FeeFiatRateUM?, @@ -36,18 +27,20 @@ internal sealed class FeeSelectorUM { ) : FeeSelectorUM() } -internal data class PrimaryButtonConfig(val enabled: Boolean, val onClick: () -> Unit) - @Immutable -internal data class FeeFiatRateUM( +data class FeeFiatRateUM( val rate: BigDecimal, val appCurrency: AppCurrency, ) @Immutable -internal sealed class FeeItem { +sealed class FeeItem { abstract val fee: Fee + fun isSame(other: FeeItem): Boolean { + return this::class == other::class + } + data class Suggested(val title: TextReference, override val fee: Fee) : FeeItem() data class Slow(override val fee: Fee) : FeeItem() data class Market(override val fee: Fee) : FeeItem() diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt index 946936b160..aac9799e08 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt @@ -4,20 +4,32 @@ import arrow.core.Either import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.models.network.Network import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback +import com.tangem.features.send.v2.api.entity.FeeSelectorUM sealed class FeeSelectorParams { + abstract val state: FeeSelectorUM abstract val onLoadFee: suspend () -> Either - abstract val network: Network abstract val cryptoCurrencyStatus: CryptoCurrencyStatus + abstract val callback: FeeSelectorModelCallback + abstract val suggestedFeeState: SuggestedFeeState data class FeeSelectorBlockParams( + override val state: FeeSelectorUM, override val onLoadFee: suspend () -> Either, - override val network: Network, override val cryptoCurrencyStatus: CryptoCurrencyStatus, - val suggestedFeeState: SuggestedFeeState, + override val callback: FeeSelectorModelCallback, + override val suggestedFeeState: SuggestedFeeState, + ) : FeeSelectorParams() + + data class FeeSelectorDetailsParams( + override val state: FeeSelectorUM, + override val onLoadFee: suspend () -> Either, + override val cryptoCurrencyStatus: CryptoCurrencyStatus, + override val callback: FeeSelectorModelCallback, + override val suggestedFeeState: SuggestedFeeState, ) : FeeSelectorParams() sealed class SuggestedFeeState { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index e619f49d9b..28f80f7202 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -33,17 +33,18 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.entity.FeeFiatRateUM +import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams -import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateUM -import com.tangem.features.send.v2.feeselector.entity.FeeItem -import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM -import com.tangem.features.send.v2.feeselector.entity.PrimaryButtonConfig import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel import com.tangem.features.send.v2.impl.R import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import java.math.BigDecimal internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( @@ -53,6 +54,16 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( private val model: FeeSelectorModel = getOrCreateModel(params = params) + init { + model.uiState + .onEach(params.callback::onFeeResult) + .launchIn(componentScope) + } + + override fun updateState(feeSelectorUM: FeeSelectorUM) { + model.updateState(feeSelectorUM) + } + @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() @@ -125,12 +136,13 @@ private fun RowScope.FeeLoading() { @Composable private fun RowScope.FeeContent(state: FeeSelectorUM.Content) { + val fiatRate = state.feeFiatRateUM EllipsisText( - text = if (state.feeFiatRateUM != null) { + text = if (fiatRate != null) { getFiatString( value = state.selectedFeeItem.fee.amount.value, - rate = state.feeFiatRateUM.rate, - appCurrency = state.feeFiatRateUM.appCurrency, + rate = fiatRate.rate, + appCurrency = fiatRate.appCurrency, approximate = state.isFeeApproximate, ) } else { @@ -167,9 +179,7 @@ private fun FeeSelectorBlockContent_Preview() { FeeSelectorBlockContent( modifier = Modifier.fillMaxWidth(), state = FeeSelectorUM.Content( - doneButtonConfig = PrimaryButtonConfig(enabled = true, onClick = {}), feeItems = persistentListOf(feeItem), - onFeeSelected = {}, selectedFeeItem = feeItem, isFeeApproximate = false, feeFiatRateUM = FeeFiatRateUM( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt index 925d689d4f..0166fadf8f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt @@ -1,13 +1,12 @@ package com.tangem.features.send.v2.feeselector -import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.send.v2.api.FeeSelectorComponent +import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel import com.tangem.features.send.v2.feeselector.ui.FeeSelectorModalBottomSheet import dagger.assisted.Assisted @@ -16,7 +15,7 @@ import dagger.assisted.AssistedInject internal class DefaultFeeSelectorComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted private val params: FeeSelectorComponent.Params, + @Assisted private val params: FeeSelectorParams.FeeSelectorDetailsParams, ) : FeeSelectorComponent, AppComponentContext by appComponentContext { private val model: FeeSelectorModel = getOrCreateModel(params = params) @@ -27,22 +26,15 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor( @Composable override fun BottomSheet() { - FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = TODO()) - } - - // Temporary workaround, to use test this component - @Composable - override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() - BackHandler(onBack = router::pop) - FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = state) + FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = state, feeSelectorIntents = model) } @AssistedFactory interface Factory : FeeSelectorComponent.Factory { override fun create( context: AppComponentContext, - params: FeeSelectorComponent.Params, + params: FeeSelectorParams.FeeSelectorDetailsParams, ): DefaultFeeSelectorComponent } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt new file mode 100644 index 0000000000..96c3867911 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt @@ -0,0 +1,17 @@ +package com.tangem.features.send.v2.feeselector.model + +import com.tangem.features.send.v2.api.entity.FeeItem + +internal interface FeeSelectorIntents { + fun onFeeItemSelected(feeItem: FeeItem) + fun onCustomFeeValueChange(index: Int, value: String) + fun onCustomFeeNextClick() + fun onDoneClick() +} + +internal class StubFeeSelectorIntents : FeeSelectorIntents { + override fun onFeeItemSelected(feeItem: FeeItem) {} + override fun onCustomFeeValueChange(index: Int, value: String) {} + override fun onCustomFeeNextClick() {} + override fun onDoneClick() {} +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index 7e2b1df414..11e7a9037a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -10,9 +10,9 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams -import com.tangem.features.send.v2.feeselector.entity.FeeItem -import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM import com.tangem.features.send.v2.feeselector.model.transformers.FeeItemSelectedTransformer import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorErrorTransformer import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorLoadedTransformer @@ -31,19 +31,23 @@ internal class FeeSelectorModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, -) : Model() { +) : Model(), FeeSelectorIntents { - private val params = paramsContainer.require() + private val params = paramsContainer.require() private var appCurrency: AppCurrency = AppCurrency.Default val uiState: StateFlow - field = MutableStateFlow(FeeSelectorUM.Loading(onDone = ::onDone)) + field = MutableStateFlow(params.state) init { initAppCurrency() loadFee() } + fun updateState(feeSelectorUM: FeeSelectorUM) { + uiState.value = feeSelectorUM + } + fun dismiss() { router.pop() } @@ -58,18 +62,16 @@ internal class FeeSelectorModel @Inject constructor( modelScope.launch { params.onLoadFee() .fold( - ifLeft = { uiState.update(FeeSelectorErrorTransformer(it)) }, - ifRight = { + ifLeft = { error -> uiState.update(FeeSelectorErrorTransformer(error)) }, + ifRight = { fee -> uiState.update( FeeSelectorLoadedTransformer( cryptoCurrencyStatus = params.cryptoCurrencyStatus, appCurrency = appCurrency, - fees = it, + fees = fee, suggestedFeeState = params.suggestedFeeState, - isFeeApproximate = isFeeApproximate(it.normal.amount.type), - onFeeSelected = ::onFeeItemSelected, - onCustomFeeValueChange = ::onCustomFeeValueChange, - onNextClick = ::onNextClick, + isFeeApproximate = isFeeApproximate(fee.normal.amount.type), + feeSelectorIntents = this@FeeSelectorModel, ), ) }, @@ -78,24 +80,24 @@ internal class FeeSelectorModel @Inject constructor( } private fun isFeeApproximate(amountType: AmountType): Boolean { - val networkId = params.network.id + val networkId = params.cryptoCurrencyStatus.currency.network.id return isFeeApproximateUseCase(networkId = networkId, amountType = amountType) } - private fun onFeeItemSelected(feeItem: FeeItem) { + override fun onFeeItemSelected(feeItem: FeeItem) { uiState.update(FeeItemSelectedTransformer(feeItem)) } - @Suppress("UnusedPrivateMember") - private fun onCustomFeeValueChange(index: Int, value: String) { + override fun onCustomFeeValueChange(index: Int, value: String) { // TODO: [REDACTED_JIRA] } - private fun onNextClick() { + override fun onCustomFeeNextClick() { // TODO: [REDACTED_JIRA] } - private fun onDone() { - // TODO: [REDACTED_JIRA] + override fun onDoneClick() { + params.callback.onFeeResult(uiState.value) + dismiss() } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt index 6ec3832f8f..a4657cef69 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt @@ -5,7 +5,8 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.api.params.FeeSelectorParams -import com.tangem.features.send.v2.feeselector.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -13,15 +14,13 @@ import kotlinx.collections.immutable.toImmutableList internal class FeeItemConverter( private val suggestedFeeState: FeeSelectorParams.SuggestedFeeState, private val normalFee: Fee, - private val onCustomFeeValueChange: (Int, String) -> Unit, - private val onNextClick: () -> Unit, + private val feeSelectorIntents: FeeSelectorIntents, private val appCurrency: AppCurrency, cryptoCurrencyStatus: CryptoCurrencyStatus, ) : Converter> { private val customFeeFieldConverter = FeeSelectorCustomFieldConverter( - onCustomFeeValueChange = onCustomFeeValueChange, - onNextClick = onNextClick, + feeSelectorIntents = feeSelectorIntents, appCurrency = appCurrency, feeCryptoCurrencyStatus = cryptoCurrencyStatus, normalFee = normalFee, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemSelectedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemSelectedTransformer.kt index f8b81f56e7..dee3471c75 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemSelectedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemSelectedTransformer.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.feeselector.model.transformers -import com.tangem.features.send.v2.feeselector.entity.FeeItem -import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.utils.transformer.Transformer internal class FeeItemSelectedTransformer(private val selectedFeeItem: FeeItem) : Transformer { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt index 3357fdd5d9..d779c46037 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt @@ -8,15 +8,15 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.utils.converter.TwoWayConverter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf internal class FeeSelectorCustomFieldConverter( - private val onCustomFeeValueChange: (Int, String) -> Unit, - private val onNextClick: () -> Unit, + private val feeSelectorIntents: FeeSelectorIntents, private val appCurrency: AppCurrency, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, private val normalFee: Fee, @@ -24,8 +24,8 @@ internal class FeeSelectorCustomFieldConverter( private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { EthereumCustomFeeConverter( - onCustomFeeValueChange = onCustomFeeValueChange, - onNextClick = onNextClick, + onCustomFeeValueChange = feeSelectorIntents::onCustomFeeValueChange, + onNextClick = feeSelectorIntents::onCustomFeeNextClick, appCurrency = appCurrency, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) @@ -33,8 +33,8 @@ internal class FeeSelectorCustomFieldConverter( private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { BitcoinCustomFeeConverter( - onCustomFeeValueChange = onCustomFeeValueChange, - onNextClick = onNextClick, + onCustomFeeValueChange = feeSelectorIntents::onCustomFeeValueChange, + onNextClick = feeSelectorIntents::onCustomFeeNextClick, appCurrency = appCurrency, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) @@ -42,7 +42,7 @@ internal class FeeSelectorCustomFieldConverter( private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { KaspaCustomFeeConverter( - onCustomFeeValueChange = onCustomFeeValueChange, + onCustomFeeValueChange = feeSelectorIntents::onCustomFeeValueChange, appCurrency = appCurrency, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorErrorTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorErrorTransformer.kt index d4340701a9..80386d5c77 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorErrorTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorErrorTransformer.kt @@ -1,12 +1,12 @@ package com.tangem.features.send.v2.feeselector.model.transformers import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.utils.transformer.Transformer internal class FeeSelectorErrorTransformer(private val error: GetFeeError) : Transformer { override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { - return FeeSelectorUM.Error(error = error, onDone = prevState.doneButtonConfig.onClick) + return FeeSelectorUM.Error(error = error) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt index a33f5e831f..a4aee4e3c2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt @@ -3,10 +3,11 @@ package com.tangem.features.send.v2.feeselector.model.transformers import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.FeeFiatRateUM +import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams -import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateUM -import com.tangem.features.send.v2.feeselector.entity.FeeItem -import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList @@ -17,29 +18,31 @@ internal class FeeSelectorLoadedTransformer( private val fees: TransactionFee, private val suggestedFeeState: FeeSelectorParams.SuggestedFeeState, private val isFeeApproximate: Boolean, - private val onFeeSelected: (FeeItem) -> Unit, - private val onCustomFeeValueChange: (Int, String) -> Unit, - private val onNextClick: () -> Unit, + private val feeSelectorIntents: FeeSelectorIntents, ) : Transformer { private val feeItemsConverter = FeeItemConverter( suggestedFeeState = suggestedFeeState, normalFee = fees.normal, - onCustomFeeValueChange = onCustomFeeValueChange, - onNextClick = onNextClick, + feeSelectorIntents = feeSelectorIntents, appCurrency = appCurrency, cryptoCurrencyStatus = cryptoCurrencyStatus, ) override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { val feeItems: ImmutableList = feeItemsConverter.convert(fees) - val selectedFee = feeItems.find { it is FeeItem.Suggested } ?: feeItems.first { it is FeeItem.Market } + + val selectedFee = when (prevState) { + is FeeSelectorUM.Content -> feeItems.first { it.isSame(prevState.selectedFeeItem) } + is FeeSelectorUM.Error, + FeeSelectorUM.Loading, + -> feeItems.find { it is FeeItem.Suggested } ?: feeItems.first { it is FeeItem.Market } + } + return FeeSelectorUM.Content( - doneButtonConfig = prevState.doneButtonConfig.copy(enabled = true), feeItems = feeItems, selectedFeeItem = selectedFee, isFeeApproximate = isFeeApproximate, - onFeeSelected = onFeeSelected, feeFiatRateUM = cryptoCurrencyStatus.value.fiatRate?.let { rate -> FeeFiatRateUM( rate = rate, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 9acf05e797..c2decb1bdb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -48,12 +47,13 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateUM -import com.tangem.features.send.v2.feeselector.entity.FeeItem -import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM -import com.tangem.features.send.v2.feeselector.entity.PrimaryButtonConfig +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.FeeFiatRateUM +import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents +import com.tangem.features.send.v2.feeselector.model.StubFeeSelectorIntents import com.tangem.features.send.v2.impl.R -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -61,7 +61,11 @@ import java.math.BigDecimal import java.math.BigInteger @Composable -internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () -> Unit) { +internal fun FeeSelectorModalBottomSheet( + state: FeeSelectorUM, + feeSelectorIntents: FeeSelectorIntents, + onDismiss: () -> Unit, +) { if (state !is FeeSelectorUM.Content) return TangemModalBottomSheetWithFooter( @@ -81,6 +85,7 @@ internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () -> content = { FeeSelectorItems( state = state, + feeSelectorIntents = feeSelectorIntents, modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp), ) }, @@ -90,8 +95,7 @@ internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () -> .fillMaxWidth() .padding(16.dp), text = stringResourceSafe(R.string.common_done), - onClick = state.doneButtonConfig.onClick, - enabled = state.doneButtonConfig.enabled, + onClick = feeSelectorIntents::onDoneClick, ) }, ) @@ -99,10 +103,15 @@ internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () -> @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable -private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = Modifier) { +private fun FeeSelectorItems( + state: FeeSelectorUM.Content, + feeSelectorIntents: FeeSelectorIntents, + modifier: Modifier = Modifier, +) { Column(modifier = modifier) { + val feeFiatRateUM = state.feeFiatRateUM state.feeItems.fastForEachIndexed { index, item -> - val isSelected = item == state.selectedFeeItem + val isSelected = item.isSame(state.selectedFeeItem) val lastItem = index == state.feeItems.size - 1 val iconTint by animateColorAsState( targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary, @@ -116,7 +125,6 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = }, label = "Fee selector icon background change", ) - val onSelect by rememberUpdatedState(state.onFeeSelected) val itemModifier = Modifier .fillMaxWidth() .background(TangemTheme.colors.background.primary) @@ -135,7 +143,7 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = Modifier }, ) - .clickableSingle(onClick = { onSelect(item) }) + .clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) }) when (item) { is FeeItem.Suggested -> RegularFeeItemContent( modifier = itemModifier, @@ -151,11 +159,11 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = ).fee(canBeLower = state.isFeeApproximate) }, ), - postDot = if (state.feeFiatRateUM != null) { + postDot = if (feeFiatRateUM != null) { getFiatReference( value = item.fee.amount.value, - rate = state.feeFiatRateUM.rate, - appCurrency = state.feeFiatRateUM.appCurrency, + rate = feeFiatRateUM.rate, + appCurrency = feeFiatRateUM.appCurrency, ) } else { null @@ -177,11 +185,11 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = ).fee(canBeLower = state.isFeeApproximate) }, ), - postDot = if (state.feeFiatRateUM != null) { + postDot = if (feeFiatRateUM != null) { getFiatReference( value = item.fee.amount.value, - rate = state.feeFiatRateUM.rate, - appCurrency = state.feeFiatRateUM.appCurrency, + rate = feeFiatRateUM.rate, + appCurrency = feeFiatRateUM.appCurrency, ) } else { null @@ -203,11 +211,11 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = ).fee(canBeLower = state.isFeeApproximate) }, ), - postDot = if (state.feeFiatRateUM != null) { + postDot = if (feeFiatRateUM != null) { getFiatReference( value = item.fee.amount.value, - rate = state.feeFiatRateUM.rate, - appCurrency = state.feeFiatRateUM.appCurrency, + rate = feeFiatRateUM.rate, + appCurrency = feeFiatRateUM.appCurrency, ) } else { null @@ -229,11 +237,11 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = ).fee(canBeLower = state.isFeeApproximate) }, ), - postDot = if (state.feeFiatRateUM != null) { + postDot = if (feeFiatRateUM != null) { getFiatReference( value = item.fee.amount.value, - rate = state.feeFiatRateUM.rate, - appCurrency = state.feeFiatRateUM.appCurrency, + rate = feeFiatRateUM.rate, + appCurrency = feeFiatRateUM.appCurrency, ) } else { null @@ -466,14 +474,13 @@ private fun FeeSelectorBS_Preview( state: FeeSelectorUM.Content, ) { TangemThemePreview { - FeeSelectorModalBottomSheet(onDismiss = {}, state = state) + FeeSelectorModalBottomSheet(onDismiss = {}, state = state, feeSelectorIntents = StubFeeSelectorIntents()) } } private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider( collection = listOf( FeeSelectorUM.Content( - doneButtonConfig = PrimaryButtonConfig(enabled = true, onClick = {}), feeItems = persistentListOf( FeeItem.Suggested( title = stringReference("Suggested by Tangem"), @@ -484,7 +491,6 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))), customFeeItem, ), - onFeeSelected = {}, // selectedFeeItem = FeeItem.Market( // amount = Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum), // ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt index 2a5863ed1d..d66b9376e2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt index 1f6cfc86d7..820e44dbb4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt @@ -9,7 +9,7 @@ import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.utils.converter.TwoWayConverter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt index 4250d48580..d6b491a71f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom import com.tangem.blockchain.common.transaction.Fee -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt index feaed422ac..3d65f6b2b1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt @@ -14,7 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.lib.crypto.BlockchainUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt index c0828d3d60..96f7c1740e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt @@ -2,7 +2,7 @@ package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.et import com.tangem.blockchain.common.transaction.Fee import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList /** diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt index 674ef7d066..235638c7ff 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt @@ -12,7 +12,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt index 24d9dab7fc..6a380e8e4c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt @@ -18,7 +18,7 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt index ac680946cb..759cf62c33 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt @@ -18,7 +18,7 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt index 4dfd766663..c3dd23ba75 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt @@ -13,7 +13,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt index 95173f58c6..2486f3bbd9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt @@ -17,7 +17,7 @@ import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.impl.R -import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt index 46bbd7c4d9..563ef43c17 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.math.BigInteger diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt index 592570e0b5..1b98e4c5de 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.v2.subcomponents.fee.ui.state import androidx.compose.runtime.Stable import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt index 84a1d7c20d..12f96545e8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt @@ -15,6 +15,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.FeeSelectorComponent import com.tangem.features.walletconnect.components.WcRoutingComponent import com.tangem.features.walletconnect.connections.components.AlertsComponent import com.tangem.features.walletconnect.connections.components.WcPairComponent @@ -31,6 +32,7 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( @Assisted private val appComponentContext: AppComponentContext, @Assisted params: Unit, private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, + private val feeSelectorComponentFactory: FeeSelectorComponent.Factory, ) : AppComponentContext by appComponentContext, WcRoutingComponent { private val model: WcRoutingModel = getOrCreateModel() @@ -73,6 +75,7 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), feeSelectorBlockComponentFactory = feeSelectorBlockComponentFactory, + feeSelectorComponentFactory = feeSelectorComponentFactory, ) is WcInnerRoute.Pair -> WcPairComponent( appComponentContext = childContext, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt index 00c218f576..c17c778409 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt @@ -15,9 +15,14 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.walletconnect.impl.R internal class PreviewFeeSelectorBlockComponent : FeeSelectorBlockComponent { + override fun updateState(feeSelectorUM: FeeSelectorUM) { + /** No-op */ + } + @Composable override fun Content(modifier: Modifier) { Row( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt index 1be9296325..1ddad75e91 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt @@ -61,8 +61,9 @@ internal abstract class WcCommonTransactionComponentDelegate( is WcTransactionRoutes.TransactionRequestInfo, is WcTransactionRoutes.Alert, is WcTransactionRoutes.CustomAllowance, + is WcTransactionRoutes.SelectFee, -> stackNavigation?.pop() - else -> Unit + null -> Unit } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt index c38c4ce6bd..7bbe97cb99 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt @@ -2,6 +2,8 @@ package com.tangem.features.walletconnect.transaction.components.common import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorComponent +import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.walletconnect.connections.components.AlertsComponentV2 import com.tangem.features.walletconnect.connections.utils.WcAlertsFactory.createCommonTransactionAppInfoAlertUM import com.tangem.features.walletconnect.transaction.components.send.WcCustomAllowanceComponent @@ -14,6 +16,7 @@ internal fun getWcCommonScreen( appComponentContext: AppComponentContext, transactionScreenConverter: () -> ComposableBottomSheetComponent, model: WcCommonTransactionModel, + feeSelectorComponentFactory: FeeSelectorComponent.Factory? = null, ): ComposableBottomSheetComponent { return when (config) { is WcTransactionRoutes.Transaction -> transactionScreenConverter() @@ -32,5 +35,20 @@ internal fun getWcCommonScreen( (model as? WcSendTransactionModel)?.onClickDoneCustomAllowance(value, isUnlimited) }, ) + is WcTransactionRoutes.SelectFee -> { + requireNotNull(feeSelectorComponentFactory) { "feeSelectorComponentFactory must not be null" } + val model = model as? WcSendTransactionModel ?: error("model must be WcSendTransactionModel") + val state = model.uiState.value ?: error("in this step state should be not null") + feeSelectorComponentFactory.create( + context = appComponentContext, + params = FeeSelectorParams.FeeSelectorDetailsParams( + state = state.feeSelectorUM, + onLoadFee = model::loadFee, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + callback = model, + suggestedFeeState = model.suggestedFeeState, + ), + ) + } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt index 4a9271ab98..e5cd9d4b3a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt @@ -3,10 +3,12 @@ package com.tangem.features.walletconnect.transaction.components.send import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.essenty.lifecycle.doOnResume import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel import com.tangem.features.walletconnect.transaction.ui.send.WcSendTransactionModalBottomSheet @@ -17,17 +19,28 @@ internal class WcSendTransactionComponent( ) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { private val feeSelectorBlockComponent by lazy { + val state = requireNotNull(model.uiState.value) { "in this step state should be not null" } feeSelectorBlockComponentFactory.create( context = appComponentContext, params = FeeSelectorParams.FeeSelectorBlockParams( + state = state.feeSelectorUM, onLoadFee = model::loadFee, - network = model.useCase.network, cryptoCurrencyStatus = model.cryptoCurrencyStatus, + callback = model, suggestedFeeState = model.suggestedFeeState, ), ) } + init { + lifecycle.doOnResume { + val state = model.uiState.value + if (state?.transaction?.feeState is WcTransactionFeeState.Success) { + feeSelectorBlockComponent.updateState(state.feeSelectorUM) + } + } + } + override fun dismiss() { model.dismiss() } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt index 5de06f65d4..3b232b8df1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt @@ -6,6 +6,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.FeeSelectorComponent import com.tangem.features.walletconnect.transaction.components.common.WcCommonTransactionComponentDelegate import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.common.getWcCommonScreen @@ -16,6 +17,7 @@ internal class WcSendTransactionContainerComponent( appComponentContext: AppComponentContext, params: WcTransactionModelParams, private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, + private val feeSelectorComponentFactory: FeeSelectorComponent.Factory, ) : WcCommonTransactionComponentDelegate(appComponentContext) { private val model: WcSendTransactionModel = getOrCreateModel(params = params) @@ -38,6 +40,7 @@ internal class WcSendTransactionContainerComponent( appComponentContext = appComponentContext, transactionScreenConverter = { createTransactionComponent(appComponentContext) }, model = model, + feeSelectorComponentFactory = feeSelectorComponentFactory, ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcCommonTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcCommonTransactionUMConverter.kt index 84dc4e2c49..84d4f29ebc 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcCommonTransactionUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcCommonTransactionUMConverter.kt @@ -6,6 +6,7 @@ import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignUseCase import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM import com.tangem.utils.converter.Converter @@ -45,6 +46,7 @@ internal class WcCommonTransactionUMConverter @Inject constructor( useCase = value.useCase, signState = value.signState, actions = value.actions, + feeSelectorUM = value.feeSelectorUM, ), ) else -> null @@ -55,5 +57,6 @@ internal class WcCommonTransactionUMConverter @Inject constructor( val useCase: WcSignUseCase<*>, val signState: WcSignState<*>, val actions: WcTransactionActionsUM, + val feeSelectorUM: FeeSelectorUM? = null, ) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt index ca1ba812c4..0127605b9c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt @@ -7,6 +7,7 @@ import com.tangem.domain.walletconnect.usecase.method.WcMutableFee import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM import com.tangem.features.walletconnect.transaction.entity.common.* @@ -37,13 +38,14 @@ internal class WcSendTransactionUMConverter @Inject constructor( onShowVerifiedAlert = value.actions.onShowVerifiedAlert, ), ), - feeState = constructFeeState(useCase = value.useCase), + feeState = constructFeeState(useCase = value.useCase, actions = value.actions), walletName = value.useCase.session.wallet.name, networkInfo = networkInfoUMConverter.convert(value.useCase.network), address = value.useCase.walletAddress.toShortAddressText(), estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(), isLoading = value.signState.domainStep == WcSignStep.Signing, ), + feeSelectorUM = value.feeSelectorUM ?: FeeSelectorUM.Loading, transactionRequestInfo = WcTransactionRequestInfoUM( blocks = buildList { add( @@ -80,15 +82,19 @@ internal class WcSendTransactionUMConverter @Inject constructor( else -> null } - private fun constructFeeState(useCase: WcTransactionUseCase): WcTransactionFeeState { + private fun constructFeeState( + useCase: WcTransactionUseCase, + actions: WcTransactionActionsUM, + ): WcTransactionFeeState { val mutableFee = useCase as? WcMutableFee ?: return WcTransactionFeeState.None val dAppFee = mutableFee.dAppFee() - return if (dAppFee != null) WcTransactionFeeState.Success(dAppFee) else WcTransactionFeeState.Loading + return WcTransactionFeeState.Success(dAppFee = dAppFee, onClick = actions.onShowFeeBottomSheet) } data class Input( val useCase: WcTransactionUseCase, val signState: WcSignState<*>, val actions: WcTransactionActionsUM, + val feeSelectorUM: FeeSelectorUM?, ) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionActionsUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionActionsUM.kt index fb0d401a7f..433587b43b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionActionsUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionActionsUM.kt @@ -8,4 +8,5 @@ internal data class WcTransactionActionsUM( val onDismiss: () -> Unit, val onSign: () -> Unit, val onCopy: () -> Unit, + val onShowFeeBottomSheet: () -> Unit = {}, ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionFeeState.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionFeeState.kt index 7c38bf7f48..8370077fd7 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionFeeState.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionFeeState.kt @@ -3,7 +3,6 @@ package com.tangem.features.walletconnect.transaction.entity.common import com.tangem.blockchain.common.transaction.Fee internal sealed class WcTransactionFeeState { - object Loading : WcTransactionFeeState() - data class Success(val fee: Fee) : WcTransactionFeeState() + data class Success(val dAppFee: Fee?, val onClick: () -> Unit) : WcTransactionFeeState() data object None : WcTransactionFeeState() } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt index 64c1ba82d3..cd85d9518d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.walletconnect.transaction.entity.send import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM @@ -13,6 +14,7 @@ internal data class WcSendTransactionUM( val transaction: WcSendTransactionItemUM, override val transactionRequestInfo: WcTransactionRequestInfoUM, val spendAllowance: WcSpendAllowanceUM? = null, + val feeSelectorUM: FeeSelectorUM, ) : WcCommonTransactionUM internal data class WcSendTransactionItemUM( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 20b19996a3..7e2951dbb6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -24,6 +24,8 @@ import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.usecase.method.* import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.walletconnect.connections.routing.WcInnerRoute import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams @@ -54,18 +56,18 @@ internal class WcSendTransactionModel @Inject constructor( private val blockAidUiConverter: WcSendAndReceiveBlockAidUiConverter, private val getFeeUseCase: GetFeeUseCase, private val getNetworkCoinUseCase: GetNetworkCoinStatusUseCase, -) : Model(), WcCommonTransactionModel { +) : Model(), WcCommonTransactionModel, FeeSelectorModelCallback { private val params = paramsContainer.require() private val _uiState = MutableStateFlow(null) - override val uiState: StateFlow = _uiState + override val uiState: StateFlow = _uiState.asStateFlow() val stackNavigation = StackNavigation() - internal var useCase: WcSignUseCase<*> by Delegates.notNull() internal var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() internal var suggestedFeeState: FeeSelectorParams.SuggestedFeeState = FeeSelectorParams.SuggestedFeeState.None + private var useCase: WcSignUseCase<*> by Delegates.notNull() private var signState: WcSignState<*> by Delegates.notNull() private var wcApproval: WcApproval? = null private var sign: () -> Unit = {} @@ -116,6 +118,12 @@ internal class WcSendTransactionModel @Inject constructor( } } + override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { + _uiState.update { it?.copy(feeSelectorUM = feeSelectorUM) } + val fee = (feeSelectorUM as? FeeSelectorUM.Content)?.selectedFeeItem?.fee ?: return + (useCase as? WcMutableFee)?.updateFee(fee) + } + suspend fun loadFee(): Either { val signModel = signState.signModel val transactionData = signModel as? TransactionData.Uncompiled ?: error("TransactionData must be Uncompiled") @@ -162,7 +170,9 @@ internal class WcSendTransactionModel @Inject constructor( onDismiss = { cancel(useCase) }, onSign = { onSign(securityCheck.getOrNull()) }, onCopy = { copyData(useCase.rawSdkRequest.request.params) }, + onShowFeeBottomSheet = ::onShowFeeBottomSheet, ), + feeSelectorUM = uiState.value?.feeSelectorUM, ), ) as? WcSendTransactionUM transactionUM = transactionUM?.copy( @@ -180,6 +190,10 @@ internal class WcSendTransactionModel @Inject constructor( stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo) } + private fun onShowFeeBottomSheet() { + stackNavigation.pushNew(WcTransactionRoutes.SelectFee) + } + private fun onSign(securityCheck: BlockAidTransactionCheck.Result?) { if (securityCheck?.result?.validation == ValidationResult.UNSAFE) { showMaliciousAlert(securityCheck.result.description) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt index 066a19eebc..4c23abfde5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt @@ -17,6 +17,9 @@ internal sealed class WcTransactionRoutes : TangemBottomSheetConfigContent, Rout @Serializable data object CustomAllowance : WcTransactionRoutes() + @Serializable + data object SelectFee : WcTransactionRoutes() + @Serializable data class Alert(val type: Type) : WcTransactionRoutes() { @Serializable diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt index e0495da0ba..db1407996f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt @@ -7,10 +7,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.divider.DividerWithPadding +import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM @@ -25,6 +27,12 @@ internal fun WcSendTransactionItems( modifier: Modifier = Modifier, address: String? = null, ) { + val onFeeBlockClicked = remember(feeState) { + when (feeState) { + WcTransactionFeeState.None -> null + is WcTransactionFeeState.Success -> feeState.onClick + } + } Column( modifier = modifier .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) @@ -55,7 +63,13 @@ internal fun WcSendTransactionItems( } DividerWithPadding(start = 40.dp, end = 12.dp) if (feeState != WcTransactionFeeState.None) { - feeSelectorBlockComponent.Content(Modifier) + feeSelectorBlockComponent.Content( + modifier = if (onFeeBlockClicked != null) { + Modifier.clickableSingle(onClick = onFeeBlockClicked) + } else { + Modifier + }, + ) } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index 0c56447574..57151c8e40 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -194,7 +194,7 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide ), walletName = "Tangem 2.0", networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - feeState = WcTransactionFeeState.Loading, + feeState = WcTransactionFeeState.None, address = "0x345FF...34FA", ), ), From eaf587e7b84b6af4860cee7f1d6e141d4cab8bd1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 12:07:19 +0300 Subject: [PATCH 83/99] Updated on 2026-08-14 --- .../DefaultWalletAddressServiceRepository.kt | 24 +++++++ .../WalletAddressServiceRepository.kt | 3 + .../transaction/error/ValidateAddressError.kt | 1 + .../usecase/ValidateWalletAddressUseCase.kt | 62 ++++++++++++------- .../send/v2/send/DefaultSendComponent.kt | 2 +- .../v2/send/confirm/model/SendConfirmModel.kt | 4 +- .../features/send/v2/send/model/SendModel.kt | 2 +- .../v2/sendnft/DefaultNFTSendComponent.kt | 2 +- .../confirm/model/NFTSendConfirmModel.kt | 4 +- .../send/v2/sendnft/model/NFTSendModel.kt | 2 +- ...dDestinationValidationResultTransformer.kt | 3 + .../ui/state/DestinationTextFieldUM.kt | 8 ++- 12 files changed, 85 insertions(+), 32 deletions(-) diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt index a85f05371d..067feb23d3 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt @@ -4,6 +4,8 @@ import android.net.Uri import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.near.NearWalletManager import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.NameResolver +import com.tangem.blockchain.common.ResolveAddressResult import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.models.network.Network import com.tangem.domain.transaction.WalletAddressServiceRepository @@ -20,6 +22,28 @@ class DefaultWalletAddressServiceRepository( private val dispatchers: CoroutineDispatcherProvider, ) : WalletAddressServiceRepository { + override suspend fun resolveAddress( + userWalletId: UserWalletId, + network: Network, + address: String, + ): ResolveAddressResult { + return withContext(dispatchers.io) { + val blockchain = network.toBlockchain() + + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + if (walletManager is NameResolver) { + walletManager.resolve(address) + } else { + ResolveAddressResult.NotSupported + } + } + } + override suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean = withContext(dispatchers.io) { val blockchain = network.toBlockchain() diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt index 02e97c1741..048ecf0ab1 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt @@ -1,5 +1,6 @@ package com.tangem.domain.transaction +import com.tangem.blockchain.common.ResolveAddressResult import com.tangem.domain.models.network.Network import com.tangem.domain.wallets.models.ParsedQrCode import com.tangem.domain.wallets.models.UserWalletId @@ -9,6 +10,8 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface WalletAddressServiceRepository { + suspend fun resolveAddress(userWalletId: UserWalletId, network: Network, address: String): ResolveAddressResult + suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean fun validateMemo(network: Network, memo: String): Boolean diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt index 8fc2ee7870..f8aff8be0d 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt @@ -8,6 +8,7 @@ sealed class AddressValidation { sealed class Success : AddressValidation() { data object Valid : Success() data object ValidXAddress : Success() + data class ValidNamedAddress(val blockchainAddress: String) : Success() } sealed class Error : AddressValidation() { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt index fda7a4282b..7963605ae2 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.transaction.usecase import arrow.core.left import arrow.core.right +import com.tangem.blockchain.common.ResolveAddressResult import com.tangem.domain.models.network.CryptoCurrencyAddress import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress @@ -24,44 +25,59 @@ class ValidateWalletAddressUseCase( userWalletId: UserWalletId, network: Network, address: String, - currencyAddress: Set?, - ): AddressValidationResult { - val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.rawId) - val isUtxoConsolidationAvailable = - walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network) - - val addressToValidate = decodedXAddress?.address ?: address - val isCurrentAddress = currencyAddress?.any { it.value == addressToValidate } ?: true - - val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable - val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) - - return when { - !isValidAddress -> AddressValidation.Error.InvalidAddress.left() - isForbidSelfSend -> AddressValidation.Error.AddressInWallet.left() - decodedXAddress != null -> AddressValidation.Success.ValidXAddress.right() - else -> AddressValidation.Success.Valid.right() - } - } + currencyAddresses: Set?, + ): AddressValidationResult = validateAddressInternal( + userWalletId, + network, + address, + isCurrentAddress = { toValidate -> + currencyAddresses?.any { it.value == toValidate } ?: true + }, + ) suspend operator fun invoke( userWalletId: UserWalletId, network: Network, address: String, senderAddresses: List, + ): AddressValidationResult = validateAddressInternal( + userWalletId, + network, + address, + isCurrentAddress = { toValidate -> + senderAddresses.any { it.address == toValidate } + }, + ) + + private suspend fun validateAddressInternal( + userWalletId: UserWalletId, + network: Network, + address: String, + isCurrentAddress: (String) -> Boolean, ): AddressValidationResult { val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.rawId) val isUtxoConsolidationAvailable = walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network) val addressToValidate = decodedXAddress?.address ?: address - val isCurrentAddress = senderAddresses.any { it.address == addressToValidate } - - val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable + val current = isCurrentAddress(addressToValidate) + val isForbidSelfSend = current && !isUtxoConsolidationAvailable val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) return when { - !isValidAddress -> AddressValidation.Error.InvalidAddress.left() + !isValidAddress -> { + val resolveAddressResult = walletAddressServiceRepository.resolveAddress( + userWalletId = userWalletId, + network = network, + address = addressToValidate, + ) + + if (resolveAddressResult is ResolveAddressResult.Resolved) { + AddressValidation.Success.ValidNamedAddress(resolveAddressResult.address).right() + } else { + AddressValidation.Error.InvalidAddress.left() + } + } isForbidSelfSend -> AddressValidation.Error.AddressInWallet.left() decodedXAddress != null -> AddressValidation.Success.ValidXAddress.right() else -> AddressValidation.Success.Valid.right() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 4f091935b8..491fcedcb7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -234,7 +234,7 @@ internal class DefaultSendComponent @AssistedInject constructor( private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent { val state = model.uiState.value val sendAmount = (state.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value + val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.actualAddress val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index d9d8e350dd..4110801b3d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -122,7 +122,7 @@ internal class SendConfirmModel @Inject constructor( enteredMemo = destinationUM?.memoTextField?.value, reduceAmountBy = amountState?.reduceAmountBy.orZero(), isIgnoreReduce = amountState?.isIgnoreReduce == true, - enteredDestination = destinationUM?.addressTextField?.value, + enteredDestination = destinationUM?.addressTextField?.actualAddress, fee = feeSelectorUM?.selectedFee, feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error, ) @@ -316,7 +316,7 @@ internal class SendConfirmModel @Inject constructor( private fun verifyAndSendTransaction() { val amountValue = amountState?.amountTextField?.cryptoAmount?.value ?: return - val destination = destinationUM?.addressTextField?.value ?: return + val destination = destinationUM?.addressTextField?.actualAddress ?: return val memo = destinationUM?.memoTextField?.value val fee = feeSelectorUM?.selectedFee val feeValue = fee?.amount?.value ?: return diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 2ea110f6a2..17d9046d23 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -165,7 +165,7 @@ internal class SendModel @Inject constructor( } else { val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") val amountUM = uiState.value.amountUM as? AmountState.Data ?: error("Invalid amount") - val enteredDestinationAddress = destinationUM.addressTextField.value + val enteredDestinationAddress = destinationUM.addressTextField.actualAddress val enteredMemo = destinationUM.memoTextField?.value val enteredAmount = amountUM.amountTextField.cryptoAmount.value ?: error("Invalid amount") diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index fd86d873b6..2c8f31fd10 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -157,7 +157,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent { val state = model.uiState.value - val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value + val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.actualAddress return if (destinationAddress != null) { SendFeeComponent( appComponentContext = factoryContext, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index e9121d6a4c..e0c6b167d7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -108,7 +108,7 @@ internal class NFTSendConfirmModel @Inject constructor( val confirmData: ConfirmData get() = ConfirmData( - enteredDestination = destinationUM?.addressTextField?.value, + enteredDestination = destinationUM?.addressTextField?.actualAddress, enteredMemo = destinationUM?.memoTextField?.value, fee = feeSelectorUM?.selectedFee, feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error, @@ -258,7 +258,7 @@ internal class NFTSendConfirmModel @Inject constructor( } private fun verifyAndSendTransaction() { - val destination = destinationUM?.addressTextField?.value ?: return + val destination = destinationUM?.addressTextField?.actualAddress ?: return val memo = destinationUM?.memoTextField?.value val fee = feeSelectorUM?.selectedFee ?: return val ownerAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: return diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 4bf63dbc36..52919729c3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -112,7 +112,7 @@ internal class NFTSendModel @Inject constructor( val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") val ownerAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("Invalid owner address") - val enteredDestinationAddress = destinationUM.addressTextField.value + val enteredDestinationAddress = destinationUM.addressTextField.actualAddress val enteredMemo = destinationUM.memoTextField?.value val nftAsset = NFTSdkAssetConverter.convertBack(params.nftAsset).second diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt index a67c26d520..b5a3177c5f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt @@ -31,6 +31,8 @@ internal class SendDestinationValidationResultTransformer( }.leftOrNull() val shouldDisableMemo = shouldDisableMemo() + val blockchainAddress = + (addressValidationResult.getOrNull() as? AddressValidation.Success.ValidNamedAddress)?.blockchainAddress return state.copy( isValidating = false, @@ -38,6 +40,7 @@ internal class SendDestinationValidationResultTransformer( addressTextField = state.addressTextField.copy( error = addressErrorText?.let(::resourceReference), isError = state.addressTextField.value.isNotEmpty() && !isValidAddress, + blockchainAddress = blockchainAddress, ), memoTextField = state.memoTextField?.copy( value = state.memoTextField.value.takeIf { !shouldDisableMemo }.orEmpty(), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt index ccc6266ae2..abf90f46a7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt @@ -21,7 +21,13 @@ internal sealed class DestinationTextFieldUM { val isError: Boolean = false, val error: TextReference? = null, val isValuePasted: Boolean, - ) : DestinationTextFieldUM() + // if value is human-readable address, this field contains the actual blockchain address + val blockchainAddress: String? = null, + ) : DestinationTextFieldUM() { + + val actualAddress: String + get() = blockchainAddress ?: value + } data class RecipientMemo( override val value: String, From ea8695c0317744f228ff137c3a46e5676b82f450 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 11:04:25 +0400 Subject: [PATCH 84/99] Updated on 2026-08-14 --- .../tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt | 3 +++ .../common/test/domain/token/MockCryptoCurrencyFactory.kt | 2 +- .../src/main/java/com/tangem/common/test/utils/TruthExt.kt | 4 ++-- .../src/main/assets/configs/feature_toggles_config.json | 4 ++++ .../kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt | 2 ++ 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index 8922e9b2e7..1612d89f64 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -9,4 +9,7 @@ internal class DefaultTokensFeatureToggles( override val isStakingLoadingRefactoringEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_LOADING_REFACTORING_ENABLED") + + override val isWalletBalanceFetcherEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "WALLET_BALANCE_FETCHER_ENABLED") } \ No newline at end of file diff --git a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt index 48fcf815ed..4580b669b4 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt @@ -45,7 +45,7 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default ) } - fun createCoin(blockchain: Blockchain): CryptoCurrency { + fun createCoin(blockchain: Blockchain): CryptoCurrency.Coin { val derivationPath = createDerivationPath( blockchain = blockchain, extraDerivationPath = null, diff --git a/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt b/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt index d28fe3df7e..1585424ba5 100644 --- a/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt +++ b/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt @@ -5,9 +5,9 @@ import com.google.common.truth.Truth fun assertEither(actual: Either, expected: Either) { actual - .onRight { Truth.assertThat(it).isEqualTo(expected) } + .onRight { Truth.assertThat(actual).isEqualTo(expected) } .onLeft { - val expectedError = expected.leftOrNull() ?: error("Expected must be Either.Left") + val expectedError = expected.leftOrNull() ?: error("Actual is Either.Left: $it") Truth.assertThat(it::class.java).isEqualTo(expectedError::class.java) } diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 0ab98cc318..bbc46e5650 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -74,5 +74,9 @@ { "name": "SEND_REDESIGN_ENABLED", "version": "undefined" + }, + { + "name": "WALLET_BALANCE_FETCHER_ENABLED", + "version": "undefined" } ] diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index f04d8d0e56..50ae6bb491 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -8,4 +8,6 @@ package com.tangem.domain.tokens interface TokensFeatureToggles { val isStakingLoadingRefactoringEnabled: Boolean + + val isWalletBalanceFetcherEnabled: Boolean } \ No newline at end of file From 45cf551d3f6a77e849b5d496c36c0bee32692aea Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 13:30:53 +0400 Subject: [PATCH 85/99] Updated on 2026-08-14 --- .../DefaultMultiNetworkStatusProducerTest.kt | 2 +- data/tokens/build.gradle.kts | 13 + ...aultMultiWalletCryptoCurrenciesProducer.kt | 64 ++++ ...ltiWalletCryptoCurrenciesProducerModule.kt | 20 + ...ltiWalletCryptoCurrenciesSupplierModule.kt | 25 ++ ...MultiWalletCryptoCurrenciesProducerTest.kt | 351 ++++++++++++++++++ .../MultiWalletCryptoCurrenciesProducer.kt | 17 + .../MultiWalletCryptoCurrenciesSupplier.kt | 20 + 8 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesProducerModule.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesSupplierModule.kt create mode 100644 data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducerTest.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesProducer.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesSupplier.kt diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt index feca7854ed..7e30b4e8a7 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt @@ -50,7 +50,7 @@ internal class DefaultMultiNetworkStatusProducerTest { @Test fun `flow is mapped for user wallet id from params`() = runTest { - // Assert + // Arrange val statuses = setOf( MockNetworkStatusFactory.createVerified(ethNetwork), MockNetworkStatusFactory.createVerified(cardanoNetwork), diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 1a51e2cbc9..ac23666c4d 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -12,6 +12,10 @@ android { namespace = "com.tangem.data.tokens" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { // region Project - Data @@ -62,4 +66,13 @@ dependencies { ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion + + // region Tests + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(projects.common.test) + // endregion } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt new file mode 100644 index 0000000000..7f655d07f8 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt @@ -0,0 +1,64 @@ +package com.tangem.data.tokens + +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer +import com.tangem.domain.wallets.models.requireColdWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.* + +/** + * Default implementation of [MultiWalletCryptoCurrenciesProducer] + * + * @property params params + * @property userWalletsStore UserWallet's store + * @property userTokensResponseStore store of `UserTokensResponse` + * @property responseCryptoCurrenciesFactory factory for creating [CryptoCurrency] from `UserTokensResponse` + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constructor( + @Assisted val params: MultiWalletCryptoCurrenciesProducer.Params, + private val userWalletsStore: UserWalletsStore, + private val userTokensResponseStore: UserTokensResponseStore, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, + private val dispatchers: CoroutineDispatcherProvider, +) : MultiWalletCryptoCurrenciesProducer { + + override val fallback: Set + get() = emptySet() + + override fun produce(): Flow> { + val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId).requireColdWallet() // TODO [REDACTED_TASK_KEY] + + if (!userWallet.isMultiCurrency) { + error("${this::class.simpleName} supports only multi-currency wallet") + } + + return userTokensResponseStore.get(userWalletId = params.userWalletId) + .distinctUntilChanged() + .map { response -> + if (response == null) return@map emptySet() + + responseCryptoCurrenciesFactory.createCurrencies( + response = response, + scanResponse = userWallet.scanResponse, + ).toSet() + } + .onEmpty { emit(fallback) } + .flowOn(dispatchers.default) + } + + @AssistedFactory + interface Factory : MultiWalletCryptoCurrenciesProducer.Factory { + override fun create( + params: MultiWalletCryptoCurrenciesProducer.Params, + ): DefaultMultiWalletCryptoCurrenciesProducer + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesProducerModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesProducerModule.kt new file mode 100644 index 0000000000..03fa952be2 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesProducerModule.kt @@ -0,0 +1,20 @@ +package com.tangem.data.tokens.di + +import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesProducer +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface MultiWalletCryptoCurrenciesProducerModule { + + @Singleton + @Binds + fun bindMultiWalletCryptoCurrenciesProducerFactory( + impl: DefaultMultiWalletCryptoCurrenciesProducer.Factory, + ): MultiWalletCryptoCurrenciesProducer.Factory +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesSupplierModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesSupplierModule.kt new file mode 100644 index 0000000000..b2bac92e91 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesSupplierModule.kt @@ -0,0 +1,25 @@ +package com.tangem.data.tokens.di + +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class MultiWalletCryptoCurrenciesSupplierModule { + + @Singleton + @Provides + fun provideMultiWalletCryptoCurrenciesSupplier( + factory: MultiWalletCryptoCurrenciesProducer.Factory, + ): MultiWalletCryptoCurrenciesSupplier { + return object : MultiWalletCryptoCurrenciesSupplier( + factory = factory, + keyCreator = { "multi_crypto_currency_${it.userWalletId}" }, + ) {} + } +} \ No newline at end of file diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducerTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducerTest.kt new file mode 100644 index 0000000000..a453c8e2b7 --- /dev/null +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducerTest.kt @@ -0,0 +1,351 @@ +package com.tangem.data.tokens + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.domain.card.MockScanResponseFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.configs.GenericCardConfig +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.isMultiCurrency +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultMultiWalletCryptoCurrenciesProducerTest { + + private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + + private val params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWallet.walletId) + private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory = mockk() + + private val producer = DefaultMultiWalletCryptoCurrenciesProducer( + params = params, + userWalletsStore = userWalletsStore, + userTokensResponseStore = userTokensResponseStore, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun resetMocks() { + clearMocks(userWalletsStore, userTokensResponseStore, responseCryptoCurrenciesFactory) + } + + @Test + fun `flow is mapped for user wallet id from params`() = runTest { + // Arrange + val userTokensResponseFlow = flowOf(null) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow + + // Act + val actual = producer.produce().let(::getEmittedValues) + + // Assert + val expected = emptySet() + + Truth.assertThat(actual.size).isEqualTo(1) + Truth.assertThat(actual.first()).isEqualTo(expected) + + verifyOrder { + userWalletsStore.getSyncStrict(params.userWalletId) + userTokensResponseStore.get(params.userWalletId) + } + + verify(inverse = true) { + responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any()) + } + } + + @Test + fun `flow will updated if UserTokensResponse is updated`() = runTest { + // Arrange + val userTokensResponseFlow = MutableSharedFlow(replay = 2) + + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.TOKEN, + sort = UserTokensResponse.SortType.MARKETCAP, + tokens = emptyList(), + ) + val cryptoCurrencies = emptySet() + + val updatedUserTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.TOKEN, + sort = UserTokensResponse.SortType.MARKETCAP, + tokens = listOf( + UserTokensResponse.Token( + id = null, + networkId = "bitcoin", + derivationPath = null, + name = "Bitcoin", + symbol = "BTC", + decimals = 8, + contractAddress = null, + addresses = listOf(), + ), + ), + ) + val updatedCryptoCurrencies = setOf( + cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow + + every { + responseCryptoCurrenciesFactory.createCurrencies( + response = userTokensResponse, + scanResponse = userWallet.scanResponse, + ) + } returns cryptoCurrencies.toList() + + every { + responseCryptoCurrenciesFactory.createCurrencies( + response = updatedUserTokensResponse, + scanResponse = userWallet.scanResponse, + ) + } returns updatedCryptoCurrencies.toList() + + val producerFlow = producer.produce() + + // Act 1 (first emit) + userTokensResponseFlow.emit(userTokensResponse) + + val actual1 = getEmittedValues(flow = producerFlow) + + // Assert + val expected1 = cryptoCurrencies + + Truth.assertThat(actual1.size).isEqualTo(1) + Truth.assertThat(actual1.first()).isEqualTo(expected1) + + verifyOrder { + userWalletsStore.getSyncStrict(params.userWalletId) + userTokensResponseStore.get(params.userWalletId) + responseCryptoCurrenciesFactory.createCurrencies( + response = userTokensResponse, + scanResponse = userWallet.scanResponse, + ) + } + + // Act 2 (second emit) + userTokensResponseFlow.emit(updatedUserTokensResponse) + + val actual2 = getEmittedValues(flow = producerFlow) + + // Assert + val expected2 = listOf(cryptoCurrencies, updatedCryptoCurrencies) + + Truth.assertThat(actual2.size).isEqualTo(2) + Truth.assertThat(actual2).isEqualTo(expected2) + + verifyOrder { + responseCryptoCurrenciesFactory.createCurrencies( + response = updatedUserTokensResponse, + scanResponse = userWallet.scanResponse, + ) + } + } + + @Test + fun `flow is filtered the same status`() = runTest { + // Arrange + val userTokensResponseFlow = MutableSharedFlow(replay = 2) + + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.TOKEN, + sort = UserTokensResponse.SortType.MARKETCAP, + tokens = emptyList(), + ) + + val cryptoCurrencies = emptySet() + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow + + every { + responseCryptoCurrenciesFactory.createCurrencies( + response = userTokensResponse, + scanResponse = userWallet.scanResponse, + ) + } returns cryptoCurrencies.toList() + + val producerFlow = producer.produce() + + // Act 1 (first emit) + userTokensResponseFlow.emit(userTokensResponse) + + val actual1 = getEmittedValues(flow = producerFlow) + + // Assert + val expected1 = cryptoCurrencies + + Truth.assertThat(actual1.size).isEqualTo(1) + Truth.assertThat(actual1.first()).isEqualTo(expected1) + + verifyOrder { + userWalletsStore.getSyncStrict(params.userWalletId) + userTokensResponseStore.get(params.userWalletId) + responseCryptoCurrenciesFactory.createCurrencies( + response = userTokensResponse, + scanResponse = userWallet.scanResponse, + ) + } + + // Act 2 (second emit) + userTokensResponseFlow.emit(userTokensResponse) + + val actual2 = getEmittedValues(flow = producerFlow) + + // Assert + val expected2 = expected1 + Truth.assertThat(actual2.size).isEqualTo(1) + Truth.assertThat(actual2.first()).isEqualTo(expected2) + } + + @Test + fun `flow throws exception`() = runTest { + // Arrange + val exception = IllegalStateException() + + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.TOKEN, + sort = UserTokensResponse.SortType.MARKETCAP, + tokens = emptyList(), + ) + + val cryptoCurrencies = emptySet() + + val innerFlow = MutableStateFlow(value = false) + val userTokensResponseFlow = flow { + if (innerFlow.value) { + emit(userTokensResponse) + } else { + throw exception + } + } + .buffer(capacity = 5) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow + + every { + responseCryptoCurrenciesFactory.createCurrencies( + response = userTokensResponse, + scanResponse = userWallet.scanResponse, + ) + } returns cryptoCurrencies.toList() + + val producerFlow = producer.produceWithFallback() + + // Act 1 (fallback) + val actual1 = getEmittedValues(flow = producerFlow) + + // Assert + val expected1 = producer.fallback + Truth.assertThat(actual1.size).isEqualTo(1) + Truth.assertThat(actual1.first()).isEqualTo(expected1) + + verifyOrder { + userWalletsStore.getSyncStrict(params.userWalletId) + userTokensResponseStore.get(params.userWalletId) + } + + // Act 2 (emit) + innerFlow.emit(value = true) + val actual2 = getEmittedValues(flow = producerFlow) + + // Assert + val expected2 = cryptoCurrencies + Truth.assertThat(actual2.size).isEqualTo(1) + Truth.assertThat(actual2.first()).isEqualTo(expected2) + + verifyOrder { + responseCryptoCurrenciesFactory.createCurrencies( + response = userTokensResponse, + scanResponse = userWallet.scanResponse, + ) + } + } + + @Test + fun `flow is empty if store returns empty flow`() = runTest { + // Arrange + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + every { userTokensResponseStore.get(params.userWalletId) } returns emptyFlow() + + // Act + val actual = producer.produce().let(::getEmittedValues) + + // Assert + val expected = producer.fallback + Truth.assertThat(actual.size).isEqualTo(1) + Truth.assertThat(actual.first()).isEqualTo(expected) + + verifyOrder { + userWalletsStore.getSyncStrict(params.userWalletId) + userTokensResponseStore.get(params.userWalletId) + } + + verify(inverse = true) { + responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any()) + } + } + + @Test + fun `produce throws exception if UserWallet isn't multi-currency wallet`() = runTest { + // Arrange + val mockUserWallet = mockk { + every { isMultiCurrency } returns false + } + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + + // Act + val actual = runCatching { producer.produce() }.exceptionOrNull() + + // Assert + val expected = IllegalStateException( + "${DefaultMultiWalletCryptoCurrenciesProducer::class.simpleName} supports only multi-currency wallet", + ) + + Truth.assertThat(actual).isInstanceOf(expected::class.java) + Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message) + + verifyOrder { userWalletsStore.getSyncStrict(params.userWalletId) } + + verify(inverse = true) { + userTokensResponseStore.get(any()) + responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any()) + } + } + + private companion object { + + val scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(2), + derivedKeys = emptyMap(), + ) + + val userWallet = MockUserWalletFactory.create(scanResponse = scanResponse) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesProducer.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesProducer.kt new file mode 100644 index 0000000000..b95ff78178 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesProducer.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Producer of all crypto currencies for a multi-currency wallet with specified [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface MultiWalletCryptoCurrenciesProducer : FlowProducer> { + + data class Params(val userWalletId: UserWalletId) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesSupplier.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesSupplier.kt new file mode 100644 index 0000000000..cd6ed8fe2c --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesSupplier.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer as Producer + +/** + * Producer of all crypto currencies for a multi-currency wallet with specified [UserWalletId] + * + * @property factory factory for creating [Producer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class MultiWalletCryptoCurrenciesSupplier( + override val factory: FlowProducer.Factory, + override val keyCreator: (Producer.Params) -> String, +) : FlowCachingSupplier>() \ No newline at end of file From 9ebfdb5b3133974250475c30c689a4dae51db767 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 14:35:52 +0500 Subject: [PATCH 86/99] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../com/tangem/tap/features/home/HomeModel.kt | 6 +- .../tangem/tap/routing/utils/ChildFactory.kt | 27 +++++++++ .../com/tangem/common/routing/AppRoute.kt | 9 +++ core/res/src/main/res/values/strings.xml | 38 ++++++++++++ .../CreateWalletSelectionModel.kt | 7 ++- .../CreateWalletSelectionModule.kt} | 9 ++- .../entity/CreateWalletSelectionUM.kt | 1 + .../ui/CreateWalletSelectionContent.kt | 41 ++++++------- .../port/AddExistingWalletImportComponent.kt | 36 +++++++++++ .../im/port/AddExistingWalletImportModel.kt | 26 ++++++++ .../port/entity/AddExistingWalletImportUM.kt | 5 ++ .../port/ui/AddExistingWalletImportContent.kt | 47 +++++++++++++++ .../root/AddExistingWalletModel.kt | 20 ++++++- .../root/DefaultAddExistingWalletComponent.kt | 6 +- .../root/di/AddExistingWalletModule.kt | 45 ++++++++++++++ .../routing/AddExistingWalletChildFactory.kt | 7 +++ .../root/routing/AddExistingWalletRoute.kt | 3 + .../root/ui/AddExistingWalletContent.kt | 17 ++---- .../start/ui/AddExistingWalletStartContent.kt | 59 ++++++++++--------- .../CreateMobileWalletModel.kt | 4 +- .../di/CreateMobileWalletModule.kt | 33 +++++++++++ .../ui/CreateMobileWalletContent.kt | 20 +++---- 23 files changed, 379 insertions(+), 89 deletions(-) rename features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/{CreateWalletSelectionFeatureModule.kt => di/CreateWalletSelectionModule.kt} (64%) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/di/CreateMobileWalletModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 85b539e159..c17cd6b366 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -226,6 +226,8 @@ dependencies { implementation(projects.features.kyc.impl) implementation(projects.features.welcome.api) implementation(projects.features.welcome.impl) + implementation(projects.features.createWalletSelection.api) + implementation(projects.features.createWalletSelection.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt index 8936e89b41..3c580e42fd 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt @@ -11,6 +11,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardSdkConfigRepository @@ -54,6 +55,7 @@ internal class HomeModel @Inject constructor( private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val router: Router, getUserCountryUseCase: GetUserCountryUseCase, ) : Model() { @@ -72,11 +74,11 @@ internal class HomeModel @Inject constructor( } fun onCreateNewWalletScreen() { - // TODO implement navigation to create new wallet + router.push(AppRoute.CreateWalletSelection) } fun onAddExistingWalletScreen() { - // TODO implement navigation to add existing wallet + router.push(AppRoute.AddExistingWallet) } fun onScanClick() { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 4c5089eeb5..825a388842 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -8,8 +8,11 @@ import com.tangem.feature.referral.api.ReferralComponent import com.tangem.feature.stories.api.StoriesComponent import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.features.createwalletselection.CreateWalletSelectionComponent import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.hotwallet.AddExistingWalletComponent +import com.tangem.features.hotwallet.CreateMobileWalletComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -80,6 +83,9 @@ internal class ChildFactory @Inject constructor( private val nftSendComponentFactory: NFTSendComponent.Factory, private val usedeskComponentFactory: UsedeskComponent.Factory, private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, + private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, + private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, + private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, private val testerRouter: TesterRouter, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, ) { @@ -420,6 +426,27 @@ internal class ChildFactory @Inject constructor( componentFactory = chooseManagedTokensComponentFactory, ) } + is AppRoute.CreateWalletSelection -> { + createComponentChild( + context = context, + params = Unit, + componentFactory = createWalletSelectionComponentFactory, + ) + } + is AppRoute.CreateMobileWallet -> { + createComponentChild( + context = context, + params = Unit, + componentFactory = createMobileWalletComponentFactory, + ) + } + is AppRoute.AddExistingWallet -> { + createComponentChild( + context = context, + params = Unit, + componentFactory = addExistingWalletComponentFactory, + ) + } } } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index bfa14feda5..410b01ee6d 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -286,4 +286,13 @@ sealed class AppRoute(val path: String) : Route { val nftAsset: NFTAsset, val nftCollectionName: String, ) : AppRoute(path = "/send/nft/${userWalletId.stringValue}/$nftCollectionName/${nftAsset.id}") + + @Serializable + object CreateWalletSelection : AppRoute(path = "/create_wallet_selection") + + @Serializable + object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") + + @Serializable + object AddExistingWallet : AppRoute(path = "/add_existing_wallet") } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index fc05ef9a38..28efd231b8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -119,6 +119,7 @@ Claim Claim rewards Close + Coming Soon Confirm Contact Tangem Support Contact Visa Support @@ -154,6 +155,7 @@ Slow Speed and fee Finish + Free Synchronize addresses Go to provider Go to token @@ -387,6 +389,19 @@ Scan Tangem to %s On %s network + Recover an existing wallet stored in your iCloud backup + iCloud backup + Go to backup + To secure your wallet with a Access Code, complete the backup first. + Finish Backup First + No backup + Physical cards that securely store your crypto offline. + Recovery phrase + Get notified of incoming transactions + Keys are stored in the app + Stay up to date with the latest features and news + Seed phrase backup + Create Mobile Wallet This information was generated with AI.\nTap here, if you find any errors. To change the access code tap the card or ring as shown above and do not remove until the end of the operation To change the passcode tap the card as shown above and do not remove until the end of the operation @@ -1246,6 +1261,29 @@ Connect to dApps WalletConnect Connecting may take a few seconds + From %s + Buy Tangem Wallet — physical cards that securely store your crypto offline. + Hardware Wallet + A secure wallet is created on your phone in seconds. + Mobile Wallet + What to choose? + Do you already have Tangem Wallet? + Scan Now + Choose how you want to create your wallet + Want to purchase a Tangem Wallet? + Buy Now + Recover an existing wallet stored in your Google Drive backup + Import from Google Drive + Add existing wallet + Physical cards that securely store your crypto offline. + Scan a Tangem Wallet + Your seed phrase to recover your wallet + Import wallet + Import a recovery phrase + You successfully backed up your wallet. + Import wallet + Import Completed + Import wallet %s Market Price last 24h %s network diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt index 0299618954..242046b568 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -1,7 +1,9 @@ package com.tangem.features.createwalletselection +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -11,12 +13,13 @@ import javax.inject.Inject @ModelScoped internal class CreateWalletSelectionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, ) : Model() { internal val uiState: StateFlow field = MutableStateFlow( CreateWalletSelectionUM( - onBackClick = { /* [REDACTED_TODO_COMMENT] */ }, + onBackClick = { router.pop() }, onMobileWalletClick = ::onMobileWalletClick, onHardwareWalletClick = ::onHardwareWalletClick, onScanClick = ::onScanClick, @@ -24,7 +27,7 @@ internal class CreateWalletSelectionModel @Inject constructor( ) private fun onMobileWalletClick() { - // TODO navigate to mobile wallet creation + router.push(AppRoute.CreateMobileWallet) } private fun onHardwareWalletClick() { diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionFeatureModule.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/di/CreateWalletSelectionModule.kt similarity index 64% rename from features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionFeatureModule.kt rename to features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/di/CreateWalletSelectionModule.kt index a810f1ba9f..239fcfe4ad 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionFeatureModule.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/di/CreateWalletSelectionModule.kt @@ -1,6 +1,9 @@ -package com.tangem.features.createwalletselection +package com.tangem.features.createwalletselection.di import com.tangem.core.decompose.model.Model +import com.tangem.features.createwalletselection.CreateWalletSelectionComponent +import com.tangem.features.createwalletselection.CreateWalletSelectionModel +import com.tangem.features.createwalletselection.DefaultCreateWalletSelectionComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -11,11 +14,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object CreateWalletSelectionFeatureModule +internal object CreateWalletSelectionModule @Module @InstallIn(SingletonComponent::class) -internal interface CreateWalletSelectionFeatureModuleBinds { +internal interface CreateWalletSelectionModuleBinds { @Binds @Singleton diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt index 70c95a5b38..38affd1226 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt @@ -2,6 +2,7 @@ package com.tangem.features.createwalletselection.entity internal data class CreateWalletSelectionUM( val isScanInProgress: Boolean = false, + val hardwareWalletPrice: String = "$54.90", val onBackClick: () -> Unit, val onMobileWalletClick: () -> Unit, val onHardwareWalletClick: () -> Unit, diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index e5ffea8e68..6e412c985b 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.* 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.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -17,6 +18,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM @@ -27,13 +29,13 @@ import com.tangem.features.createwalletselection.impl.R @Composable internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifier: Modifier = Modifier) { Column( - modifier = Modifier + modifier = modifier .background(TangemTheme.colors.background.primary) .fillMaxSize() .systemBarsPadding(), ) { TopAppBar( - modifier = modifier + modifier = Modifier .statusBarsPadding(), colors = TopAppBarDefaults.topAppBarColors( containerColor = TangemTheme.colors.background.primary, @@ -52,8 +54,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi Text( modifier = Modifier .padding(16.dp), - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "What to choose?", + text = stringResourceSafe(R.string.wallet_create_nav_info_title), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, maxLines = 1, @@ -74,8 +75,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "Choose how you want to create your wallet", + text = stringResourceSafe(R.string.wallet_create_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -83,10 +83,8 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi WalletBlock( modifier = Modifier .padding(top = 24.dp), - // TODO [REDACTED_TASK_KEY] update and extract this string - title = "Mobile Wallet", - // TODO [REDACTED_TASK_KEY] update and extract this string - description = "A secure wallet is created on your phone in seconds.", + title = stringResourceSafe(R.string.wallet_create_mobile_title), + description = stringResourceSafe(R.string.wallet_create_mobile_description), badge = { Box( modifier = Modifier @@ -97,8 +95,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi .padding(horizontal = 8.dp, vertical = 4.dp), ) { Text( - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "Free", + text = stringResourceSafe(R.string.common_free), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.secondary, ) @@ -107,10 +104,8 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi onClick = state.onMobileWalletClick, ) WalletBlock( - // TODO [REDACTED_TASK_KEY] update and extract this string - title = "Hardware Wallet", - // TODO [REDACTED_TASK_KEY] update and extract this string - description = "Buy Tangem Wallet — physical cards that securely store your crypto offline.", + title = stringResourceSafe(R.string.wallet_create_hardware_title), + description = stringResourceSafe(R.string.wallet_create_hardware_description), badge = { Box( modifier = Modifier @@ -121,8 +116,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi .padding(horizontal = 8.dp, vertical = 4.dp), ) { Text( - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "From $54.90", + text = stringResourceSafe(R.string.wallet_create_hardware_badge, state.hardwareWalletPrice), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.accent, ) @@ -150,12 +144,13 @@ private fun WalletBlock( modifier = modifier .fillMaxWidth() .padding(top = 8.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) .background( color = TangemTheme.colors.background.secondary, shape = TangemTheme.shapes.roundedCornersXMedium, ) - .padding(16.dp) - .clickable(onClick = onClick), + .clickable(onClick = onClick) + .padding(16.dp), ) { Row { Text( @@ -202,16 +197,14 @@ private fun AlreadyHaveTangemWalletBlock( modifier = Modifier .weight(1f) .padding(end = 16.dp), - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "Do you already have Tangem Wallet?", + text = stringResourceSafe(R.string.wallet_create_scan_question), style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, ) TangemButton( modifier = Modifier .wrapContentWidth(), - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "Scan Now", + text = stringResourceSafe(R.string.wallet_create_scan_title), onClick = onScanClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), size = TangemButtonSize.RoundedAction, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt new file mode 100644 index 0000000000..a29db42a78 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt @@ -0,0 +1,36 @@ +package com.tangem.features.hotwallet.addexistingwallet.im.port + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.AddExistingWalletImportContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class AddExistingWalletImportComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { + private val model: AddExistingWalletImportModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + AddExistingWalletImportContent( + state = state, + modifier = modifier, + ) + } + + interface ModelCallbacks { + fun onBackClick() + } + + data class Params( + val callbacks: ModelCallbacks, + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportModel.kt new file mode 100644 index 0000000000..7f39c0e85a --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportModel.kt @@ -0,0 +1,26 @@ +package com.tangem.features.hotwallet.addexistingwallet.im.port + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class AddExistingWalletImportModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params: AddExistingWalletImportComponent.Params = paramsContainer.require() + + internal val uiState: StateFlow + field = MutableStateFlow( + AddExistingWalletImportUM( + onBackClick = params.callbacks::onBackClick, + ), + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt new file mode 100644 index 0000000000..5daf5de360 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt @@ -0,0 +1,5 @@ +package com.tangem.features.hotwallet.addexistingwallet.im.port.entity + +internal data class AddExistingWalletImportUM( + val onBackClick: () -> Unit, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt new file mode 100644 index 0000000000..4e3fc2638a --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt @@ -0,0 +1,47 @@ +package com.tangem.features.hotwallet.addexistingwallet.im.port.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM +import com.tangem.core.ui.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .systemBarsPadding(), + ) { + TangemTopAppBar( + modifier = Modifier + .statusBarsPadding(), + startButton = TopAppBarButtonUM.Back(state.onBackClick), + title = stringResourceSafe(R.string.wallet_import_title), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewAddExistingWalletImportContent() { + TangemThemePreview { + AddExistingWalletImportContent( + state = AddExistingWalletImportUM( + onBackClick = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt index abbb917a03..dd9a7da008 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt @@ -1,25 +1,41 @@ package com.tangem.features.hotwallet.addexistingwallet.root +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent +import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent +import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute import com.tangem.utils.coroutines.CoroutineDispatcherProvider import javax.inject.Inject @ModelScoped internal class AddExistingWalletModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, ) : Model() { val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks() + val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks() + + val stackNavigation = StackNavigation() inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks { override fun onBackClick() { - // TODO implement in [REDACTED_TASK_KEY] + router.pop() } override fun onImportPhraseClick() { - // TODO implement in [REDACTED_TASK_KEY] + stackNavigation.push(AddExistingWalletRoute.Import) + } + } + + inner class AddExistingWalletImportModelCallbacks : AddExistingWalletImportComponent.ModelCallbacks { + override fun onBackClick() { + stackNavigation.pop() } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt index 4ef6395750..104d2582c2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.tangem.core.decompose.context.AppComponentContext @@ -27,12 +26,11 @@ internal class DefaultAddExistingWalletComponent @AssistedInject constructor( private val model: AddExistingWalletModel = getOrCreateModel(params) - private val stackNavigation = StackNavigation() private val startRoute = AddExistingWalletRoute.Start private val innerStack = childStack( key = "addExistingWalletInnerStack", - source = stackNavigation, + source = model.stackNavigation, serializer = null, initialConfiguration = startRoute, handleBackButton = true, @@ -61,7 +59,7 @@ internal class DefaultAddExistingWalletComponent @AssistedInject constructor( if (isEmptyStack) { router.pop() } else { - stackNavigation.pop() + model.stackNavigation.pop() } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt new file mode 100644 index 0000000000..d2019f9f33 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt @@ -0,0 +1,45 @@ +package com.tangem.features.hotwallet.addexistingwallet.root.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.AddExistingWalletComponent +import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel +import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent +import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartModel +import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AddExistingWalletModule + +@Module +@InstallIn(SingletonComponent::class) +internal interface AddExistingWalletModuleBinds { + + @Binds + @Singleton + fun bindAddExistingWalletComponentFactory( + impl: DefaultAddExistingWalletComponent.Factory, + ): AddExistingWalletComponent.Factory + + @Binds + @IntoMap + @ClassKey(AddExistingWalletModel::class) + fun bindAddExistingWalletModel(model: AddExistingWalletModel): Model + + @Binds + @IntoMap + @ClassKey(AddExistingWalletStartModel::class) + fun bindAddExistingWalletStartModel(model: AddExistingWalletStartModel): Model + + @Binds + @IntoMap + @ClassKey(AddExistingWalletImportModel::class) + fun bindAddExistingWalletImportModel(model: AddExistingWalletImportModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt index b9079208c6..4c1e57c968 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt @@ -4,6 +4,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent +import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import javax.inject.Inject internal class AddExistingWalletChildFactory @Inject constructor() { @@ -20,6 +21,12 @@ internal class AddExistingWalletChildFactory @Inject constructor() { callbacks = model.addExistingWalletStartModelCallbacks, ), ) + is AddExistingWalletRoute.Import -> AddExistingWalletImportComponent( + context = childContext, + params = AddExistingWalletImportComponent.Params( + callbacks = model.addExistingWalletImportModelCallbacks, + ), + ) } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt index 56ca941809..1566d1bc74 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt @@ -7,4 +7,7 @@ internal sealed class AddExistingWalletRoute : Route { @Serializable object Start : AddExistingWalletRoute() + + @Serializable + object Import : AddExistingWalletRoute() } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt index 9d3367cbc7..bc43867900 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt @@ -1,12 +1,10 @@ package com.tangem.features.hotwallet.addexistingwallet.root.ui import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.slide @@ -18,20 +16,15 @@ import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingW @Composable internal fun AddExistingWalletContent(stackState: ChildStack) { - Column( - modifier = Modifier.Companion + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier .background(color = TangemTheme.colors.background.primary) .fillMaxSize() .imePadding() .systemBarsPadding(), - horizontalAlignment = Alignment.Companion.CenterHorizontally, ) { - Children( - stack = stackState, - animation = stackAnimation(slide()), - modifier = Modifier.weight(1f), - ) { - it.instance.Content(Modifier.weight(1f)) - } + it.instance.Content(Modifier.fillMaxSize()) } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt index a0a2ea6abe..66e907f5d8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt @@ -7,6 +7,8 @@ import androidx.compose.material3.* 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.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -20,19 +22,21 @@ import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.stringResourceSafe @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modifier: Modifier = Modifier) { Column( - modifier = Modifier + modifier = modifier .background(TangemTheme.colors.background.primary) .fillMaxSize() .systemBarsPadding(), ) { TangemTopAppBar( - modifier = modifier + modifier = Modifier .statusBarsPadding(), startButton = TopAppBarButtonUM.Back(state.onBackClick), title = "Add existing wallet", @@ -50,8 +54,7 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "Import wallet", + text = stringResourceSafe(R.string.wallet_import_seed_navtitle), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -59,31 +62,35 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi OptionBlock( modifier = Modifier .padding(top = 24.dp), - // TODO [REDACTED_TASK_KEY] update and extract this string - title = "Import a recovery phrase", - // TODO [REDACTED_TASK_KEY] update and extract this string - description = "Your 12-word seed phrase to recover your wallet", + title = stringResourceSafe(R.string.wallet_import_seed_title), + description = stringResourceSafe(R.string.wallet_import_seed_description), badge = null, onClick = state.onImportPhraseClick, enabled = true, ) OptionBlock( - // TODO [REDACTED_TASK_KEY] update and extract this string - title = "Scan a Tangem Wallet", - // TODO [REDACTED_TASK_KEY] update and extract this string - description = "Physical cards that securely store your crypto offline.", - badge = null, + title = stringResourceSafe(R.string.wallet_import_scan_title), + description = stringResourceSafe(R.string.wallet_import_scan_description), + badge = { + Icon( + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp), + painter = painterResource(R.drawable.ic_tangem_24), + contentDescription = null, + tint = TangemTheme.colors.icon.secondary, + ) + }, onClick = state.onScanCardClick, enabled = true, ) OptionBlock( - // TODO [REDACTED_TASK_KEY] update and extract this string - title = "Import from Google Drive", - // TODO [REDACTED_TASK_KEY] update and extract this string - description = "Recover an existing wallet stored in your Google Drive backup", + title = stringResourceSafe(R.string.wallet_import_google_drive_title), + description = stringResourceSafe(R.string.wallet_import_google_drive_description), badge = { Box( modifier = Modifier + .padding(horizontal = 4.dp) .background( color = TangemTheme.colors.field.focused, shape = TangemTheme.shapes.roundedCorners8, @@ -91,8 +98,7 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi .padding(horizontal = 8.dp, vertical = 4.dp), ) { Text( - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "Coming Soon", + text = stringResourceSafe(R.string.common_coming_soon), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, ) @@ -121,20 +127,21 @@ private fun OptionBlock( modifier = modifier .fillMaxWidth() .padding(top = 8.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) .background( color = TangemTheme.colors.background.secondary, shape = TangemTheme.shapes.roundedCornersXMedium, ) - .padding(16.dp) .conditional(onClick != null) { onClick?.let { clickableSingle(onClick = it) } ?: Modifier - }, + } + .padding(16.dp), ) { Row { Text( modifier = Modifier - .weight(1f) - .padding(end = 8.dp), + .weight(1f, fill = false) + .padding(end = 4.dp), text = title, style = TangemTheme.typography.subtitle1, color = if (enabled) { @@ -179,16 +186,14 @@ private fun BuyTangemWalletBlock(onScanClick: () -> Unit, modifier: Modifier = M modifier = Modifier .weight(1f) .padding(end = 16.dp), - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "Want to purchase a Tangem Wallet?", + text = stringResourceSafe(R.string.wallet_import_buy_question), style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, ) PrimaryButton( modifier = Modifier .wrapContentWidth(), - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "Buy Now", + text = stringResourceSafe(R.string.wallet_import_buy_title), onClick = onScanClick, size = TangemButtonSize.RoundedAction, colors = TangemButtonsDefaults.secondaryButtonColors, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index 629e6c0a74..6491acb916 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.hotwallet.createmobilewallet import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -11,12 +12,13 @@ import javax.inject.Inject @ModelScoped internal class CreateMobileWalletModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, ) : Model() { internal val uiState: StateFlow field = MutableStateFlow( CreateMobileWalletUM( - onBackClick = { /* [REDACTED_TODO_COMMENT] */ }, + onBackClick = { router.pop() }, onCreateClick = ::onCreateClick, ), ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/di/CreateMobileWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/di/CreateMobileWalletModule.kt new file mode 100644 index 0000000000..d34f94f439 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/di/CreateMobileWalletModule.kt @@ -0,0 +1,33 @@ +package com.tangem.features.hotwallet.createmobilewallet.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.CreateMobileWalletComponent +import com.tangem.features.hotwallet.createmobilewallet.CreateMobileWalletModel +import com.tangem.features.hotwallet.createmobilewallet.DefaultCreateMobileWalletComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CreateMobileWalletModule + +@Module +@InstallIn(SingletonComponent::class) +internal interface CreateMobileWalletModuleBinds { + + @Binds + @Singleton + fun bindCreateMobileWalletComponentFactory( + impl: DefaultCreateMobileWalletComponent.Factory, + ): CreateMobileWalletComponent.Factory + + @Binds + @IntoMap + @ClassKey(CreateMobileWalletModel::class) + fun bindCreateMobileWalletModel(model: CreateMobileWalletModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt index 6f3a77c019..5de3f84a92 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt @@ -15,6 +15,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM @@ -24,13 +25,13 @@ import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalle @Composable internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Modifier = Modifier) { Column( - modifier = Modifier + modifier = modifier .background(TangemTheme.colors.background.primary) .fillMaxSize() .systemBarsPadding(), ) { TangemTopAppBar( - modifier = modifier + modifier = Modifier .statusBarsPadding(), startButton = TopAppBarButtonUM.Back(state.onBackClick), title = TextReference.EMPTY, @@ -59,8 +60,7 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo top = 20.dp, end = 16.dp, ), - // TODO [REDACTED_TASK_KEY] update and extract this string - text = "Create Mobile Wallet", + text = stringResourceSafe(R.string.hw_create_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -68,19 +68,15 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo FeatureBlock( modifier = Modifier .padding(top = 32.dp), - // TODO [REDACTED_TASK_KEY] update and extract this string - title = "Keys are stored in the app", - // TODO [REDACTED_TASK_KEY] update and extract this string - description = "Get notified of incoming transactions", + title = stringResourceSafe(R.string.hw_create_keys_title), + description = stringResourceSafe(R.string.hw_create_keys_description), iconRes = R.drawable.ic_lock_24, ) FeatureBlock( modifier = Modifier .padding(top = 24.dp), - // TODO [REDACTED_TASK_KEY] update and extract this string - title = "Seed phrase backup", - // TODO [REDACTED_TASK_KEY] update and extract this string - description = "Stay up to date with the latest features and news", + title = stringResourceSafe(R.string.hw_create_seed_title), + description = stringResourceSafe(R.string.hw_create_seed_description), iconRes = R.drawable.ic_settings_24, ) } From a6f51bdad56fc5df3c2bc5caef2aa626e826186d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Jun 2025 17:36:14 +0500 Subject: [PATCH 87/99] Updated on 2026-08-14 --- .../v2/impl/amount/entity/SwapAmountUM.kt | 8 +- .../v2/impl/amount/model/SwapAmountModel.kt | 10 -- .../converter/SwapAmountErrorConverter.kt | 28 ++++++ .../SwapAmountChangeCurrencyTransformer.kt | 8 +- .../SwapAmountSelectQuoteTransformer.kt | 41 +++++--- .../SwapAmountSetQuotesTransformer.kt | 97 +++---------------- .../SwapAmountValueChangeTransformer.kt | 8 +- .../SwapAmountValueMaxTransformer.kt | 7 +- 8 files changed, 79 insertions(+), 128 deletions(-) create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountErrorConverter.kt diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index 62b7c86556..2ba34fa932 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -34,8 +34,8 @@ internal sealed class SwapAmountUM { override val primaryAmount: SwapAmountFieldUM, override val secondaryAmount: SwapAmountFieldUM, override val selectedAmountType: SwapAmountType, - val primaryCryptoCurrencyStatus: CryptoCurrencyStatus?, - val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?, + val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, + val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, // selected swap route val swapDirection: SwapDirection, @@ -61,7 +61,7 @@ sealed class SwapAmountFieldUM { ) : SwapAmountFieldUM() { override val amountField: AmountState = AmountState.Empty( isPrimaryButtonEnabled = false, - isRedesignEnabled = false, + isRedesignEnabled = true, ) } @@ -70,7 +70,7 @@ sealed class SwapAmountFieldUM { ) : SwapAmountFieldUM() { override val amountField: AmountState = AmountState.Empty( isPrimaryButtonEnabled = false, - isRedesignEnabled = false, + isRedesignEnabled = true, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index a7610c26f1..8313317b76 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -116,8 +116,6 @@ internal class SwapAmountModel @Inject constructor( uiState.transformerUpdate( SwapAmountSelectQuoteTransformer( quoteUM = quoteUM, - primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, ), @@ -136,8 +134,6 @@ internal class SwapAmountModel @Inject constructor( override fun onAmountValueChange(value: String) { uiState.transformerUpdate( SwapAmountValueChangeTransformer( - primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, @@ -155,8 +151,6 @@ internal class SwapAmountModel @Inject constructor( override fun onMaxValueClick() { uiState.transformerUpdate( SwapAmountValueMaxTransformer( - primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, @@ -169,8 +163,6 @@ internal class SwapAmountModel @Inject constructor( override fun onCurrencyChangeClick(isFiat: Boolean) { uiState.transformerUpdate( SwapAmountChangeCurrencyTransformer( - primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, isFiatSelected = isFiat, ), ) @@ -378,8 +370,6 @@ internal class SwapAmountModel @Inject constructor( uiState.transformerUpdate( SwapAmountSetQuotesTransformer( quotes = quotes, - primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, ), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountErrorConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountErrorConverter.kt new file mode 100644 index 0000000000..36b944a04d --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountErrorConverter.kt @@ -0,0 +1,28 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.swap.v2.impl.R +import com.tangem.utils.converter.Converter + +internal class SwapAmountErrorConverter( + private val cryptoCurrency: CryptoCurrency, +) : Converter { + + override fun convert(value: ExpressError): TextReference? = when (value) { + is ExpressError.AmountError.TooSmallError -> resourceReference( + R.string.express_provider_min_amount, + wrappedList(value.amount.format { crypto(cryptoCurrency = cryptoCurrency) }), + ) + is ExpressError.AmountError.TooBigError -> resourceReference( + R.string.express_provider_max_amount, + wrappedList(value.amount.format { crypto(cryptoCurrency = cryptoCurrency) }), + ) + else -> null + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt index 05c0a9d16b..e1a149555d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt @@ -1,22 +1,20 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount import com.tangem.utils.transformer.Transformer internal class SwapAmountChangeCurrencyTransformer( - private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val isFiatSelected: Boolean, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState return prevState.updateAmount( onPrimaryAmount = { copy( amountField = AmountCurrencyTransformer( - cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, value = isFiatSelected, ).transform(prevState.primaryAmount.amountField), ) @@ -24,7 +22,7 @@ internal class SwapAmountChangeCurrencyTransformer( onSecondaryAmount = { copy( amountField = AmountCurrencyTransformer( - cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, value = isFiatSelected, ).transform(prevState.secondaryAmount.amountField), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 0c11067bfc..b11d0238e1 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -3,33 +3,50 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer internal class SwapAmountSelectQuoteTransformer( private val quoteUM: SwapQuoteUM, - private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val secondaryMaximumAmountBoundary: EnterAmountBoundary, private val secondaryMinimumAmountBoundary: EnterAmountBoundary, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState + val providerErrorConverter = SwapAmountErrorConverter( + cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency, + ) + return prevState.copy( - isPrimaryButtonEnabled = true, + isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, selectedQuote = quoteUM, - secondaryAmount = if ( - prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct - ) { + primaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) { + val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content + val amountField = swapAmountField?.amountField as? AmountState.Data + + val amountError = (quoteUM as? SwapQuoteUM.Error)?.expressError?.let(providerErrorConverter::convert) + + swapAmountField?.copy( + amountField = amountField?.copy( + amountTextField = amountField.amountTextField.copy( + error = amountError ?: TextReference.EMPTY, + isError = amountError != null, + ), + ) ?: swapAmountField.amountField, + ) ?: prevState.primaryAmount + } else { + prevState.primaryAmount + }, + secondaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) { val secondaryAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content val fromAmount = (prevState.primaryAmount.amountField as? AmountState.Data) ?.amountTextField?.cryptoAmount?.value.orZero() @@ -38,17 +55,17 @@ internal class SwapAmountSelectQuoteTransformer( swapDirection = prevState.swapDirection, fromTokenAmount = fromAmount, toTokenAmount = toAmount.orZero(), - primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, ) secondaryAmountField?.copy( priceImpact = priceImpact, amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, maxEnterAmount = secondaryMaximumAmountBoundary, minimumTransactionAmount = secondaryMinimumAmountBoundary, - value = toAmount?.parseBigDecimal(secondaryCryptoCurrencyStatus.currency.decimals) + value = toAmount?.parseBigDecimal(prevState.secondaryCryptoCurrencyStatus.currency.decimals) .orEmpty(), ).transform(secondaryAmountField.amountField), ) ?: prevState.secondaryAmount diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index ac39b1417e..d890417658 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -1,29 +1,15 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers -import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer -import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -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.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.express.models.ExpressError -import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.swap.v2.impl.R -import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM -import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM -import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent import com.tangem.utils.StringsSigns import com.tangem.utils.extensions.isPositive -import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -31,91 +17,30 @@ import java.math.BigDecimal internal class SwapAmountSetQuotesTransformer( private val quotes: List, - private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val secondaryMaximumAmountBoundary: EnterAmountBoundary, private val secondaryMinimumAmountBoundary: EnterAmountBoundary, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - val fromAmount = when (prevState.swapDirection) { - SwapDirection.Direct -> prevState.primaryAmount.amountField - SwapDirection.Reverse -> prevState.secondaryAmount.amountField - } as? AmountState.Data - val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value.orZero() - val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty - return prevState.copy( - isPrimaryButtonEnabled = bestQuote is SwapQuoteUM.Content, + val selectQuoteTransformer = SwapAmountSelectQuoteTransformer( + quoteUM = bestQuote, + secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, + secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + ) + + val updatedState = selectQuoteTransformer.transform(prevState = prevState) + if (updatedState !is SwapAmountUM.Content) return prevState + + return updatedState.copy( + isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotes.isNotEmpty(), swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote), - selectedQuote = bestQuote, - primaryAmount = if ( - prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct - ) { - val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content - val amountField = swapAmountField?.amountField as? AmountState.Data - - val amountError = (bestQuote as? SwapQuoteUM.Error)?.expressError.getAmountError() - - if (amountField?.amountTextField?.isError == true) { - prevState.primaryAmount - } else { - swapAmountField?.copy( - amountField = amountField?.copy( - amountTextField = amountField.amountTextField.copy( - error = amountError ?: TextReference.EMPTY, - isError = amountError != null, - ), - ) ?: swapAmountField.amountField, - ) ?: prevState.primaryAmount - } - } else { - prevState.primaryAmount - }, - secondaryAmount = if ( - prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct - ) { - val amountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content - val toAmount = (bestQuote as? SwapQuoteUM.Content)?.quoteAmount - - val priceImpact = calculatePriceImpact( - swapDirection = prevState.swapDirection, - fromTokenAmount = fromAmountValue, - toTokenAmount = toAmount.orZero(), - primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, - ) - - amountField?.copy( - priceImpact = priceImpact, - amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, - maxEnterAmount = secondaryMaximumAmountBoundary, - minimumTransactionAmount = secondaryMinimumAmountBoundary, - value = toAmount?.parseBigDecimal(secondaryCryptoCurrencyStatus.currency.decimals).orEmpty(), - ).transform(amountField.amountField), - ) ?: prevState.secondaryAmount - } else { - prevState.secondaryAmount - }, ) } - private fun ExpressError?.getAmountError(): TextReference? = when (this) { - is ExpressError.AmountError.TooSmallError -> resourceReference( - R.string.express_provider_min_amount, - wrappedList(amount.format { crypto(cryptoCurrency = primaryCryptoCurrencyStatus.currency) }), - ) - is ExpressError.AmountError.TooBigError -> resourceReference( - R.string.express_provider_max_amount, - wrappedList(amount.format { crypto(cryptoCurrency = primaryCryptoCurrencyStatus.currency) }), - ) - else -> null - } - private fun getQuotesWithDiff(sortedQuotes: List, bestQuote: SwapQuoteUM): ImmutableList { return sortedQuotes.sortedWith(SwapQuotesComparator) .map { quote -> diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt index 59687934ce..1c0dbd8086 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt @@ -2,15 +2,11 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount import com.tangem.utils.transformer.Transformer -@Suppress("LongParameterList") internal class SwapAmountValueChangeTransformer( - private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val primaryMaximumAmountBoundary: EnterAmountBoundary, private val secondaryMaximumAmountBoundary: EnterAmountBoundary, private val primaryMinimumAmountBoundary: EnterAmountBoundary, @@ -25,7 +21,7 @@ internal class SwapAmountValueChangeTransformer( onPrimaryAmount = { copy( amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, maxEnterAmount = primaryMaximumAmountBoundary, minimumTransactionAmount = primaryMinimumAmountBoundary, value = value, @@ -35,7 +31,7 @@ internal class SwapAmountValueChangeTransformer( onSecondaryAmount = { copy( amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, maxEnterAmount = secondaryMaximumAmountBoundary, minimumTransactionAmount = secondaryMinimumAmountBoundary, value = value, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt index 8392215305..980216e2a3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt @@ -2,15 +2,12 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.transformer.Transformer internal class SwapAmountValueMaxTransformer( - private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val primaryMaximumAmountBoundary: EnterAmountBoundary, private val secondaryMaximumAmountBoundary: EnterAmountBoundary, private val primaryMinimumAmountBoundary: EnterAmountBoundary, @@ -25,7 +22,7 @@ internal class SwapAmountValueMaxTransformer( onPrimaryAmount = { copy( amountField = AmountFieldSetMaxAmountTransformer( - cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, maxAmount = primaryMaximumAmountBoundary, minAmount = primaryMinimumAmountBoundary, ).transform(prevState.primaryAmount.amountField), @@ -34,7 +31,7 @@ internal class SwapAmountValueMaxTransformer( onSecondaryAmount = { copy( amountField = AmountFieldSetMaxAmountTransformer( - cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, maxAmount = secondaryMaximumAmountBoundary, minAmount = secondaryMinimumAmountBoundary, ).transform(prevState.secondaryAmount.amountField), From 0dc200beacc9c23c1ac24bb1c4a70884badc00d0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Jun 2025 17:40:32 +0500 Subject: [PATCH 88/99] Updated on 2026-08-14 --- .../ui/amountScreen/ui/AmountBlockV2.kt | 52 +++++++++---------- .../amount/SendAmountBlockComponent.kt | 3 -- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt index 611e613b72..b9fa864b84 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt @@ -4,7 +4,7 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -13,14 +13,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -30,11 +30,11 @@ import com.tangem.core.ui.res.TangemThemePreview @Composable fun AmountBlockV2( amountState: AmountState, - currencyIconState: CurrencyIconState, isClickDisabled: Boolean, isEditingDisabled: Boolean, - onClick: () -> Unit, modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + extraContent: @Composable () -> Unit = {}, ) { if (amountState !is AmountState.Data) return val amount = amountState.amountTextField @@ -48,7 +48,7 @@ fun AmountBlockV2( val fiatAmount = amount.fiatAmount.value.format { fiat( - fiatCurrencySymbol = amount.fiatAmount.currencySymbol, + fiatCurrencySymbol = amountState.appCurrency.symbol, fiatCurrencyCode = amountState.appCurrency.code, ) } @@ -58,33 +58,26 @@ fun AmountBlockV2( } else { cryptoAmount to fiatAmount } - - val title = TextReference.Str( - stringResourceSafe( - R.string.send_from_wallet_name, - amountState.title - .resolveReference(), - ), - ) val currencyTitle = amount.cryptoAmount.currencySymbol AmountBlockV2( - title = title, + title = amountState.title, balance = amountState.availableBalance, currencyTitle = currencyTitle, - currencyIconState = currencyIconState, + currencyIconState = amountState.tokenIconState, firstAmount = firstAmount, secondAmount = secondAmount, isClickDisabled = isClickDisabled, isEditingDisabled = isEditingDisabled, onClick = onClick, modifier = modifier, + extraContent = extraContent, ) } @Suppress("LongParameterList") @Composable -internal fun AmountBlockV2( +private fun AmountBlockV2( title: TextReference, balance: TextReference, currencyTitle: String, @@ -93,14 +86,17 @@ internal fun AmountBlockV2( secondAmount: String, isClickDisabled: Boolean, isEditingDisabled: Boolean, - onClick: () -> Unit, modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + extraContent: @Composable () -> Unit = {}, ) { Column( modifier = modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) - .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) + .conditional(onClick != null) { + clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick!!) + } .padding(TangemTheme.dimens.spacing16), ) { Row { @@ -109,7 +105,7 @@ internal fun AmountBlockV2( style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) - Spacer(modifier = modifier.weight(1f)) + SpacerWMax() Text( text = balance.resolveReference(), style = TangemTheme.typography.body2, @@ -130,11 +126,16 @@ internal fun AmountBlockV2( style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) - Text( - text = secondAmount, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = secondAmount, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + extraContent() + } } Column( verticalArrangement = Arrangement.spacedBy(2.dp), @@ -163,7 +164,6 @@ private fun AmountBlockPreview(@PreviewParameter(AmountBlockV2PreviewProvider::c TangemThemePreview { AmountBlockV2( amountState = value, - currencyIconState = CurrencyIconState.Empty(), isClickDisabled = false, isEditingDisabled = false, onClick = {}, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt index 7bf133c215..fe6e07f89a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt @@ -39,11 +39,8 @@ internal class SendAmountBlockComponent( val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle() if (params.isRedesignEnabled) { - val amountState = state as? AmountState.Data ?: return - AmountBlockV2( amountState = state, - currencyIconState = amountState.tokenIconState, isClickDisabled = !isClickEnabled, isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink, onClick = onClick, From 7ee8825f978ad54d5faca4359bbfd474d6c4b8c1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Jun 2025 17:47:59 +0500 Subject: [PATCH 89/99] Updated on 2026-08-14 --- .../impl/amount/SwapAmountComponentParams.kt | 14 +- .../v2/impl/amount/model/SwapAmountModel.kt | 120 ++++++++---------- .../ui/preview/SwapAmountContentPreview.kt | 39 +++++- 3 files changed, 92 insertions(+), 81 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt index b482191a87..beb46d4941 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt @@ -1,6 +1,5 @@ package com.tangem.features.swap.v2.impl.amount -import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -15,19 +14,19 @@ internal sealed class SwapAmountComponentParams { abstract val amountUM: SwapAmountUM abstract val analyticsCategoryName: String abstract val userWallet: UserWallet - abstract val appCurrency: AppCurrency abstract val swapDirection: SwapDirection abstract val isBalanceHidingFlow: StateFlow + abstract val primaryCryptoCurrencyStatusFlow: StateFlow + abstract val secondaryCryptoCurrency: CryptoCurrency? data class AmountParams( override val amountUM: SwapAmountUM, override val analyticsCategoryName: String, override val userWallet: UserWallet, override val isBalanceHidingFlow: StateFlow, - override val appCurrency: AppCurrency, override val swapDirection: SwapDirection, - val primaryCryptoCurrency: CryptoCurrency, - val secondaryCryptoCurrency: CryptoCurrency?, + override val primaryCryptoCurrencyStatusFlow: StateFlow, + override val secondaryCryptoCurrency: CryptoCurrency?, val callback: SwapAmountComponent.ModelCallback, val currentRoute: Flow, ) : SwapAmountComponentParams() @@ -37,10 +36,9 @@ internal sealed class SwapAmountComponentParams { override val analyticsCategoryName: String, override val userWallet: UserWallet, override val isBalanceHidingFlow: StateFlow, - override val appCurrency: AppCurrency, override val swapDirection: SwapDirection, - val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?, + override val primaryCryptoCurrencyStatusFlow: StateFlow, + override val secondaryCryptoCurrency: CryptoCurrency?, val blockClickEnableFlow: StateFlow, ) : SwapAmountComponentParams() } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 8313317b76..b8461d6736 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -14,10 +14,11 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.express.models.ExpressError import com.tangem.domain.express.models.ExpressProvider -import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapQuoteModel import com.tangem.domain.swap.usecase.GetSwapPairsUseCase @@ -41,12 +42,10 @@ import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate @@ -63,19 +62,23 @@ internal class SwapAmountModel @Inject constructor( private val getSwapQuoteUseCase: GetSwapQuoteUseCase, private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener, private val getAllowanceUseCase: GetAllowanceUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val appRouter: AppRouter, ) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback { private val params: SwapAmountComponentParams = paramsContainer.require() private val swapDirection = params.swapDirection - private val appCurrency = params.appCurrency + private var appCurrency = AppCurrency.Default private var userWallet = params.userWallet - private var primaryCryptoCurrency: CryptoCurrency by Delegates.notNull() - private var secondaryCryptoCurrency: CryptoCurrency? = null + private var primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency + private var secondaryCryptoCurrency: CryptoCurrency? = params.secondaryCryptoCurrency - private var primaryCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() - private var secondaryCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var primaryCryptoCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value + private var secondaryCryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( + currency = primaryCryptoCurrencyStatus.currency, + value = CryptoCurrencyStatus.Loading, + ) private var primaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull() private var secondaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull() @@ -86,24 +89,11 @@ internal class SwapAmountModel @Inject constructor( field = MutableStateFlow(params.amountUM) init { - when (params) { - is SwapAmountComponentParams.AmountParams -> { - primaryCryptoCurrency = params.primaryCryptoCurrency - secondaryCryptoCurrency = params.secondaryCryptoCurrency - } - is SwapAmountComponentParams.AmountBlockParams -> { - primaryCryptoCurrency = params.primaryCryptoCurrencyStatus.currency - secondaryCryptoCurrency = params.secondaryCryptoCurrencyStatus?.currency - primaryCryptoCurrencyStatus = params.primaryCryptoCurrencyStatus - params.secondaryCryptoCurrencyStatus?.let { - secondaryCryptoCurrencyStatus = it - } - } + modelScope.launch { + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } } - - initialState() configAmountNavigation() - + subscribeOnCryptoCurrencyStatusFlow() observeChooseSelectToken() // todo observe balance hiding flow } @@ -182,6 +172,27 @@ internal class SwapAmountModel @Inject constructor( ) } + private fun subscribeOnCryptoCurrencyStatusFlow() { + params.primaryCryptoCurrencyStatusFlow.onEach { primaryCurrencyStatus -> + primaryCryptoCurrencyStatus = primaryCurrencyStatus + + val state = uiState.value + if (state is SwapAmountUM.Content) { + secondaryCryptoCurrency = state.primaryCryptoCurrencyStatus.currency + secondaryCryptoCurrencyStatus = state.primaryCryptoCurrencyStatus + initCurrencies( + primaryStatus = state.primaryCryptoCurrencyStatus, + secondaryStatus = state.secondaryCryptoCurrencyStatus, + ) + } else { + initPairs( + primaryCryptoCurrency = primaryCryptoCurrency, + secondaryCryptoCurrency = secondaryCryptoCurrency, + ) + } + }.launchIn(modelScope) + } + private fun observeChooseSelectToken() { swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow .onEach { currency -> @@ -202,46 +213,12 @@ internal class SwapAmountModel @Inject constructor( .launchIn(modelScope) } - private fun initialState() { - if (uiState.value is SwapAmountUM.Empty) { - initPairs( - primaryCryptoCurrency = primaryCryptoCurrency, - secondaryCryptoCurrency = secondaryCryptoCurrency, - ) - uiState.update { - SwapAmountUM.Content( - isPrimaryButtonEnabled = it.isPrimaryButtonEnabled, - primaryAmount = it.primaryAmount, - secondaryAmount = it.secondaryAmount, - swapDirection = swapDirection, - swapCurrencies = SwapCurrencies.EMPTY, - appCurrency = appCurrency, - swapQuotes = persistentListOf(), - swapRateType = ExpressRateType.Float, - selectedAmountType = SwapAmountType.From, - selectedQuote = SwapQuoteUM.Empty, - primaryCryptoCurrencyStatus = null, - secondaryCryptoCurrencyStatus = null, - ) - } - } - } - private fun initPairs(primaryCryptoCurrency: CryptoCurrency, secondaryCryptoCurrency: CryptoCurrency?) { modelScope.launch { val cryptoCurrencyStatusList = getMultiCryptoCurrencyStatusUseCase .invokeMultiWalletSync(userWallet.walletId) .getOrElse { emptyList() } - val primaryStatus = cryptoCurrencyStatusList.firstOrNull { - it.currency.id == primaryCryptoCurrency.id - } - if (primaryStatus == null) { - Timber.e("Failed to get crypto currency status") - // todo error - return@launch - } - val cryptoCurrencyStatusListExceptPrimary = cryptoCurrencyStatusList.filter { val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount val notCustomTokenFilter = !it.currency.isCustom @@ -263,7 +240,9 @@ internal class SwapAmountModel @Inject constructor( ) if (secondaryStatus != null) { - initCurrencies(primaryStatus, secondaryStatus) + initCurrencies(primaryCryptoCurrencyStatus, secondaryStatus) + this@SwapAmountModel.secondaryCryptoCurrency = secondaryCryptoCurrency + secondaryCryptoCurrencyStatus = secondaryStatus uiState.update { SwapAmountReadyStateConverter( swapCurrencies = swapCurrencies, @@ -288,9 +267,6 @@ internal class SwapAmountModel @Inject constructor( } private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus) { - primaryCryptoCurrencyStatus = primaryStatus - secondaryCryptoCurrencyStatus = secondaryStatus - primaryMinimumAmountBoundary = EnterAmountBoundary( amount = getMinimumTransactionAmountSyncUseCase .invoke( @@ -317,8 +293,12 @@ internal class SwapAmountModel @Inject constructor( val state = uiState.value as? SwapAmountUM.Content ?: return val (fromCryptoCurrency, toCryptoCurrency) = when (state.swapDirection) { - SwapDirection.Direct -> primaryCryptoCurrencyStatus.currency to secondaryCryptoCurrencyStatus.currency - SwapDirection.Reverse -> secondaryCryptoCurrencyStatus.currency to primaryCryptoCurrencyStatus.currency + SwapDirection.Direct -> { + state.primaryCryptoCurrencyStatus.currency to state.secondaryCryptoCurrencyStatus.currency + } + SwapDirection.Reverse -> { + state.secondaryCryptoCurrencyStatus.currency to state.primaryCryptoCurrencyStatus.currency + } } val fromAmount = when (state.swapDirection) { @@ -338,9 +318,11 @@ internal class SwapAmountModel @Inject constructor( uiState.transformerUpdate(SwapQuoteLoadingStateTransformer) modelScope.launch { - val quotes = swapGroups.firstOrNull { + val quotes = swapGroups.filter { it.currencyStatus.currency.id == toCryptoCurrency.id - }?.providers?.map { provider -> + }.flatMap { + it.providers + }.map { provider -> async { getSwapQuoteUseCase( userWallet = userWallet, @@ -353,7 +335,7 @@ internal class SwapAmountModel @Inject constructor( SwapQuoteUM.Error( provider = provider, expressError = error, - ) + ).takeIf { error is ExpressError.AmountError } }, ifRight = { quote: SwapQuoteModel -> convertToSwapProviderUM( @@ -365,7 +347,7 @@ internal class SwapAmountModel @Inject constructor( }, ) } - }?.awaitAll().orEmpty() + }.awaitAll().filterNotNull() uiState.transformerUpdate( SwapAmountSetQuotesTransformer( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 6db113e583..596cecef01 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -6,8 +6,11 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM @@ -16,6 +19,34 @@ import kotlinx.collections.immutable.persistentListOf internal data object SwapAmountContentPreview { + private val cryptoCurrencyStatus = CryptoCurrencyStatus( + currency = CryptoCurrency.Coin( + id = CryptoCurrency.ID.fromValue("coin⟨BITCOIN⟩bitcoin"), + network = Network( + id = Network.ID( + value = "bitcoin", + derivationPath = Network.DerivationPath.None, + ), + backendId = "bitcoin", + name = "Bitcoin", + currencySymbol = "BTC", + derivationPath = Network.DerivationPath.None, + isTestnet = false, + standardType = Network.StandardType.Unspecified("bitcoin"), + hasFiatFeeRate = false, + canHandleTokens = false, + transactionExtrasType = Network.TransactionExtrasType.NONE, + + ), + name = "Bitcoin", + symbol = "BTC", + decimals = 8, + iconUrl = "", + isCustom = false, + ), + value = CryptoCurrencyStatus.Loading, + ) + val emptyState = SwapAmountUM.Content( isPrimaryButtonEnabled = false, primaryAmount = SwapAmountFieldUM.Empty( @@ -29,8 +60,8 @@ internal data object SwapAmountContentPreview { swapCurrencies = SwapCurrencies.EMPTY, swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, - primaryCryptoCurrencyStatus = null, - secondaryCryptoCurrencyStatus = null, + primaryCryptoCurrencyStatus = cryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, swapRateType = ExpressRateType.Float, appCurrency = AppCurrency.Default, ) @@ -65,8 +96,8 @@ internal data object SwapAmountContentPreview { swapCurrencies = SwapCurrencies.EMPTY, swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, - primaryCryptoCurrencyStatus = null, - secondaryCryptoCurrencyStatus = null, + primaryCryptoCurrencyStatus = cryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, swapRateType = ExpressRateType.Float, isPrimaryButtonEnabled = true, ) From 4febcb6c9371e566b2101bcf3d0bd9e1fc2a08a2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Jun 2025 17:51:33 +0500 Subject: [PATCH 90/99] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 6 +- core/res/src/main/res/values-ja/strings.xml | 76 ++++++- core/res/src/main/res/values-ru/strings.xml | 5 + .../impl/amount/SwapAmountBlockComponent.kt | 104 +++++++++ .../v2/impl/amount/model/SwapAlertFactory.kt | 64 ++++++ .../amount/model/SwapAmountClickIntents.kt | 1 + .../v2/impl/amount/model/SwapAmountModel.kt | 17 ++ .../impl/amount/ui/SwapAmountBlockContent.kt | 201 ++++++++++++++++++ .../ui/preview/SwapAmountClickIntentsStub.kt | 2 + 9 files changed, 473 insertions(+), 3 deletions(-) create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAlertFactory.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index e90c8e89e2..1432eaaaa2 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -574,7 +574,7 @@ Netzwerk nicht hinzugefügt Nicht hinzugefügt Nicht unterstützte NFT-Arten - cNFTs und pNFTs werden noch nicht unterstützt + cNFTs und pNFTs werden derzeit nicht unterstützt. Bitte sende diese nicht an Deine Wallet. NFT senden Eigenschaften Unbetitelte Sammlung @@ -821,6 +821,7 @@ Überprüfe deine Netzwerkverbindung Informationen zur Netzwerkgebühr nicht erreichbar Von + Von %s Grenzwert Gasgebühr Dies ist die maximale Gasgebühr, die für den Abschluss einer Transaktion oder eines Vertrags ausgegeben wird. Ein Gaslimit verhindert unerwartete oder unbegrenzte Gebühren bei der Ausführung einer Transaktion. Gaspreis @@ -828,6 +829,7 @@ Max Höchstbetrag Gebühr bis zu + Memo: %s Ungültiges Memo Abdeckung der Netzgebühren Nonce @@ -878,6 +880,7 @@ Die Netzwerkgebühr wird durch die Nutzung von %1$s Energieträgern gedeckt. Die Netzwerkgebühr wird durch den Verbrauch von %1$s Energie reduziert inklusive einer Netzgebühr von %1$s + Zur Adresse Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. Der Betrag geht über die Bilanz hinaus @@ -887,6 +890,7 @@ Die Gebühr geht über die Bilanz hinaus Der Gesamtbetrag geht über die Bilanz hinaus Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht – nahtlos. + Wird an den Empfänger gesendet Senden mit Swap Transaktion gesendet Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 11cb1fb068..06b321ef1a 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1,5 +1,14 @@ + アクセスコードがないとウォレットは保護されません。 + とにかくスキップ + アクセスコードが設定されていません + アクセスコードを入力 + 続行するには、以前に入力したコードを確認してください + アクセスコードを再入力 + ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 + アクセスコードの作成 + アクセスコード トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して買付できるようにします。 トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して売却できるようにします。 売却 @@ -49,6 +58,17 @@ システムのデフォルト テーマ アプリ設定 + ウォレットのバックアップが正常に完了しました。 + バックアップが完了しました + シークレットリカバリーフレーズは、ウォレットへのアクセスと復元のために使用される%sのランダムな単語のセットです。 + これらの単語は紛失した場合、復元できません。必ず安全な場所に保管してください。 + 安全に保管してください + これらの%s個の単語をパスワードマネージャーなどの安全な場所に保存し、決して他の人と共有しないでください。 + 復元は不可能です + リカバリーフレーズ + これらの%s語を順番に書き留めて、安全かつプライベートに保管してください。 + ウォレットと、リカバリーフレーズのセキュリティとバックアップの全責任は、Tangemではなくユーザーにあります。 + リカバリーフレーズ 残高を表示または非表示にするには、デバイスの画面を下向きにするか、設定でオフにしてください。 今後表示しない わかりました @@ -101,6 +121,7 @@ 承認 承認 注意 + バックアップ 残高: %s 残高 生体認証 @@ -117,10 +138,12 @@ 請求 報酬を受け取る 閉じる + 近日公開 確認 Tangemサポートへ問い合わせる Visaサポートへ問い合わせる 続ける + 変換する コピー アドレスをコピー 作成 @@ -150,6 +173,7 @@ 遅い 速度と料金 終了 + 無料 アドレスを同期する プロバイダーへ移動 トークンへ移動 @@ -202,6 +226,7 @@ もっと見る 署名 署名して送信 + スキップ ステーキング ステーキング 始める @@ -222,6 +247,7 @@ アクセスできません ステーキング解除 %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 + 値がコピーされました はい @@ -377,10 +403,19 @@ 許可を与える 無制限 既存のウォレットを追加 + 新しいウォレットを作成する Tangemを注文 Tangemをスキャン %sへ %sネットワーク + バックアップへ移動 + アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。 + まずバックアップを完了する + 受信取引の通知を受け取る + 鍵はアプリに保存されます + 最新の機能とニュースをお届けします + シードフレーズのバックアップ + モバイルウォレットを作成する この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。 アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。 パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 @@ -565,7 +600,7 @@ ネットワークが追加されていません 未追加 サポートされていないNFTタイプ - cNFTとpNFTはまだサポートされていません + cNFTとpNFTはまだサポートされていません。ウォレットに送信しないでください。 NFTを送信する 特徴 無題のコレクション @@ -621,6 +656,7 @@ 秘密鍵を非公開で生成する カードは有効化され、使用可能になりました 成功! + ウォレットの設定が完了し、使用できるようになりました。 この場合は、最初からやり直す必要があります。 アクティベーション処理を途中で終了しますか? スタート @@ -783,7 +819,9 @@ 推奨手数料を下回る手数料が指定されたため、取引に遅れが生じる可能性があります。続行しますか? 理由: %1$s \nコード: %2$s 取引は完了していません + 別のトークンに変換する 金額 + 受取人に送信されます 取引手数料は、vByteフィールドのSatoshiの値を調整して設定できます。 取引にかかる手数料です。自由に設定できます。 最大手数料 @@ -807,6 +845,7 @@ ネットワーク接続を確認してください ネットワーク手数料についての情報にアクセスできません より + %sから ガス上限 これは、取引または契約を完了するために使用されるガスの最大額です。ガス上限を設定することで、取引実行時に予期せぬ請求や無制限の請求を防ぐことができます。 ガス代 @@ -814,6 +853,7 @@ 最大 最大金額 最大手数料 + メモ: %s 無効なメモ ネットワーク手数料のカバー ナンス @@ -827,6 +867,7 @@ カスタム手数料が高くなっています %1$sネットワークの特殊性により、残高全体を転送する場合の手数料は高くなります。手数料を削減するには、 %2$sを残します。 手数料が高くなっています + 受取人の口座が有効化されていません。最低送金額はレント免除残高%1$s以上である必要があります。 口座残高はレンタル料金より低くすることはできません。残高を少なくとも%1$sに維持するか、すべての資金を引き出してください。 手数料が送金金額を超えており、マイナスの値になってしまいます。 無効な金額 @@ -845,7 +886,7 @@ 最近の取引 受取人 有効なアドレスではありません - トークンを失わないように、受信ウォレットアドレスが%sネットワーク上にあることを確認してください。 + トークンの損失を避けるために、受信ウォレットアドレスが** %sネットワーク**上にあることを確認してください %s に送信 %sネットワークアドレスを確認してください。エラーが発生すると転送が失われる可能性があります。 送金先 @@ -864,6 +905,7 @@ ネットワーク手数料は%1$sエネルギーを使用してカバーされます %1$sエネルギーを使用するとネットワーク手数料が減額されます ネットワーク手数料%1$sを含む + アドレスへ 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 金額が残高を超えています @@ -873,6 +915,8 @@ 手数料が残高を超えています 合計金額が残高を超えています トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 + 受信者に送信されます + 受取金額 スワップして送信 取引が送信されました 設定したいカードまたはリングをスキャンするために準備してください。 @@ -1028,6 +1072,7 @@ 破格のレート 手間がかからず直感的に操作でき、数回タップするだけでトークンを交換できます。 とにかく便利 + プロバイダー経由のスワップ この金額には以下が含まれます:\n- サービスプロバイダーの手数料\n- 取引所からユーザーのアドレスに%s を送り返すためのネットワーク手数料。 金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです この金額には、サービスプロバイダーの手数料が含まれています。 @@ -1178,6 +1223,27 @@ dAppsに接続する WalletConnect 接続には数秒かかる場合があります + %sから + Tangemウォレットを購入 — 暗号資産をオフラインで安全に保管できる物理カードです。 + ハードウェアウォレット + 数秒でスマートフォンに安全なウォレットが作成されます。 + モバイルウォレット + 何を選ぶべき? + すでにTangemウォレットをお持ちですか? + 今すぐスキャン + ウォレットの作成方法を選択してください + Tangemウォレットを購入しますか? + 今すぐ購入 + 既存のウォレットを追加 + 暗号資産をオフラインで安全に保管する物理カード。 + Tangemウォレットをスキャンする + ウォレットを復元するためのシードフレーズ + ウォレットをインポート + リカバリーフレーズをインポートする + ウォレットのバックアップが正常に完了しました。 + ウォレットをインポート + インポート完了 + ウォレットをインポート %s市場価格 直近24時間 %sネットワーク @@ -1186,6 +1252,8 @@ 今すぐ10 % オフで購入 1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。 Tangemウォレットを見る + このウォレットを保護するためのシークレットコードです。ログインと署名に使用されます。 + アクセスコードの設定 / 変更 ウォレットの受信取引とTangemの更新について通知を受け取る。 取引通知 ウォレット設定 @@ -1274,6 +1342,10 @@ 下にスワイプして更新するか、後でもう一度お試しください。 一部のネットワークにアクセスできません 一部のトークン残高を更新できませんでした + トラストラインを有効にする + %sが足りません。このトークンを関連付けるには、XLMアカウントに資金を追加してください。 + このトークンを受け取るには、トラストラインを有効にする必要があります。ネットワークには 0.5 XLMの準備金が必要です。 + トラストラインが必要 これはテストネットカードです。取引処理はできませんので、テストおよび開発目的でのみご利用ください。 テスト目的のみ 残高が古い可能性があります。ページを更新してください。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 8ac257b580..4c4ce1336a 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -223,6 +223,7 @@ Недоступно Завершить стейкинг Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. + Значение скопировано неделю с Да @@ -843,6 +844,7 @@ Установлена высокая комиссия Ввиду особенности сети %1$s комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %2$s. Комиссия повышена + Аккаунт получателя не активирован. Минимальная сумма перевода должна быть не меньше баланса, необходимого для покрытия арендной платы: %1$s. Баланс вашего счета не может быть ниже арендной платы. Пожалуйста, оставьте на счете не менее %1$s или выведите все средства. Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению Недопустимая сумма @@ -1259,6 +1261,9 @@ Свайпните вниз для обновления или попробуйте позже. Некоторые сети недоступны Некоторые балансы токенов не удалось обновить + Открыть Trustline + Чтобы получить этот токен, необходимо открыть Trustline. Сеть удержит резерв в размере 0.5 XLM. + Откройте Trustline Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. Только для целей тестирования Возможно, баланс устарел. Обновите страницу. diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt new file mode 100644 index 0000000000..f0f3fc46a4 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt @@ -0,0 +1,104 @@ +package com.tangem.features.swap.v2.impl.amount + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams.AmountBlockParams +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel +import com.tangem.features.swap.v2.impl.amount.ui.SwapAmountBlockContent +import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +internal class SwapAmountBlockComponent( + private val appComponentContext: AppComponentContext, + private val params: AmountBlockParams, + private val onResult: (SwapAmountUM) -> Unit, + val onClick: () -> Unit, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SwapAmountModel = getOrCreateModel(params = params) + + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = true, + childFactory = ::bottomSheetChild, + ) + + init { + model.uiState.onEach { + onResult(it) + }.launchIn(componentScope) + } + + fun updateState(amountUM: SwapAmountUM) = model.updateState(amountUM) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() + + val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle() + + SwapAmountBlockContent( + amountUM = state, + onInfoClick = model::onInfoClick, + isClickEnabled = isClickEnabled, + onClick = onClick, + onProviderSelectClick = { + val amountUM = model.uiState.value as? SwapAmountUM.Content ?: return@SwapAmountBlockContent + val selectedProvider = amountUM.selectedQuote.provider ?: return@SwapAmountBlockContent + val cryptoCurrency = params.secondaryCryptoCurrency ?: return@SwapAmountBlockContent + + model.bottomSheetNavigation.activate( + SwapChooseProviderConfig( + providers = amountUM.swapQuotes, + cryptoCurrency = cryptoCurrency, + selectedProvider = selectedProvider, + ), + ) + }, + ) + + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: SwapChooseProviderConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent { + return SwapChooseProviderComponent( + context = childByContext(componentContext), + params = SwapChooseProviderComponent.Params( + providers = config.providers, + cryptoCurrency = config.cryptoCurrency, + selectedProvider = config.selectedProvider, + callback = model, + onDismiss = { model.bottomSheetNavigation.dismiss() }, + ), + ) + } + + data class SwapChooseProviderConfig( + val providers: ImmutableList, + val cryptoCurrency: CryptoCurrency, + val selectedProvider: ExpressProvider, + ) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAlertFactory.kt new file mode 100644 index 0000000000..80af642d28 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAlertFactory.kt @@ -0,0 +1,64 @@ +package com.tangem.features.swap.v2.impl.amount.model + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.features.swap.v2.impl.R +import com.tangem.utils.StringsSigns.PERCENT +import javax.inject.Inject + +@ModelScoped +internal class SwapAlertFactory @Inject constructor( + private val uiMessageSender: UiMessageSender, +) { + + fun priceImpactAlert(hasPriceImpact: Boolean, currencySymbol: String, provider: ExpressProvider) { + val slippage = provider.slippage?.let { "${it.parseBigDecimal(1)}$PERCENT" } + val combinedMessage = buildList { + when (provider.type) { + ExpressProviderType.CEX -> { + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_cex_description_with_slippage, + formatArgs = wrappedList(currencySymbol, slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(currencySymbol))) + } + } + ExpressProviderType.DEX, + ExpressProviderType.DEX_BRIDGE, + -> { + if (hasPriceImpact) { + add(resourceReference(R.string.swapping_high_price_impact_description)) + add(stringReference("\n\n")) + } + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_dex_description_with_slippage, + formatArgs = wrappedList(slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_dex_description, wrappedList(currencySymbol))) + } + } + else -> Unit + } + } + uiMessageSender.send( + DialogMessage( + title = resourceReference(R.string.swapping_alert_title), + message = combinedReference(combinedMessage.toWrappedList()), + firstActionBuilder = { okAction() }, + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt index f9c92b77a1..34cf98f664 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt @@ -6,5 +6,6 @@ import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType internal interface SwapAmountClickIntents : AmountScreenClickIntents { fun onExpandEditField(selectedAmountType: SwapAmountType) + fun onInfoClick() fun onSelectTokenClick() } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index b8461d6736..97287393a0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.swap.v2.impl.amount.model import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter @@ -29,6 +30,7 @@ import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.GetAllowanceUseCase import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent.SwapChooseProviderConfig import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType @@ -64,6 +66,7 @@ internal class SwapAmountModel @Inject constructor( private val getAllowanceUseCase: GetAllowanceUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val appRouter: AppRouter, + private val swapAlertFactory: SwapAlertFactory, ) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback { private val params: SwapAmountComponentParams = paramsContainer.require() @@ -85,6 +88,8 @@ internal class SwapAmountModel @Inject constructor( private var primaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull() private var secondaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val uiState: StateFlow field = MutableStateFlow(params.amountUM) @@ -121,6 +126,18 @@ internal class SwapAmountModel @Inject constructor( } } + override fun onInfoClick() { + val amountUM = uiState.value as? SwapAmountUM.Content ?: return + val selectedProvider = amountUM.selectedQuote.provider ?: return + val cryptoCurrency = secondaryCryptoCurrency ?: return + + swapAlertFactory.priceImpactAlert( + hasPriceImpact = (amountUM.secondaryAmount as? SwapAmountFieldUM.Content)?.priceImpact != null, + currencySymbol = cryptoCurrency.symbol, + provider = selectedProvider, + ) + } + override fun onAmountValueChange(value: String) { uiState.transformerUpdate( SwapAmountValueChangeTransformer( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt new file mode 100644 index 0000000000..291627de98 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -0,0 +1,201 @@ +package com.tangem.features.swap.v2.impl.amount.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Canvas +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.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.ui.AmountBlockV2 +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview +import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent + +@Suppress("DestructuringDeclarationWithTooManyEntries") +@Composable +internal fun SwapAmountBlockContent( + amountUM: SwapAmountUM, + isClickEnabled: Boolean, + onProviderSelectClick: () -> Unit, + onInfoClick: () -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (amountUM !is SwapAmountUM.Content) return + ConstraintLayout( + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.action) + .clickable( + indication = ripple(), + interactionSource = remember { MutableInteractionSource() }, + enabled = isClickEnabled, + onClick = onClick, + ), + ) { + val (from, to, separator, provider) = createRefs() + AmountBlockV2( + amountState = amountUM.primaryAmount.amountField, + isClickDisabled = true, + isEditingDisabled = false, + modifier = Modifier.constrainAs(from) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + extraContent = { + SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick) + }, + ) + AmountBlockV2( + amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( + title = resourceReference(R.string.send_with_swap_recipient_amount_title), + availableBalance = TextReference.EMPTY, + ) ?: amountUM.secondaryAmount.amountField, + isClickDisabled = true, + isEditingDisabled = false, + modifier = Modifier.constrainAs(to) { + top.linkTo(from.bottom, 8.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + extraContent = { + SwapPriceImpact(amountFieldUM = amountUM.secondaryAmount, onInfoClick = onInfoClick) + }, + ) + SwapAmountDivider( + modifier = Modifier.constrainAs(separator) { + top.linkTo(from.bottom) + bottom.linkTo(to.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) + SwapChooseProviderContent( + expressProvider = amountUM.selectedQuote.provider, + onClick = onProviderSelectClick, + modifier = Modifier.constrainAs(provider) { + top.linkTo(to.bottom) + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) + } +} + +@Composable +private fun SwapPriceImpact(amountFieldUM: SwapAmountFieldUM, onInfoClick: () -> Unit) { + val priceImpact = (amountFieldUM as? SwapAmountFieldUM.Content)?.priceImpact + if (priceImpact != null) { + Text( + text = priceImpact.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.attention, + ) + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_information_24), + ), + tint = TangemTheme.colors.icon.attention, + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = onInfoClick, + ), + ) + } +} + +@Composable +private fun SwapAmountDivider(modifier: Modifier = Modifier) { + Box(modifier = modifier) { + SwapDivider() + Text( + text = stringResourceSafe(R.string.swap_via_provider), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .background(TangemTheme.colors.stroke.primary, RoundedCornerShape(32.dp)) + .padding(1.dp) + .background(TangemTheme.colors.text.primary2, RoundedCornerShape(32.dp)) // workaround + .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f), RoundedCornerShape(32.dp)) + .padding(horizontal = 11.dp, vertical = 5.dp) + .align(Alignment.Center), + ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun BoxScope.SwapDivider() { + val color = TangemTheme.colors.background.tertiary + Canvas( + Modifier + .fillMaxWidth() + .align(Alignment.Center), + ) { + val gapWidth = 2.dp.toPx() + val radius = 2.dp.toPx() + + val pathEffect = PathEffect.dashPathEffect( + intervals = floatArrayOf(gapWidth, gapWidth * 3), + phase = 0f, + ) + drawLine( + color = color, + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + pathEffect = pathEffect, + cap = StrokeCap.Round, + strokeWidth = radius, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun SwapAmountBlockContent_Preview() { + TangemThemePreview { + SwapAmountBlockContent( + amountUM = SwapAmountContentPreview.defaultState, + isClickEnabled = true, + onProviderSelectClick = {}, + onInfoClick = {}, + onClick = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt index 98bcb53665..194e61c3d3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt @@ -6,6 +6,8 @@ import com.tangem.features.swap.v2.impl.amount.model.SwapAmountClickIntents internal object SwapAmountClickIntentsStub : SwapAmountClickIntents { override fun onExpandEditField(selectedAmountType: SwapAmountType) {} + override fun onInfoClick() {} + override fun onSelectTokenClick() {} override fun onAmountValueChange(value: String) {} From cec47458033aab86adc377844b640aa449c07e80 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 19:45:12 +0500 Subject: [PATCH 91/99] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 4 +- core/res/src/main/res/values-es/strings.xml | 2 +- core/res/src/main/res/values-fr/strings.xml | 2 +- core/res/src/main/res/values-ja/strings.xml | 9 +++- .../src/main/res/values-uk-rUA/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 42 +++++++++++++++++-- 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 1432eaaaa2..f369cd7218 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -580,7 +580,7 @@ Unbetitelte Sammlung %1$d NFTs in der %2$d Sammlung - + %1$d NFT in der %2$d Sammlung %1$d NFTs in der %2$d Sammlung Tippe hier, um das erste NFT zu erhalten @@ -1320,7 +1320,7 @@ Wir haben einen unbekannten Fehler festgestellt. Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht. Nicht unterstützte Netzwerke - Tangem unterstützt ein erforderliches Netzwerk um %s. + Tangem unterstützt ein erforderliches Netzwerk um %s Verifizierte Domain Falsche Karte oder falscher Ring in der App ausgewählt Wir haben eine Art Problem diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index bc9fd8a0e8..46aea2ae3b 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -560,7 +560,7 @@ Colección sin título %1$d NFT en %2$d colecciones - + %1$d NFT en %2$d coleccione %1$d NFTs en %2$d colecciones Pulse aquí para recibir el primer NFT diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 5bd4e39551..b3b307cac4 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -561,7 +561,7 @@ Collection sans titre %1$d NFT dans la collection %2$d - + %1$d NFT dans la collection %2$d %1$d NFT dans la collection %2$d Appuyez ici pour recevoir le premier NFT diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 06b321ef1a..731aa8038e 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -408,9 +408,14 @@ Tangemをスキャン %sへ %sネットワーク + iCloudバックアップに保存されている既存のウォレットを復元する + iCloudバックアップ バックアップへ移動 アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。 まずバックアップを完了する + バックアップなし + 暗号資産をオフラインで安全に保管する物理カード + リカバリーフレーズ 受信取引の通知を受け取る 鍵はアプリに保存されます 最新の機能とニュースをお届けします @@ -1234,6 +1239,8 @@ ウォレットの作成方法を選択してください Tangemウォレットを購入しますか? 今すぐ購入 + Googleドライブのバックアップに保存されている既存のウォレットを復元する + Googleドライブからインポート 既存のウォレットを追加 暗号資産をオフラインで安全に保管する物理カード。 Tangemウォレットをスキャンする @@ -1368,7 +1375,7 @@ 不明なエラーが発生しました Tangemは現在%sで必要なネットワークをサポートしていません。 サポートされていないネットワーク - Tangemは%sで必要なネットワークをサポートします。 + Tangemは%sで必要なネットワークをサポートします 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 314b79ecef..d1048dabe7 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -574,7 +574,7 @@ Риси NFTs в %2$d колекціях%1$d - + %1$d NFT в %2$d колекціi %1$d NFTs в %2$d колекціях diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 28efd231b8..9499629f9e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,5 +1,14 @@ + Your wallet won’t be protected without an Access Code. + Skip anyway + Access Code not set + Enter Access Code + Confirm your previously entered code to continue + Re-enter Access Code + Set a %s-digit Access Code to unlock your wallet. + Create Access Code + Access code Can’t find your token? Go to the Market section on the main page and add it to your portfolio for purchase Can’t find your token? Go to the Market section on the main page and add it to your portfolio for selling. Sell @@ -49,6 +58,17 @@ System default Theme App settings + You successfully backed up your wallet. + Backup Completed + Your Secret Recovery Phrase is a fixed set of %s random words used to access and recover your wallet. + These words can’t be recovered if lost. Make sure to keep it somewhere secure. + Keep It Safe + Save these %s words in a secure location, such as a password manager, and never share them with anyone. + No Recovery Possible + Recovery phrase + Write down these %s words in order and keep them safe and private + Full responsibility for the security and backup of the wallet and recovery phrase lies with the user, not with Tangem. + Recovery phrase To hide or show your balances, simply flip your device screen down, or switch it off in Settings Don\'t show again Got it @@ -103,6 +123,7 @@ Approval Approve Attention + Backup Balance: %s Balance biometric authentication @@ -124,6 +145,7 @@ Contact Tangem Support Contact Visa Support Continue + Convert Copy Copy address Create @@ -208,6 +230,7 @@ Show more Sign Sign and send + Skip Stake Staking Start @@ -229,6 +252,7 @@ Unreachable Unstake Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. + Value copied week with Yes @@ -596,7 +620,7 @@ Untitled collection %1$d NFTs in %2$d collections - + %1$d NFT in %2$d collection %1$d NFTs in %2$d collections Tap here to receive first NFT @@ -647,6 +671,7 @@ Generate keys privately Your card is activated and ready to be used Success! + Your wallet is set up and ready to use! In this case, you will need to start from the beginning. Do you want to exit the activation process? Getting started @@ -813,7 +838,9 @@ You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? Reason: %1$s\nCode: %2$s The transaction is not completed + Convert to another token Amount + Will be sent to recipient You can set your transaction fee by adjusting the value in the Satoshi per vByte field. The fee that will be charged for your transaction. You can set your own value. Max fee @@ -859,6 +886,7 @@ Custom fee is high Due to the peculiarities of the %1$s network, the fee for transferring the entire balance is higher. To reduce the commission, you can leave %2$s. The fee is higher + The recipient account is not activated. The minimum transfer amount must be equal to or greater than the rent-exempt balance: %1$s. Your account balance cannot be lower than the rent fee. Please maintain at least %1$s on your account or withdraw all funds. The included commission exceeds the transfer amount, leading to a negative value Invalid amount @@ -877,7 +905,7 @@ Recent Recipient Not a valid address - Ensure the receiving wallet address is on the %s network to avoid losing your tokens + Ensure the receiving wallet address is on **the %s network** to avoid losing your tokens send to %s Ensure you %s network address, as errors may result in lost transfers Send to @@ -907,6 +935,7 @@ Total amount exceeds balance Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. Will be sent a recipient + Amount to receive Send with swap Transaction sent Prepare to scan card or ring you want to set up. @@ -1062,6 +1091,7 @@ Unbeatable Rates Hassle-free and intuitive, allowing you to swap tokens in just a few taps Simply Convenient + Swap via provider The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. The amount includes:\n• service provider\'s fee\n• network fee for sending %1$s from the exchange back to the user\'s address. \n\nProvider slippage is up to %2$s The amount includes the service provider\'s fee. @@ -1292,6 +1322,8 @@ Get now with 10% off Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap.\nLink up to three cards for a backup. Discover Tangem Wallet + Secret code to protect this wallet. Used for login and signatures. + Set/Change Access Code Stay notified on wallet incoming transactions and Tangem updates. Transaction Notifications Wallet settings @@ -1381,6 +1413,10 @@ Swipe down to refresh or try again later. Some networks are unreachable Some token balances could not be updated + Enable Trustline + Not enough %s. Top up your XLM account to associate this token + A Trustline must be enabled to receive this token. The network requires a 0.5 XLM reserve. + Trustline Required This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. For testing purposes only Balance may be outdated. Refresh the page. @@ -1403,7 +1439,7 @@ We\'ve encountered unknown error Tangem does not currently support a required network by %s. Unsuported networks - Tangem support a required network by %s. + Tangem support a required network by %s Verified domain Wrong card or ring selected in the App We\'ve got some kind of problem From 2f160f54d972fa8f2219d5e5bff22efd41ee8926 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 16:10:30 +0400 Subject: [PATCH 92/99] Updated on 2026-08-14 --- .../repository/DefaultCurrenciesRepository.kt | 5 + .../data/tokens/utils/CustomTokensMerger.kt | 93 ++++++---- .../data/tokens/utils/FoundTokenConverter.kt | 18 -- .../tokens/utils/CustomTokensMergerTest.kt | 165 ++++++++++++++++++ .../tokens/repository/CurrenciesRepository.kt | 5 +- .../repository/MockCurrenciesRepository.kt | 5 + 6 files changed, 234 insertions(+), 57 deletions(-) delete mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/FoundTokenConverter.kt create mode 100644 data/tokens/src/test/kotlin/com/tangem/data/tokens/utils/CustomTokensMergerTest.kt diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index efb58c1d8b..bb4a57959a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -16,6 +16,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.DemoConfig @@ -573,6 +574,10 @@ internal class DefaultCurrenciesRepository( ) } + override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver { + return userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().cardTypesResolver + } + private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> responseCryptoCurrenciesFactory.createCurrencies( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt index 18895b5065..67d189f978 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt @@ -4,6 +4,7 @@ import arrow.atomic.AtomicBoolean import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -15,11 +16,15 @@ import timber.log.Timber /** * Responsible for merging custom tokens into a user's token response. * It handles the logic to update tokens with additional details if necessary. + * + * @property tangemTechApi Tangem Tech API + * @property userTokensSaver user tokens saver + * @property dispatchers dispatchers */ internal class CustomTokensMerger( private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, private val userTokensSaver: UserTokensSaver, + private val dispatchers: CoroutineDispatcherProvider, ) { /** @@ -29,21 +34,17 @@ internal class CustomTokensMerger( * is needed, and if so, updating the token from [TangemTechApi.getCoins] response. It then pushes to the backend * and returns an updated UserTokensResponse. * - * @param userWalletId The identifier for the user's wallet, used when pushing updates. - * @param response The original user tokens response that may need to be updated. - * @return A potentially updated UserTokensResponse, with custom tokens merged if necessary. + * @param userWalletId the identifier for the user's wallet, used when pushing updates + * @param response the original user tokens response that may need to be updated + * + * @return a potentially updated UserTokensResponse, with custom tokens merged if necessary */ suspend fun mergeIfPresented(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse { // use flag to check: we can't compare two token list after merge because Token equals don't include some fields val wasMerged = AtomicBoolean(false) - val mergedTokens = withContext(dispatchers.default) { - response.tokens - .map { token -> - async { mergeIfPresented(token, wasMerged) } - } - .awaitAll() - } + val mergedTokens = mergeTokens(response = response, wasMerged = wasMerged) + val updatedResponse = response.copy(tokens = mergedTokens) // previously here was used compare response.tokens, but it's not working correctly @@ -55,33 +56,48 @@ internal class CustomTokensMerger( return updatedResponse } - private suspend fun mergeIfPresented( - token: UserTokensResponse.Token, + private suspend fun mergeTokens( + response: UserTokensResponse, wasMerged: AtomicBoolean, - ): UserTokensResponse.Token { - if (isCoinOrNonCustomToken(token)) return token + ): List { + return withContext(dispatchers.default) { + response.tokens + .map { token -> + async { + if (isCoinOrNonCustomToken(token)) return@async token - return merge(token, wasMerged) - } - - private suspend fun merge( - customToken: UserTokensResponse.Token, - wasMerged: AtomicBoolean, - ): UserTokensResponse.Token { - val foundToken = fetchToken(customToken) - if (foundToken != null) { - wasMerged.set(true) + mergeToken(customToken = token, wasMerged = wasMerged) + } + } + .awaitAll() } - return foundToken ?: customToken } private fun isCoinOrNonCustomToken(token: UserTokensResponse.Token): Boolean { return token.contractAddress.isNullOrEmpty() || token.id != null } - private suspend fun fetchToken(token: UserTokensResponse.Token): UserTokensResponse.Token? { - val response = withContext(dispatchers.io) { - safeApiCall( + private suspend fun mergeToken( + customToken: UserTokensResponse.Token, + wasMerged: AtomicBoolean, + ): UserTokensResponse.Token { + val foundToken = findToken(token = customToken) + + return if (foundToken != null) { + wasMerged.set(true) + customToken.mergeWith(foundToken) + } else { + customToken + } + } + + /** + * Find a [token] among the cryptocurrencies available for application. + * If result is not null, then the token is available and should not be custom. + */ + private suspend fun findToken(token: UserTokensResponse.Token): CoinsResponse.Coin? { + return withContext(dispatchers.io) { + val response = safeApiCall( call = { tangemTechApi.getCoins( contractAddress = token.contractAddress, @@ -89,19 +105,20 @@ internal class CustomTokensMerger( ).bind() }, onError = { - Timber.w(it, "Unable to fetch token") + Timber.e(it, "Unable to fetch token:\n$token") null }, ) - } - val foundToken = response?.coins?.firstOrNull() ?: return null - return token.copy( - id = foundToken.id, - name = foundToken.name, - symbol = foundToken.symbol.ifEmpty { - token.symbol - }, + response?.coins?.firstOrNull() + } + } + + private fun UserTokensResponse.Token.mergeWith(coin: CoinsResponse.Coin): UserTokensResponse.Token { + return copy( + id = coin.id, + name = coin.name, + symbol = coin.symbol.ifEmpty { symbol }, ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/FoundTokenConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/FoundTokenConverter.kt deleted file mode 100644 index 78ce164eb6..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/FoundTokenConverter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.data.tokens.utils - -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.tokens.model.FoundToken -import com.tangem.utils.converter.Converter - -internal object FoundTokenConverter : Converter { - - override fun convert(value: CoinsResponse.Coin): FoundToken { - return FoundToken( - id = value.id, - name = value.name, - symbol = value.symbol, - contractAddress = requireNotNull(value.networks.first().contractAddress), - decimals = requireNotNull(value.networks.first().decimalCount).intValueExact(), - ) - } -} \ No newline at end of file diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/utils/CustomTokensMergerTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/utils/CustomTokensMergerTest.kt new file mode 100644 index 0000000000..bb4882036a --- /dev/null +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/utils/CustomTokensMergerTest.kt @@ -0,0 +1,165 @@ +package com.tangem.data.tokens.utils + +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class CustomTokensMergerTest { + + private val tangemTechApi: TangemTechApi = mockk() + private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) + private val merger = CustomTokensMerger( + tangemTechApi = tangemTechApi, + userTokensSaver = userTokensSaver, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun resetMocks() { + clearMocks(tangemTechApi, userTokensSaver) + } + + @Test + fun `mergeIfPresented successfully if UserTokensResponse DOESN'T CONTAIN custom tokens`() = runTest { + // Act + val actual = merger.mergeIfPresented(userWalletId = userWalletId, response = defaultUserTokensResponse) + + // Assert + val expected = defaultUserTokensResponse + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(inverse = true) { + tangemTechApi.getCoins(contractAddress = any(), networkIds = any()) + userTokensSaver.push(any(), any()) + } + } + + @Test + fun `mergeIfPresented successfully if UserTokensResponse CONTAINS custom tokens`() = runTest { + // Arrange + val customToken = UserTokensResponse.Token( + id = null, + networkId = "cum", + name = "Matilda Rich", + symbol = "consectetuer", + decimals = 9582, + contractAddress = "contractAddress", + ) + + val userTokensResponse = defaultUserTokensResponse.copy( + tokens = defaultUserTokensResponse.tokens + customToken, + ) + + coEvery { + tangemTechApi.getCoins(contractAddress = customToken.contractAddress, networkIds = customToken.networkId) + } returns ApiResponse.Success(data = createCoinResponse(token = customToken)) + + // Act + val actual = merger.mergeIfPresented(userWalletId = userWalletId, response = userTokensResponse) + + // Assert + val updatedCustomToken = customToken.copy( + id = "NEW_${customToken.id}", + name = "NEW_${customToken.name}", + symbol = "NEW_${customToken.symbol}", + ) + + val expected = defaultUserTokensResponse.copy( + tokens = defaultUserTokensResponse.tokens + updatedCustomToken, + ) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + tangemTechApi.getCoins( + contractAddress = customToken.contractAddress, + networkIds = customToken.networkId, + ) + + userTokensSaver.push(userWalletId = userWalletId, response = expected) + } + } + + @Test + fun `mergeIfPresented successfully if API returns error`() = runTest { + // Arrange + val customToken = UserTokensResponse.Token( + id = null, + networkId = "cum", + name = "Matilda Rich", + symbol = "consectetuer", + decimals = 9582, + contractAddress = "contractAddress", + ) + + val userTokensResponse = defaultUserTokensResponse.copy( + tokens = defaultUserTokensResponse.tokens + customToken, + ) + + @Suppress("UNCHECKED_CAST") + val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException) as ApiResponse + + coEvery { + tangemTechApi.getCoins(contractAddress = customToken.contractAddress, networkIds = customToken.networkId) + } returns apiResponse + + // Act + val actual = merger.mergeIfPresented(userWalletId = userWalletId, response = userTokensResponse) + + // Assert + val expected = userTokensResponse + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { + tangemTechApi.getCoins( + contractAddress = customToken.contractAddress, + networkIds = customToken.networkId, + ) + } + + coVerify(inverse = true) { userTokensSaver.push(userWalletId = any(), response = any()) } + } + + private companion object { + + val userWalletId = UserWalletId("011") + + val defaultUserTokensResponse = UserTokensResponseFactory().createUserTokensResponse( + currencies = MockCryptoCurrencyFactory().ethereumAndStellar, + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + fun createCoinResponse(token: UserTokensResponse.Token): CoinsResponse { + return CoinsResponse( + imageHost = null, + coins = listOf( + CoinsResponse.Coin( + id = "NEW_${token.id}", + name = "NEW_${token.name}", + symbol = "NEW_${token.symbol}", + active = true, + ), + ), + total = 1, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index bb43280673..63bb8891bd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.repository +import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.core.error.DataError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -8,7 +9,6 @@ import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow -import kotlin.jvm.Throws /** * Repository for everything related to the tokens of user wallet @@ -263,4 +263,7 @@ interface CurrenciesRepository { */ @Throws suspend fun syncTokens(userWalletId: UserWalletId) + + @Throws + fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index cbf52f0709..475ea789ec 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -2,6 +2,7 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse +import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.core.error.DataError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -166,4 +167,8 @@ internal class MockCurrenciesRepository( override suspend fun syncTokens(userWalletId: UserWalletId) { return Unit } + + override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver { + error("No-op") + } } \ No newline at end of file From fe1e883dd86db3aa1fd110235bc71f1907bf78c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 22:11:33 +0500 Subject: [PATCH 93/99] Updated on 2026-08-14 --- .../walletconnect/connections/entity/WcAppInfoUM.kt | 2 +- .../walletconnect/connections/model/WcPairModel.kt | 8 +++++++- .../model/transformers/WcAppInfoTransformer.kt | 2 +- .../features/walletconnect/connections/ui/WcAppInfoBS.kt | 9 ++++++--- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt index e222f95e6d..5af3637df7 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -26,7 +26,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent { val appSubtitle: String, val notification: WcAppInfoSecurityNotification?, val walletName: String, - val onWalletClick: () -> Unit, + val onWalletClick: (() -> Unit)?, val networksInfo: WcNetworksInfo, val onNetworksClick: () -> Unit, override val connectButtonConfig: WcPrimaryButtonConfig, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index bf9adcb783..4b6d7923bb 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -20,6 +20,8 @@ import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.isLocked +import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.walletconnect.connections.components.WcPairComponent import com.tangem.features.walletconnect.connections.components.WcSelectNetworksComponent @@ -38,6 +40,8 @@ internal interface WcPairComponentCallback : WcSelectWalletComponent.ModelCallback, WcSelectNetworksComponent.ModelCallback +private const val WC_WALLETS_SELECTOR_MIN_COUNT = 2 + @Stable @ModelScoped internal class WcPairModel @Inject constructor( @@ -97,6 +101,8 @@ internal class WcPairModel @Inject constructor( } is WcPairState.Loading -> appInfoUiState.update { createLoadingState() } is WcPairState.Proposal -> { + val availableWallets = pairState.dAppSession.proposalNetwork.keys + .filter { !it.isLocked && it.isMultiCurrency } sessionProposal = pairState.dAppSession proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWalletFlow.value) additionallyEnabledNetworks = proposalNetwork.available @@ -110,7 +116,7 @@ internal class WcPairModel @Inject constructor( stackNavigation.pushNew( WcAppInfoRoutes.SelectWallet(selectedUserWalletFlow.value.walletId), ) - }, + }.takeIf { availableWallets.size >= WC_WALLETS_SELECTOR_MIN_COUNT }, onNetworksClick = { stackNavigation.pushNew( WcAppInfoRoutes.SelectNetworks( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index 61d9f198d0..9de8083b7c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -15,7 +15,7 @@ internal class WcAppInfoTransformer( private val dAppVerifiedStateConverter: WcDAppVerifiedStateConverter, private val onDismiss: () -> Unit, private val onConnect: (securityStatus: CheckDAppResult) -> Unit, - private val onWalletClick: () -> Unit, + private val onWalletClick: (() -> Unit)?, private val onNetworksClick: () -> Unit, private val userWallet: UserWallet, private val proposalNetwork: WcSessionProposal.ProposalNetwork, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt index 9e2490c0f9..2cbc154342 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt @@ -34,6 +34,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.wrappedList @@ -253,9 +254,11 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier .fillMaxWidth() .padding(TangemTheme.dimens.spacing12) WalletRowItem( - modifier = Modifier - .clickable(onClick = state.onWalletClick) - .then(itemsModifier), + modifier = if (state.onWalletClick != null) { + Modifier.clickableSingle(onClick = state.onWalletClick) + } else { + Modifier + }.then(itemsModifier), walletName = state.walletName, ) HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) From c1a34f25ef054238771ee165287c32e3e4dd51b5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 14:54:57 +0500 Subject: [PATCH 94/99] Updated on 2026-08-14 --- .../connections/model/WcConnectedAppInfoModel.kt | 3 ++- .../model/transformers/WcAppInfoTransformer.kt | 2 +- .../model/transformers/WcAppSubtitleConverter.kt | 12 ++++++++++++ .../WcTransactionAppInfoContentUMConverter.kt | 3 ++- 4 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppSubtitleConverter.kt diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt index 622f3c98f0..d49fa80461 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt @@ -17,6 +17,7 @@ import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfoUM import com.tangem.features.walletconnect.connections.entity.WcNetworkInfoItem import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig +import com.tangem.features.walletconnect.connections.model.transformers.WcAppSubtitleConverter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow @@ -58,7 +59,7 @@ internal class WcConnectedAppInfoModel @Inject constructor( appIcon = session.sdkModel.appMetaData.icons.firstOrNull().orEmpty(), isVerified = session.securityStatus == CheckDAppResult.SAFE, verifiedDAppState = extractVerifiedState(session), - appSubtitle = session.sdkModel.appMetaData.url, + appSubtitle = WcAppSubtitleConverter.convert(session.sdkModel.appMetaData), walletName = session.wallet.name, networks = session.networks .map { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index 9de8083b7c..1c8f2ce99e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -29,7 +29,7 @@ internal class WcAppInfoTransformer( verifiedDAppState = dAppVerifiedStateConverter.convert( dAppSession.securityStatus to dAppSession.dAppMetaData.name, ), - appSubtitle = dAppSession.dAppMetaData.url, + appSubtitle = WcAppSubtitleConverter.convert(dAppSession.dAppMetaData), notification = createNotification(dAppSession.securityStatus), walletName = userWallet.name, onWalletClick = onWalletClick, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppSubtitleConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppSubtitleConverter.kt new file mode 100644 index 0000000000..0ad62857a1 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppSubtitleConverter.kt @@ -0,0 +1,12 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData +import com.tangem.utils.converter.Converter +import java.net.URI + +internal object WcAppSubtitleConverter : Converter { + override fun convert(value: WcAppMetaData): String { + val uri = URI(value.url.trim()) + return uri.host + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt index 05a70969d3..621f543caf 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt @@ -1,6 +1,7 @@ package com.tangem.features.walletconnect.transaction.converter import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.features.walletconnect.connections.model.transformers.WcAppSubtitleConverter import com.tangem.features.walletconnect.connections.model.transformers.WcDAppVerifiedStateConverter import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM import com.tangem.utils.converter.Converter @@ -15,7 +16,7 @@ internal class WcTransactionAppInfoContentUMConverter @Inject constructor() : verifiedState = WcDAppVerifiedStateConverter(onVerifiedClick = value.onShowVerifiedAlert).convert( value.session.securityStatus to value.session.sdkModel.appMetaData.name, ), - appSubtitle = value.session.sdkModel.appMetaData.url, + appSubtitle = WcAppSubtitleConverter.convert(value.session.sdkModel.appMetaData), ) data class Input( From 7624642501b04e3e18df7a5653a8fec237d12d91 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 20:15:19 +0300 Subject: [PATCH 95/99] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 950e7b9476..02dca1e527 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 950e7b9476c0888a0dcf07690214a5e3c6610a62 +Subproject commit 02dca1e527f2695b69a9a1d01fc88880a0661486 From c0390577007de0350304fd36169e87037d50348e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 18:40:50 +0300 Subject: [PATCH 96/99] Updated on 2026-08-14 --- .../data/managetokens/utils/ManagedCryptoCurrencyFactory.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index eb12b523a3..769560c5bf 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -178,7 +178,7 @@ internal class ManagedCryptoCurrencyFactory( derivationStyleProvider = scanResponse?.derivationStyleProvider, canHandleTokens = scanResponse?.let { it.card.canHandleToken(blockchain, it.cardTypesResolver, excludedBlockchains) - } ?: true, + } ?: false, // use card specific check if available ) ?: return null return when { @@ -187,7 +187,8 @@ internal class ManagedCryptoCurrencyFactory( decimals = blockchain.decimals(), isL2Network = l2BlockchainsList.contains(blockchain), ) - network.canHandleTokens -> { + // use general check from blockchain, check availability for card in place of use + blockchain.canHandleTokens() -> { val formattedContractAddress = blockchain.reformatContractAddress(contractAddress) if (formattedContractAddress == null) { Timber.w("Couldn't reformat $contractAddress") From 47ba4043a387428b90ba0464c7c9d636cb8c90e5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Jul 2025 16:22:47 +0700 Subject: [PATCH 97/99] Updated on 2026-08-14 --- .../walletconnect2/di/WalletConnectInteractorModule.kt | 2 ++ .../walletconnect2/domain/WalletConnectInteractor.kt | 10 ++++++++++ .../com/tangem/tap/routing/utils/DeepLinkFactory.kt | 2 ++ .../kotlin/com/tangem/common/routing/DeepLinkRoute.kt | 4 ++++ .../walletconnect/network/ethereum/WcEthNetwork.kt | 6 +----- .../walletconnect/network/solana/WcSolanaNetwork.kt | 7 +------ .../data/walletconnect/pair/DefaultWcPairUseCase.kt | 1 + .../data/walletconnect/pair/WcPairSdkDelegate.kt | 6 ++++-- .../data/walletconnect/utils/WcNamespaceConverter.kt | 5 +++-- .../domain/walletconnect/DefaultWcPairUseCaseTest.kt | 3 ++- 10 files changed, 30 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index e30556434c..6992564847 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -43,6 +43,7 @@ internal object WalletConnectInteractorModule { currenciesRepository: CurrenciesRepository, walletManagersFacade: WalletManagersFacade, userWalletsListManager: UserWalletsListManager, + walletConnectFeatureToggles: WalletConnectFeatureToggles, coroutineDispatcherProvider: CoroutineDispatcherProvider, ): WalletConnectInteractor { return WalletConnectInteractor( @@ -55,6 +56,7 @@ internal object WalletConnectInteractorModule { walletManagersFacade = walletManagersFacade, userWalletsListManager = userWalletsListManager, dispatchers = coroutineDispatcherProvider, + walletConnectFeatureToggles = walletConnectFeatureToggles, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 0487bee8ce..069e6089e9 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -16,6 +16,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.filterNotNull import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper @@ -39,8 +40,10 @@ class WalletConnectInteractor( private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, private val userWalletsListManager: UserWalletsListManager, + private val walletConnectFeatureToggles: WalletConnectFeatureToggles, val blockchainHelper: WcBlockchainHelper, ) { + private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled } private var isWalletConnectReadyForDeepLinks = false @@ -88,6 +91,7 @@ class WalletConnectInteractor( } private fun initWithWallet(userWallet: UserWallet) { + if (isNewWc) return if (userWallet.isMultiCurrency) { Timber.i("WalletConnect: initialize and setup networks for ${userWallet.walletId}") startListeningWc(userWallet.walletId.stringValue, getCardId(userWallet)) @@ -243,6 +247,7 @@ class WalletConnectInteractor( } fun approveSessionProposal(accounts: List) { + if (isNewWc) return Timber.i("Approve session proposal: $accounts") val userNamespaces: Map> = accounts .groupBy { account -> @@ -259,16 +264,19 @@ class WalletConnectInteractor( } fun rejectSessionProposal() { + if (isNewWc) return Timber.i("Reject session proposal") walletConnectRepository.reject() } fun disconnectSession(topic: String) { + if (isNewWc) return Timber.i("Disconnect session: $topic") walletConnectRepository.disconnect(topic) } fun cancelRequest(topic: String, id: Long) { + if (isNewWc) return Timber.i("Cancel request: $topic, $id") walletConnectRepository.cancelRequest(topic, id) } @@ -323,6 +331,7 @@ class WalletConnectInteractor( } suspend fun continueWithRequest(request: WcPreparedRequest) { + if (isNewWc) return val currentRequest = this.currentRequest if (currentRequest == null || request.topic != currentRequest.topic) return @@ -386,6 +395,7 @@ class WalletConnectInteractor( * @param deeplink deeplink to handle */ fun addDeeplink(deeplink: String) { + if (isNewWc) return val deeplinkRegex = Regex(WC_PARAM_REGEX) val matched = deeplinkRegex.findAll(deeplink) val sessionTopic = matched.firstOrNull { it.value.contains(WC_TOPIC_QUERY_NAME) }?.groupValues?.lastOrNull() diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 02afc7ecfe..798ad3a70f 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -108,6 +108,7 @@ internal class DeepLinkFactory @Inject constructor( } } + @Suppress("CyclomaticComplexMethod") private fun handleTangemDeepLinks(deeplinkUri: Uri, coroutineScope: CoroutineScope, isFromOnNewIntent: Boolean) { val queryParams = getQueryParams(deeplinkUri) when (deeplinkUri.host) { @@ -127,6 +128,7 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkRoute.Buy.host -> buyDeepLink.create() DeepLinkRoute.Sell.host -> sellDeepLink.create() DeepLinkRoute.Swap.host -> swapDeepLink.create() + DeepLinkRoute.WalletConnect.host -> walletConnectDeepLink.create(deeplinkUri) else -> { Timber.i( """ diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt index b16393478e..78a4ad85ea 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -51,6 +51,10 @@ sealed class DeepLinkRoute { data object Swap : DeepLinkRoute() { override val host: String = "swap" } + + data object WalletConnect : DeepLinkRoute() { + override val host: String = "wc" + } } enum class DeepLinkScheme(val scheme: String) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index c495325ca3..6bcb815d18 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -110,15 +110,11 @@ internal class WcEthNetwork( } internal class NamespaceConverter( - private val excludedBlockchains: ExcludedBlockchains, + override val excludedBlockchains: ExcludedBlockchains, ) : WcNamespaceConverter { override val namespaceKey: NamespaceKey = NamespaceKey("eip155") - override fun toNetwork(chainId: String, wallet: UserWallet): Network? { - return toNetwork(chainId, wallet, excludedBlockchains) - } - override fun toBlockchain(chainId: CAIP2): Blockchain? { if (chainId.namespace != namespaceKey.key) return null val ethChainId = chainId.reference.toIntOrNull() ?: return null diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt index 2612c60f22..4a310d86d3 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -18,7 +18,6 @@ import com.tangem.domain.walletconnect.model.WcSolanaMethodName import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase -import com.tangem.domain.wallets.models.UserWallet import com.tangem.lib.crypto.UserWalletManager import jakarta.inject.Inject import timber.log.Timber @@ -66,7 +65,7 @@ internal class WcSolanaNetwork( } internal class NamespaceConverter @Inject constructor( - private val excludedBlockchains: ExcludedBlockchains, + override val excludedBlockchains: ExcludedBlockchains, ) : WcNamespaceConverter { override val namespaceKey: NamespaceKey = NamespaceKey("solana") @@ -80,10 +79,6 @@ internal class WcSolanaNetwork( } } - override fun toNetwork(chainId: String, wallet: UserWallet): Network? { - return toNetwork(chainId, wallet, excludedBlockchains) - } - override fun toCAIP2(network: Network): CAIP2? { val blockchain = network.toBlockchain() val chainId = when (blockchain) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 77b7c18e93..6b60b36eca 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -105,6 +105,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( newSession }.onLeft { analytics.send(WcAnalyticEvents.DAppConnectionFailed(it.code)) + sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey) Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}") } emit(WcPairState.Approving.Result(sessionForApprove, either)) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt index 1a72488035..0b83809241 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -13,7 +13,9 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout import kotlin.coroutines.resume +import kotlin.time.Duration.Companion.seconds internal class WcPairSdkDelegate : WcSdkObserver { @@ -26,7 +28,7 @@ internal class WcPairSdkDelegate : WcSdkObserver { .first() val pairCall = async { sdkPair(url) } - val proposal = async { proposalCallback() } + val proposal = async { withTimeout(20.seconds) { proposalCallback() } } pairCall.await().onLeft { proposal.cancel() return@coroutineScope it.left() @@ -42,7 +44,7 @@ internal class WcPairSdkDelegate : WcSdkObserver { .first() val approveCall = async { sdkApprove(sessionApprove) } - val approveCallback = async { approveCallback() } + val approveCallback = async { withTimeout(20.seconds) { approveCallback() } } approveCall.await() .onLeft { approveCallback.cancel() diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt index 258733919d..b1a98f54da 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt @@ -12,6 +12,7 @@ import com.tangem.domain.wallets.models.requireColdWallet internal interface WcNamespaceConverter { val namespaceKey: NamespaceKey + val excludedBlockchains: ExcludedBlockchains fun toBlockchain(chainId: CAIP2): Blockchain? fun toBlockchain(chainId: String): Blockchain? = toCAIP2(chainId)?.let { caip2 -> toBlockchain(caip2) } @@ -19,9 +20,9 @@ internal interface WcNamespaceConverter { fun toCAIP2(network: Network): CAIP2? fun toCAIP2(chainId: String): CAIP2? = CAIP2.fromRaw(chainId) - fun toNetwork(chainId: String, wallet: UserWallet): Network? - fun toNetwork(chainId: String, wallet: UserWallet, excludedBlockchains: ExcludedBlockchains): Network? { + fun toNetwork(chainId: String, wallet: UserWallet): Network? { val blockchain = toBlockchain(chainId) ?: return null + if (blockchain.isTestnet()) return null return NetworkFactory(excludedBlockchains).create( blockchain = blockchain, diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index e493e6b602..47778833c3 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -232,6 +232,7 @@ internal class DefaultWcPairUseCaseTest { val error = WcPairError.ApprovalFailed("error").left() coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.approve(sdkApprove) } returns error + coEvery { sdkDelegate.rejectSession(sdkApprove.proposerPublicKey) } returns Unit coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } val errorResult = WcPairState.Approving.Result(sessionForApprove, error) @@ -250,8 +251,8 @@ internal class DefaultWcPairUseCaseTest { assertEquals(approveLoading, awaitItem()) coVerifyOrder { sdkDelegate.approve(sdkApprove) + sdkDelegate.rejectSession(sdkApprove.proposerPublicKey) } - assertEquals(errorResult, awaitItem()) awaitComplete() } From b89a6690eb10e99a9a838615de8ea6797447c50c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Jul 2025 12:26:25 +0300 Subject: [PATCH 98/99] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../kotlin/com/tangem/common/BaseTestCase.kt | 30 ++++++++++--------- .../com/tangem/common/HiltTestRunner.kt | 4 +-- .../allure/FailedStepScreenshotInterceptor.kt | 23 ++++++++++++++ gradle/dependencies.toml | 6 ++-- 5 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/common/allure/FailedStepScreenshotInterceptor.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c17cd6b366..91ee07995e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -330,6 +330,7 @@ dependencies { androidTestImplementation(deps.test.espresso.intents) androidTestImplementation(deps.test.kaspresso) androidTestImplementation(deps.test.kaspresso.compose) + androidTestImplementation(deps.test.kaspresso.allure) androidTestImplementation(deps.test.compose.junit) androidTestImplementation(deps.test.hamcrest) androidTestImplementation(deps.test.hilt) diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index b5c7601cde..48696ff92e 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -1,31 +1,37 @@ package com.tangem.common import android.Manifest -import android.content.Context -import android.util.Log import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.datastore.dataStoreFile import androidx.test.espresso.intent.Intents -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import com.kaspersky.components.composesupport.config.withComposeSupport +import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor +import com.kaspersky.components.alluresupport.withForcedAllureSupport +import com.kaspersky.components.composesupport.config.addComposeSupport import com.kaspersky.kaspresso.kaspresso.Kaspresso import com.kaspersky.kaspresso.testcases.api.testcase.TestCase +import com.tangem.common.allure.FailedStepScreenshotInterceptor import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.MainActivity import dagger.hilt.android.testing.HiltAndroidRule -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.rules.RuleChain import org.junit.rules.TestRule -import org.junit.runner.RunWith import javax.inject.Inject abstract class BaseTestCase : TestCase( - kaspressoBuilder = Kaspresso.Builder.withComposeSupport() + kaspressoBuilder = Kaspresso.Builder.withForcedAllureSupport( + shouldRecordVideo = false + ).apply { + stepWatcherInterceptors = stepWatcherInterceptors.filter { + it !is ScreenshotStepInterceptor + }.toMutableList() + stepWatcherInterceptors.addAll( + listOf( + FailedStepScreenshotInterceptor(screenshots) + ) + ) + }.addComposeSupport() ) { @Inject @@ -61,8 +67,4 @@ abstract class BaseTestCase : TestCase( Intents.release() } - companion object { - private const val INIT_DELAY = 1000L - } - } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/HiltTestRunner.kt b/app/src/androidTest/kotlin/com/tangem/common/HiltTestRunner.kt index 3f5c02d5a5..ece09d8ca2 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/HiltTestRunner.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/HiltTestRunner.kt @@ -2,10 +2,10 @@ package com.tangem.common import android.app.Application import android.content.Context -import androidx.test.runner.AndroidJUnitRunner +import com.kaspersky.kaspresso.runner.KaspressoRunner import com.tangem.common.di.TangemMockedApplication_Application -class HiltTestRunner : AndroidJUnitRunner() { +class HiltTestRunner : KaspressoRunner() { override fun newApplication( cl: ClassLoader?, diff --git a/app/src/androidTest/kotlin/com/tangem/common/allure/FailedStepScreenshotInterceptor.kt b/app/src/androidTest/kotlin/com/tangem/common/allure/FailedStepScreenshotInterceptor.kt new file mode 100644 index 0000000000..6a4672170f --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/allure/FailedStepScreenshotInterceptor.kt @@ -0,0 +1,23 @@ +package com.tangem.common.allure + +import com.kaspersky.components.alluresupport.files.attachScreenshotToAllureReport +import com.kaspersky.kaspresso.device.screenshots.Screenshots +import com.kaspersky.kaspresso.interceptors.watcher.testcase.StepWatcherInterceptor +import com.kaspersky.kaspresso.testcases.models.info.StepInfo + +class FailedStepScreenshotInterceptor ( + private val screenshots: Screenshots +) : StepWatcherInterceptor { + + override fun interceptAfterWithSuccess(stepInfo: StepInfo) = Unit + + override fun interceptAfterWithError(stepInfo: StepInfo, error: Throwable) { + intercept("${makeTag(stepInfo)}_failure_${error.javaClass.simpleName}") + } + + private fun intercept(tag: String) { + screenshots.takeAndApply(tag) { attachScreenshotToAllureReport() } + } + + private fun makeTag(stepInfo: StepInfo): String = "${stepInfo.testClassName}_step_${stepInfo.ordinal}" +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 92b3c84211..36ba0d9d05 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -107,8 +107,9 @@ junitAndroidExt = "1.1.5" mockk = "1.13.4" turbine = "1.2.0" truth = "1.1.3" -kaspresso = "1.5.4" -kaspresso-compose = "1.5.4" +kaspresso = "1.6.0" +kaspresso-compose = "1.6.0" +kaspresso-allure = "1.6.0" compose-junit = "1.6.2" hamcrest = "2.2" orchestrator = "1.5.1" @@ -209,6 +210,7 @@ test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } test-turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } test-kaspresso = { module = "com.kaspersky.android-components:kaspresso", version.ref = "kaspresso" } test-kaspresso-compose = { module = "com.kaspersky.android-components:kaspresso-compose-support", version.ref = "kaspresso-compose" } +test-kaspresso-allure = { module = "com.kaspersky.android-components:kaspresso-allure-support", version.ref = "kaspresso-allure" } test-compose-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-junit" } test-hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } test-hilt = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } From ad9de30a014764dccfbf586421c28a45581357a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 13:36:38 +0400 Subject: [PATCH 99/99] Updated on 2026-08-14 --- ...faultMultiWalletCryptoCurrenciesFetcher.kt | 131 +++++ ...ultiWalletCryptoCurrenciesFetcherModule.kt | 51 ++ .../repository/DefaultCurrenciesRepository.kt | 2 +- ...tMultiWalletCryptoCurrenciesFetcherTest.kt | 481 ++++++++++++++++++ .../MultiWalletCryptoCurrenciesFetcher.kt | 14 + 5 files changed, 678 insertions(+), 1 deletion(-) create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt create mode 100644 data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesFetcher.kt diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt new file mode 100644 index 0000000000..fcb4cdd145 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt @@ -0,0 +1,131 @@ +package com.tangem.data.tokens + +import arrow.core.Either +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.tokens.utils.CustomTokensMerger +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE +import com.tangem.datasource.api.express.models.request.LeastTokenInfo +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.core.utils.catchOn +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.isMultiCurrency +import com.tangem.domain.wallets.models.requireColdWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber + +/** + * Default implementation of [MultiWalletCryptoCurrenciesFetcher] + * + * @property userWalletsStore [UserWallet]'s store + * @property tangemTechApi Tangem Tech API + * @property userTokensResponseStore store of [UserTokensResponse] + * @property userTokensSaver user tokens saver + * @property cardCryptoCurrencyFactory factory for creating crypto currencies for specified card + * @property expressServiceLoader express service loader + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Suppress("LongParameterList") +internal class DefaultMultiWalletCryptoCurrenciesFetcher( + private val demoConfig: DemoConfig, + private val userWalletsStore: UserWalletsStore, + private val tangemTechApi: TangemTechApi, + private val customTokensMerger: CustomTokensMerger, + private val userTokensResponseStore: UserTokensResponseStore, + private val userTokensSaver: UserTokensSaver, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, + private val expressServiceLoader: ExpressServiceLoader, + private val dispatchers: CoroutineDispatcherProvider, +) : MultiWalletCryptoCurrenciesFetcher { + + private val userTokensResponseFactory = UserTokensResponseFactory() + + override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) { + val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + + if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet") + + val response = if (userWallet is UserWallet.Cold && userWallet.isDemoWalletWithoutSavedTokens()) { + createDefaultUserTokensResponse(scanResponse = userWallet.scanResponse) + } else { + safeApiCall( + call = { + withContext(dispatchers.io) { + tangemTechApi.getUserTokens(userId = userWallet.walletId.stringValue).bind() + } + }, + onError = { + handleFetchTokensError(error = it, userWallet = userWallet.requireColdWallet()) // TODO 11142 + }, + ) + } + + val compatibleUserTokensResponse = response + .let { it.copy(tokens = it.tokens.distinct()) } + .let { customTokensMerger.mergeIfPresented(userWalletId = userWallet.walletId, response = it) } + + userTokensSaver.store(userWalletId = userWallet.walletId, response = compatibleUserTokensResponse) + + fetchExpressAssetsByNetworkIds(userWallet = userWallet, userTokens = compatibleUserTokensResponse) + } + + private suspend fun UserWallet.Cold.isDemoWalletWithoutSavedTokens(): Boolean { + val isDemoCard = demoConfig.isDemoCardId(cardId = cardId) + + return if (isDemoCard) { + val response = userTokensResponseStore.getSyncOrNull(userWalletId = walletId) + + response == null + } else { + false + } + } + + private suspend fun handleFetchTokensError(error: ApiResponseError, userWallet: UserWallet): UserTokensResponse { + val userWalletId = userWallet.walletId + + val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + ?: createDefaultUserTokensResponse(scanResponse = userWallet.requireColdWallet().scanResponse) + + if (error is ApiResponseError.HttpException && error.code == ApiResponseError.HttpException.Code.NOT_FOUND) { + Timber.w(error, "Requested currencies could not be found in the remote store for: $userWalletId") + + userTokensSaver.push(userWalletId, response) + } + + return response + } + + private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) { + val tokens = userTokens.tokens.map { token -> + LeastTokenInfo( + contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, + network = token.networkId, + ) + } + + expressServiceLoader.update(userWallet = userWallet, userTokens = tokens) + } + + private fun createDefaultUserTokensResponse(scanResponse: ScanResponse): UserTokensResponse { + return userTokensResponseFactory.createUserTokensResponse( + currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt new file mode 100644 index 0000000000..93689fc896 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt @@ -0,0 +1,51 @@ +package com.tangem.data.tokens.di + +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesFetcher +import com.tangem.data.tokens.utils.CustomTokensMerger +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class MultiWalletCryptoCurrenciesFetcherModule { + + @Singleton + @Provides + fun provideMultiWalletCryptoCurrenciesFetcher( + tangemTechApi: TangemTechApi, + userWalletsStore: UserWalletsStore, + userTokensResponseStore: UserTokensResponseStore, + userTokensSaver: UserTokensSaver, + cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, + expressServiceLoader: ExpressServiceLoader, + dispatchers: CoroutineDispatcherProvider, + ): MultiWalletCryptoCurrenciesFetcher { + return DefaultMultiWalletCryptoCurrenciesFetcher( + demoConfig = DemoConfig(), + userWalletsStore = userWalletsStore, + tangemTechApi = tangemTechApi, + customTokensMerger = CustomTokensMerger( + tangemTechApi = tangemTechApi, + userTokensSaver = userTokensSaver, + dispatchers = dispatchers, + ), + userTokensResponseStore = userTokensResponseStore, + userTokensSaver = userTokensSaver, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + expressServiceLoader = expressServiceLoader, + dispatchers = dispatchers, + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index bb4a57959a..595c23de02 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -575,7 +575,7 @@ internal class DefaultCurrenciesRepository( } override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver { - return userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().cardTypesResolver + return userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().cardTypesResolver // TODO [REDACTED_TASK_KEY] } private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt new file mode 100644 index 0000000000..f9ee73909a --- /dev/null +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt @@ -0,0 +1,481 @@ +package com.tangem.data.tokens + +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.utils.assertEither +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.tokens.utils.CustomTokensMerger +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE +import com.tangem.datasource.api.express.models.request.LeastTokenInfo +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.isMultiCurrency +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { + + private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + private val userTokensResponseFactory = UserTokensResponseFactory() + + private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val tangemTechApi: TangemTechApi = mockk() + private val customTokensMerger: CustomTokensMerger = mockk() + private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) + private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() + private val expressServiceLoader: ExpressServiceLoader = mockk(relaxUnitFun = true) + + private val fetcher = DefaultMultiWalletCryptoCurrenciesFetcher( + demoConfig = DemoConfig(), + userWalletsStore = userWalletsStore, + tangemTechApi = tangemTechApi, + customTokensMerger = customTokensMerger, + userTokensResponseStore = userTokensResponseStore, + userTokensSaver = userTokensSaver, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + expressServiceLoader = expressServiceLoader, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun resetMocks() { + clearMocks( + userWalletsStore, + tangemTechApi, + userTokensResponseStore, + userTokensSaver, + cardCryptoCurrencyFactory, + expressServiceLoader, + ) + } + + @Test + fun `fetch failure if UserWallet ISN'T MULTI-CURRENCY wallet`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { isMultiCurrency } returns false + } + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + + // Act + val actual = fetcher(params) + + // Assert + val expected = IllegalStateException("${this::class.simpleName} supports only multi-currency wallet").left() + assertEither(actual, expected) + + verifyOrder { userWalletsStore.getSyncStrict(key = params.userWalletId) } + coVerify(inverse = true) { + userTokensResponseStore.getSyncOrNull(any()) + } + } + + @Test + fun `fetch successfully if CARD IS DEMO and STORED TOKENS ARE EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "AC01000000041225" + } + + val defaultCoins = listOf( + cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), + cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), + ) + + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + tokens = listOf( + userTokensResponseFactory.createResponseToken(defaultCoins.first()), + userTokensResponseFactory.createResponseToken(defaultCoins.last()), + ), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null + every { + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + } returns defaultCoins + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + } returns userTokensResponse + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = mockUserWallet.scanResponse) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens()) + } + } + + @Test + fun `fetch successfully if CARD IS DEMO and STORED TOKENS AREN'T EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "AC01000000041225" + } + + val apiResponse = ApiResponse.Success( + data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns defaultResponse + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) + } returns apiResponse.data + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) + userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens()) + } + + coVerify(inverse = true) { + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + } + } + + @Test + fun `fetch successfully if CARD ISN'T DEMO`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "cardID" + } + + val apiResponse = ApiResponse.Success( + data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) + } returns apiResponse.data + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) + userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens()) + } + + coVerify(inverse = true) { + userTokensResponseStore.getSyncOrNull(userWalletId = any()) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + } + } + + @Test + fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "cardID" + } + + @Suppress("UNCHECKED_CAST") + val apiResponse = ApiResponse.Error( + cause = ApiResponseError.TimeoutException, + ) as ApiResponse + + val defaultCoins = listOf( + cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), + cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), + ) + + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + tokens = listOf( + userTokensResponseFactory.createResponseToken(defaultCoins.first()), + userTokensResponseFactory.createResponseToken(defaultCoins.last()), + ), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null + coEvery { + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + } returns defaultCoins + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + } returns userTokensResponse + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens()) + } + + coVerify(inverse = true) { + userTokensSaver.push(userWalletId = any(), response = any()) + } + } + + @Test + fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "cardID" + } + + @Suppress("UNCHECKED_CAST") + val apiResponse = ApiResponse.Error( + cause = ApiResponseError.TimeoutException, + ) as ApiResponse + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) + } returns defaultResponse + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) + userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens()) + } + + coVerify(inverse = true) { + userTokensSaver.push(userWalletId = any(), response = any()) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + } + } + + @Test + fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "cardID" + } + + @Suppress("UNCHECKED_CAST") + val apiResponse = ApiResponse.Error( + cause = ApiResponseError.HttpException( + code = ApiResponseError.HttpException.Code.NOT_FOUND, + message = null, + errorBody = null, + ), + ) as ApiResponse + + val defaultCoins = listOf( + cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), + cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), + ) + + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + tokens = listOf( + userTokensResponseFactory.createResponseToken(defaultCoins.first()), + userTokensResponseFactory.createResponseToken(defaultCoins.last()), + ), + ) + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null + coEvery { + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + } returns defaultCoins + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + } returns userTokensResponse + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + userTokensSaver.push(userWalletId = params.userWalletId, response = userTokensResponse) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) + userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens()) + } + } + + @Test + fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest { + // Arrange + val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) + + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + every { cardId } returns "cardID" + } + + @Suppress("UNCHECKED_CAST") + val apiResponse = ApiResponse.Error( + cause = ApiResponseError.HttpException( + code = ApiResponseError.HttpException.Code.NOT_FOUND, + message = null, + errorBody = null, + ), + ) as ApiResponse + + every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse + coEvery { + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) + } returns defaultResponse + + // Act + val actual = fetcher(params) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = params.userWalletId) + tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) + userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) + userTokensSaver.push(userWalletId = params.userWalletId, response = defaultResponse) + customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) + userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse) + expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens()) + } + + coVerify(inverse = true) { + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + } + } + + private companion object { + val userWalletId = UserWalletId("011") + + val defaultResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, + tokens = listOf( + UserTokensResponse.Token( + id = null, + networkId = "bitcoin", + derivationPath = null, + name = "Bitcoin", + symbol = "BTC", + decimals = 8, + contractAddress = null, + addresses = listOf(), + ), + ), + ) + + fun UserTokensResponse.toLeastTokens(): List { + return tokens.map { token -> + LeastTokenInfo( + contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, + network = token.networkId, + ) + } + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesFetcher.kt new file mode 100644 index 0000000000..1da20f04b2 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/MultiWalletCryptoCurrenciesFetcher.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Fetcher of crypto currencies for a multi-currency wallet with [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface MultiWalletCryptoCurrenciesFetcher : FlowFetcher { + + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file