From 7ea613144d7af1cee0d4990670e4774daf2c874f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Jun 2025 15:50:44 +0500 Subject: [PATCH 01/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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 ebb5b5830ef8123e5263c90975e1e45b082b24a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Jun 2025 18:56:25 +0900 Subject: [PATCH 62/65] 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 63/65] 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 64/65] 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 65/65] 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"