From b83d49cdea4b7a24c343c8bcb0918af7f7c1919d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 19:00:35 +0500 Subject: [PATCH 01/72] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 12 ++- .../com/tangem/common/routing/AppRoute.kt | 1 + .../currency/icon/CurrencyIconTopBadge.kt | 4 +- .../nft/component/NFTDetailsBlockComponent.kt | 17 ++++ .../nft/component/NFTDetailsComponent.kt | 1 + .../transformer/UpdateDataStateTransformer.kt | 8 +- .../collections/model/NFTCollectionsModel.kt | 3 +- .../nft/collections/ui/NFTCollection.kt | 55 ++---------- .../tangem/features/nft/common/ui/NFTLogo.kt | 60 +++++++++++++ .../block/DefaultNFTDetailsBlockComponent.kt | 36 ++++++++ .../nft/details/block/ui/NFTDetailsBlock.kt | 84 +++++++++++++++++++ .../nft/details/model/NFTDetailsModel.kt | 10 ++- .../features/nft/{ => di}/NFTFeatureModule.kt | 12 ++- 13 files changed, 244 insertions(+), 59 deletions(-) create mode 100644 features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTLogo.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt rename features/nft/impl/src/main/kotlin/com/tangem/features/nft/{ => di}/NFTFeatureModule.kt (82%) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 3c0ac636bb..31026d4c57 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -420,7 +420,11 @@ internal class ChildFactory @Inject constructor( is AppRoute.NFTDetails -> createComponentChild( context = context, - params = NFTDetailsComponent.Params(userWalletId = route.userWalletId, nftAsset = route.nftAsset), + params = NFTDetailsComponent.Params( + userWalletId = route.userWalletId, + nftAsset = route.nftAsset, + nftCollectionName = route.collectionName, + ), componentFactory = nftDetailsComponentFactory, ) is AppRoute.OnboardingNote, @@ -772,7 +776,11 @@ internal class ChildFactory @Inject constructor( is AppRoute.NFTDetails -> route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = NFTDetailsComponent.Params(userWalletId = route.userWalletId, nftAsset = route.nftAsset), + params = NFTDetailsComponent.Params( + userWalletId = route.userWalletId, + nftAsset = route.nftAsset, + nftCollectionName = route.collectionName, + ), componentFactory = nftDetailsComponentFactory, ) } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index cc4bda0a08..2c697c52b9 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -294,5 +294,6 @@ sealed class AppRoute(val path: String) : Route { data class NFTDetails( val userWalletId: UserWalletId, val nftAsset: NFTAsset, + val collectionName: String, ) : AppRoute(path = "/nft_details/${userWalletId.stringValue}/${nftAsset.collectionId}/${nftAsset.id.stringValue}") } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt index 03057d495d..f7020ce9e4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import com.tangem.core.ui.res.TangemTheme @@ -19,12 +20,13 @@ fun CurrencyIconTopBadge( alpha: Float, colorFilter: ColorFilter?, modifier: Modifier = Modifier, + background: Color = TangemTheme.colors.background.primary, ) { Box( modifier = modifier .size(TangemTheme.dimens.size18) .background( - color = TangemTheme.colors.background.primary, + color = background, shape = CircleShape, ), ) { diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt new file mode 100644 index 0000000000..676f626597 --- /dev/null +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt @@ -0,0 +1,17 @@ +package com.tangem.features.nft.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.wallets.models.UserWalletId + +interface NFTDetailsBlockComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + val nftAsset: NFTAsset, + val nftCollectionName: String, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt index 0f43e3a58f..2848de0dd5 100644 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt @@ -10,6 +10,7 @@ interface NFTDetailsComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, val nftAsset: NFTAsset, + val nftCollectionName: String, ) interface Factory : ComponentFactory diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt index e5fe0846c9..c5eec23071 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt @@ -20,7 +20,7 @@ internal class UpdateDataStateTransformer( private val onRetryClick: () -> Unit, private val onExpandCollectionClick: (NFTCollection) -> Unit, private val onRetryAssetsClick: (NFTCollection) -> Unit, - private val onAssetClick: (NFTAsset) -> Unit, + private val onAssetClick: (NFTAsset, String) -> Unit, private val initialSearchBarFactory: () -> SearchBarUM, ) : Transformer { @@ -105,12 +105,12 @@ internal class UpdateDataStateTransformer( is NFTCollection.Assets.Value -> NFTCollectionAssetsListUM.Content( items = assets .items - .map { it.transform() } + .map { it.transform(name.orEmpty()) } .toPersistentList(), ) } - private fun NFTAsset.transform(): NFTCollectionAssetUM = NFTCollectionAssetUM( + private fun NFTAsset.transform(collectionName: String): NFTCollectionAssetUM = NFTCollectionAssetUM( id = id.toString(), name = name.orEmpty(), imageUrl = media?.url, @@ -121,7 +121,7 @@ internal class UpdateDataStateTransformer( is NFTSalePrice.Value -> NFTSalePriceUM.Content(salePrice.value.toString()) }, onItemClick = { - onAssetClick(this) + onAssetClick(this, collectionName) }, ) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt index 21618350db..b8b89b6d9b 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -115,11 +115,12 @@ internal class NFTCollectionsModel @Inject constructor( // TODO refresh all } - private fun onAssetClick(asset: NFTAsset) { + private fun onAssetClick(asset: NFTAsset, collectionName: String) { router.push( AppRoute.NFTDetails( userWalletId = params.userWalletId, nftAsset = asset, + collectionName = collectionName, ), ) } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt index ffaf0c94b1..e89b5bbe41 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt @@ -2,7 +2,6 @@ package com.tangem.features.nft.collections.ui import android.content.res.Configuration import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -11,25 +10,19 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource 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.datasource.CollectionPreviewParameterProvider -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.currency.icon.CurrencyIconTopBadge 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.features.nft.collections.entity.NFTCollectionAssetsListUM import com.tangem.features.nft.collections.entity.NFTCollectionUM +import com.tangem.features.nft.common.ui.NFTLogo import com.tangem.features.nft.impl.R import kotlinx.collections.immutable.persistentListOf @@ -52,7 +45,11 @@ internal fun NFTCollection(state: NFTCollectionUM, modifier: Modifier = Modifier ), verticalAlignment = Alignment.CenterVertically, ) { - Logo(state) + NFTLogo( + imageUrl = state.logoUrl, + networkIconId = state.networkIconId, + background = TangemTheme.colors.background.primary, + ) Text(state) @@ -81,46 +78,6 @@ internal fun NFTCollection(state: NFTCollectionUM, modifier: Modifier = Modifier } } -@Composable -private fun Logo(state: NFTCollectionUM) { - val networkBadgeOffset = TangemTheme.dimens.spacing6 - - Box( - modifier = Modifier, - ) { - SubcomposeAsyncImage( - modifier = Modifier - .align(Alignment.CenterStart) - .size(TangemTheme.dimens.size36) - .clip(TangemTheme.shapes.roundedCorners8), - model = ImageRequest.Builder(LocalContext.current) - .data(state.logoUrl) - .crossfade(true) - .build(), - loading = { - RectangleShimmer(radius = TangemTheme.dimens.radius8) - }, - error = { - Box( - modifier = Modifier - .clip(shape = TangemTheme.shapes.roundedCorners8) - .background(TangemTheme.colors.field.primary), - ) - }, - contentScale = ContentScale.Crop, - contentDescription = null, - ) - CurrencyIconTopBadge( - modifier = Modifier - .offset(x = networkBadgeOffset, y = -networkBadgeOffset) - .align(Alignment.TopEnd), - iconResId = state.networkIconId, - alpha = 1f, - colorFilter = null, - ) - } -} - @Composable private fun RowScope.Text(state: NFTCollectionUM) { Column( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTLogo.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTLogo.kt new file mode 100644 index 0000000000..db5f6d01ca --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTLogo.kt @@ -0,0 +1,60 @@ +package com.tangem.features.nft.common.ui + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.currency.icon.CurrencyIconTopBadge +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun NFTLogo(imageUrl: String?, @DrawableRes networkIconId: Int, background: Color = Color.Transparent) { + val networkBadgeOffset = TangemTheme.dimens.spacing6 + + Box( + modifier = Modifier, + ) { + SubcomposeAsyncImage( + modifier = Modifier + .align(Alignment.CenterStart) + .size(TangemTheme.dimens.size36) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .crossfade(true) + .build(), + loading = { + RectangleShimmer(radius = TangemTheme.dimens.radius8) + }, + error = { + Box( + modifier = Modifier + .clip(shape = TangemTheme.shapes.roundedCorners8) + .background(TangemTheme.colors.field.primary), + ) + }, + contentScale = ContentScale.Crop, + contentDescription = null, + ) + CurrencyIconTopBadge( + modifier = Modifier + .offset(x = networkBadgeOffset, y = -networkBadgeOffset) + .align(Alignment.TopEnd), + iconResId = networkIconId, + alpha = 1f, + colorFilter = null, + background = background, + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt new file mode 100644 index 0000000000..1c20de2872 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt @@ -0,0 +1,36 @@ +package com.tangem.features.nft.details.block + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.nft.details.block.ui.NFTDetailsBlock +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class DefaultNFTDetailsBlockComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: NFTDetailsBlockComponent.Params, +) : NFTDetailsBlockComponent, AppComponentContext by context { + + @Composable + override fun Content(modifier: Modifier) { + NFTDetailsBlock( + assetName = stringReference(params.nftAsset.name.orEmpty()), + collectionName = stringReference(params.nftCollectionName), + assetImage = params.nftAsset.media?.url, + networkIconRes = getActiveIconRes(params.nftAsset.network.id.value), + ) + } + + @AssistedFactory + interface Factory : NFTDetailsBlockComponent.Factory { + override fun create( + context: AppComponentContext, + params: NFTDetailsBlockComponent.Params, + ): DefaultNFTDetailsBlockComponent + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt new file mode 100644 index 0000000000..6336d2dbb2 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt @@ -0,0 +1,84 @@ +package com.tangem.features.nft.details.block.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +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.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.common.ui.NFTLogo +import com.tangem.features.nft.impl.R + +@Composable +internal fun NFTDetailsBlock( + assetName: TextReference, + collectionName: TextReference, + assetImage: String?, + networkIconRes: Int, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.action) + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = "NFT Asset", + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.secondary, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + NFTLogo( + assetImage, + networkIconRes, + background = TangemTheme.colors.background.action, + ) + + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = assetName.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = collectionName.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 NFTDetailsBlock_Preview() { + TangemThemePreview { + NFTDetailsBlock( + assetName = stringReference("NFT Asset Name"), + collectionName = stringReference("NFT Collection"), + assetImage = null, + networkIconRes = R.drawable.img_polygon_22, + ) + } +} +// endregion \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index e421efcde8..77fc4b7e96 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -9,7 +9,8 @@ import com.tangem.features.nft.details.entity.NFTAssetUM import com.tangem.features.nft.details.entity.NFTDetailsUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject @ModelScoped @@ -56,6 +57,13 @@ internal class NFTDetailsModel @Inject constructor( private fun onSendClick() { // TODO implement + // router.push( + // AppRoute.NFTSend( + // userWalletId = params.userWalletId, + // nftAsset = params.nftAsset, + // nftCollectionName = params.nftCollectionName, + // ), + // ) } private fun navigateBack() { diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt similarity index 82% rename from features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt rename to features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt index 75f083f217..0d8889e2e8 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt @@ -1,13 +1,17 @@ -package com.tangem.features.nft +package com.tangem.features.nft.di import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.decompose.model.Model +import com.tangem.features.nft.DefaultNFTFeatureToggles +import com.tangem.features.nft.NFTFeatureToggles import com.tangem.features.nft.collections.DefaultNFTCollectionsComponent import com.tangem.features.nft.collections.model.NFTCollectionsModel import com.tangem.features.nft.component.NFTCollectionsComponent +import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.nft.component.NFTDetailsComponent import com.tangem.features.nft.component.NFTReceiveComponent import com.tangem.features.nft.details.DefaultNFTDetailsComponent +import com.tangem.features.nft.details.block.DefaultNFTDetailsBlockComponent import com.tangem.features.nft.details.model.NFTDetailsModel import com.tangem.features.nft.receive.DefaultNFTReceiveComponent import com.tangem.features.nft.receive.model.NFTReceiveModel @@ -62,4 +66,10 @@ internal interface NFTFeatureModuleBinds { @IntoMap @ClassKey(NFTDetailsModel::class) fun bindNFTDetailsModel(model: NFTDetailsModel): Model + + @Binds + @Singleton + fun bindNFTDetailsBlockComponentFactory( + impl: DefaultNFTDetailsBlockComponent.Factory, + ): NFTDetailsBlockComponent.Factory } \ No newline at end of file From c2d9984101d2d043656b9ad0289e98f48b65eb5e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 19:19:44 +0500 Subject: [PATCH 02/72] Updated on 2026-08-14 --- .../ui/components/atoms/text/ReadMoreText.kt | 485 ++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/ReadMoreText.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/ReadMoreText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/ReadMoreText.kt new file mode 100644 index 0000000000..4f071c2b9d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/ReadMoreText.kt @@ -0,0 +1,485 @@ +package com.tangem.core.ui.components.atoms.text + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp + +/** + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +private const val READ_MORE_TAG = "read_more" +private const val READ_LESS_TAG = "read_less" + +/** + * Basic element that displays text with read more. + * + * @param text The text to be displayed. + * @param expanded whether this text is expanded or collapsed. + * @param modifier [Modifier] to apply to this layout node. + * @param onExpandRequested called when this text is clicked. If `null`, then this text will not be + * interactable, unless something else handles its input events and updates its state. + * @param contentPadding a padding around the text. + * @param style Style configuration for the text such as color, font, line height etc. + * @param onTextLayout Callback that is executed when a new text layout is calculated. A + * [TextLayoutResult] object that callback provides contains paragraph information, size of the + * text, baselines and other details. The callback can be used to add additional decoration or + * functionality to the text. For example, to draw selection around the text. + * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the + * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false, + * [readMoreOverflow] and TextAlign may have unexpected effects. + * @param readMoreText The read more text to be displayed in the collapsed state. + * @param readMoreMaxLines An optional maximum number of lines for the text to span, wrapping if + * necessary. If the text exceeds the given number of lines, it will be truncated according to + * [readMoreOverflow]. If it is not null, then it must be greater than zero. + * @param readMoreOverflow How visual overflow should be handled in the collapsed state. + * @param readMoreStyle Style configuration for the read more text such as color, font, line height + * etc. + * @param readLessText The read less text to be displayed in the expanded state. + * @param readLessStyle Style configuration for the read less text such as color, font, line height + * etc. + * @param toggleArea A clickable area of text to toggle. + */ +@Composable +fun ReadMoreText( + text: String, + expanded: Boolean, + modifier: Modifier = Modifier, + onExpandRequested: ((Boolean) -> Unit)? = null, + contentPadding: PaddingValues = PaddingValues(0.dp), + style: TextStyle = TextStyle.Default, + onTextLayout: (TextLayoutResult) -> Unit = {}, + softWrap: Boolean = true, + readMoreText: String = "", + readMoreMaxLines: Int = 2, + readMoreOverflow: ReadMoreTextOverflow = ReadMoreTextOverflow.Ellipsis, + readMoreStyle: SpanStyle = style.toSpanStyle(), + readLessText: String = "", + readLessStyle: SpanStyle = readMoreStyle, + toggleArea: ToggleArea = ToggleArea.All, +) { + ReadMoreTextInternal( + text = AnnotatedString(text), + expanded = expanded, + modifier = modifier, + onExpandRequested = onExpandRequested, + contentPadding = contentPadding, + style = style, + onTextLayout = onTextLayout, + softWrap = softWrap, + readMoreText = readMoreText, + readMoreMaxLines = readMoreMaxLines, + readMoreOverflow = readMoreOverflow, + readMoreStyle = readMoreStyle, + readLessText = readLessText, + readLessStyle = readLessStyle, + toggleArea = toggleArea, + ) +} + +/** + * Basic element that displays text with read more. + * + * @param text The text to be displayed. + * @param expanded whether this text is expanded or collapsed. + * @param modifier [Modifier] to apply to this layout node. + * @param onExpandRequested called when this text is clicked. If `null`, then this text will not be + * interactable, unless something else handles its input events and updates its state. + * @param contentPadding a padding around the text. + * @param style Style configuration for the text such as color, font, line height etc. + * @param onTextLayout Callback that is executed when a new text layout is calculated. A + * [TextLayoutResult] object that callback provides contains paragraph information, size of the + * text, baselines and other details. The callback can be used to add additional decoration or + * functionality to the text. For example, to draw selection around the text. + * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the + * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false, + * [readMoreOverflow] and TextAlign may have unexpected effects. + * @param readMoreText The read more text to be displayed in the collapsed state. + * @param readMoreMaxLines An optional maximum number of lines for the text to span, wrapping if + * necessary. If the text exceeds the given number of lines, it will be truncated according to + * [readMoreOverflow]. If it is not null, then it must be greater than zero. + * @param readMoreOverflow How visual overflow should be handled in the collapsed state. + * @param readMoreStyle Style configuration for the read more text such as color, font, line height + * etc. + * @param readLessText The read less text to be displayed in the expanded state. + * @param readLessStyle Style configuration for the read less text such as color, font, line height + * etc. + * @param toggleArea A clickable area of text to toggle. + */ +@Composable +fun ReadMoreText( + text: AnnotatedString, + expanded: Boolean, + modifier: Modifier = Modifier, + onExpandRequested: ((Boolean) -> Unit)? = null, + contentPadding: PaddingValues = PaddingValues(0.dp), + style: TextStyle = TextStyle.Default, + onTextLayout: (TextLayoutResult) -> Unit = {}, + softWrap: Boolean = true, + readMoreText: String = "", + readMoreMaxLines: Int = 2, + readMoreOverflow: ReadMoreTextOverflow = ReadMoreTextOverflow.Ellipsis, + readMoreStyle: SpanStyle = style.toSpanStyle(), + readLessText: String = "", + readLessStyle: SpanStyle = readMoreStyle, + toggleArea: ToggleArea = ToggleArea.All, +) { + ReadMoreTextInternal( + text = text, + expanded = expanded, + modifier = modifier, + onExpandRequested = onExpandRequested, + contentPadding = contentPadding, + style = style, + onTextLayout = onTextLayout, + softWrap = softWrap, + readMoreText = readMoreText, + readMoreMaxLines = readMoreMaxLines, + readMoreOverflow = readMoreOverflow, + readMoreStyle = readMoreStyle, + readLessText = readLessText, + readLessStyle = readLessStyle, + toggleArea = toggleArea, + ) +} + +@Suppress("LongMethod", "LongParameterList") +@Composable +private fun ReadMoreTextInternal( + text: AnnotatedString, + expanded: Boolean, + modifier: Modifier = Modifier, + onExpandRequested: ((Boolean) -> Unit)? = null, + contentPadding: PaddingValues = PaddingValues(0.dp), + style: TextStyle = TextStyle.Default, + onTextLayout: (TextLayoutResult) -> Unit = {}, + softWrap: Boolean = true, + readMoreText: String = "", + readMoreMaxLines: Int = 2, + readMoreOverflow: ReadMoreTextOverflow = ReadMoreTextOverflow.Ellipsis, + readMoreStyle: SpanStyle = style.toSpanStyle(), + readLessText: String = "", + readLessStyle: SpanStyle = readMoreStyle, + toggleArea: ToggleArea = ToggleArea.All, +) { + require(readMoreMaxLines > 0) { "readMoreMaxLines should be greater than 0" } + + val overflowText: String = remember(readMoreOverflow) { + buildString { + when (readMoreOverflow) { + ReadMoreTextOverflow.Clip -> { + } + ReadMoreTextOverflow.Ellipsis -> { + append(Typography.ellipsis) + } + } + if (readMoreText.isNotEmpty()) { + append(Typography.nbsp) + } + } + } + val readMoreTextWithStyle: AnnotatedString = remember(readMoreText, readMoreStyle) { + buildAnnotatedString { + if (readMoreText.isNotEmpty()) { + withStyle(readMoreStyle) { + append(readMoreText.replace(' ', Typography.nbsp)) + } + } + } + } + val readLessTextWithStyle: AnnotatedString = remember(readLessText, readLessStyle) { + buildAnnotatedString { + if (readLessText.isNotEmpty()) { + withStyle(readLessStyle) { + append(readLessText) + } + } + } + } + + val textMeasurer = rememberTextMeasurer() + val state = remember { ReadMoreState() } + + val currentText = buildAnnotatedString { + if (expanded) { + append(text) + if (readLessTextWithStyle.isNotEmpty()) { + append(' ') + if (toggleArea == ToggleArea.More) { + withLink( + LinkAnnotation.Clickable(tag = READ_LESS_TAG) { + onExpandRequested?.invoke(false) + }, + ) { + append(readLessTextWithStyle) + } + } else { + append(readLessTextWithStyle) + } + } + } else { + val collapsedText = state.collapsedText + if (collapsedText.isNotEmpty()) { + append(collapsedText) + append(overflowText) + + if (toggleArea == ToggleArea.More) { + withLink( + LinkAnnotation.Clickable(tag = READ_MORE_TAG) { + onExpandRequested?.invoke(true) + }, + ) { + append(readMoreTextWithStyle) + } + } else { + append(readMoreTextWithStyle) + } + } else { + append(text) + } + } + } + val toggleableModifier = if (onExpandRequested != null && toggleArea == ToggleArea.All) { + Modifier.clickable( + enabled = state.isCollapsible, + onClick = { onExpandRequested(!expanded) }, + ) + } else { + Modifier + } + BoxWithConstraints( + modifier = modifier + .then(toggleableModifier) + .padding(contentPadding), + ) { + BasicText( + text = currentText, + modifier = Modifier, + style = style, + onTextLayout = onTextLayout, + overflow = TextOverflow.Ellipsis, + softWrap = softWrap, + maxLines = if (expanded) Int.MAX_VALUE else readMoreMaxLines, + ) + + val constraints = Constraints(maxWidth = constraints.maxWidth) + LaunchedEffect( + textMeasurer, + constraints, + overflowText, + readMoreTextWithStyle, + style, + readMoreStyle, + text, + readMoreMaxLines, + softWrap, + ) { + state.applyCollapsedText( + textMeasurer = textMeasurer, + constraints = constraints, + overflowText = overflowText, + readMoreTextWithStyle = readMoreTextWithStyle, + style = style, + readMoreStyle = readMoreStyle, + text = text, + readMoreMaxLines = readMoreMaxLines, + softWrap = softWrap, + ) + } + } +} + +@Stable +private class ReadMoreState { + private var _collapsedText: AnnotatedString by mutableStateOf(AnnotatedString("")) + + var collapsedText: AnnotatedString + get() = _collapsedText + internal set(value) { + if (value != _collapsedText) { + _collapsedText = value + } + } + + val isCollapsible: Boolean + get() = collapsedText.isNotEmpty() + + @Suppress("LongParameterList") + fun applyCollapsedText( + textMeasurer: TextMeasurer, + constraints: Constraints, + overflowText: String, + readMoreTextWithStyle: AnnotatedString, + style: TextStyle, + readMoreStyle: SpanStyle, + text: AnnotatedString, + readMoreMaxLines: Int, + softWrap: Boolean, + ) { + val overflowTextWidth = if (overflowText.isNotEmpty()) { + textMeasurer.measure( + text = overflowText, + style = style, + ).size.width + } else { + 0 + } + val readMoreTextWidth = if (readMoreTextWithStyle.isNotEmpty()) { + textMeasurer.measure( + text = readMoreTextWithStyle, + style = style.merge(readMoreStyle), + ).size.width + } else { + 0 + } + val textLayout = textMeasurer.measure( + text = text, + style = style, + maxLines = readMoreMaxLines, + overflow = TextOverflow.Clip, + softWrap = softWrap, + constraints = constraints, + ) + + val clipTextCount = textLayout.getLineEnd(lineIndex = textLayout.lineCount - 1) + val isLineClipped = text.count() > clipTextCount + if (isLineClipped) { + val countUntilMaxLine = + textLayout.getLineEnd(readMoreMaxLines - 1, visibleEnd = true) + + val decorationWidth = overflowTextWidth + readMoreTextWidth + val replaceCount = text + .substringOf(textLayout, line = readMoreMaxLines) + .calculateReplaceCountToBeSingleLineWith( + maximumTextWidth = constraints.maxWidth - decorationWidth, + measureTextWidth = { subText -> + textMeasurer.measure( + text = subText, + style = style, + softWrap = softWrap, + ).size.width + }, + ) + collapsedText = text.subSequence(0, countUntilMaxLine - replaceCount) + } else { + collapsedText = AnnotatedString("") + } + } + + private fun AnnotatedString.substringOf(layout: TextLayoutResult, line: Int): AnnotatedString { + val lastLineStartIndex = layout.getLineStart(line - 1) + val lastLineEndIndex = layout.getLineEnd(line - 1, visibleEnd = true) + return subSequence(lastLineStartIndex, lastLineEndIndex) + } + + private inline fun AnnotatedString.calculateReplaceCountToBeSingleLineWith( + maximumTextWidth: Int, + measureTextWidth: (subText: AnnotatedString) -> Int, + ): Int { + var replacedTextWidth: Int + var replacedCount = -1 + do { + replacedCount++ + replacedTextWidth = measureTextWidth( + subSequence(0, this.length - replacedCount), + ) + } while (replacedCount < this.length && replacedTextWidth >= maximumTextWidth) + + val lastVisibleChar: Char? = this.getOrNull(this.length - replacedCount - 1) + val firstOverflowChar: Char? = this.getOrNull(this.length - replacedCount) + if (lastVisibleChar?.isSurrogate() == true && firstOverflowChar?.isHighSurrogate() == false) { + val subText = subSequence(0, this.length - replacedCount) + if (subText.isNotEmpty()) { + return length - subText.indexOfLast { it.isHighSurrogate() } + } + } + return replacedCount + } +} + +@JvmInline +value class ToggleArea private constructor(internal val value: Int) { + + override fun toString(): String { + return when (this) { + All -> "All" + More -> "More" + else -> "Invalid" + } + } + + companion object { + /** + * All area of the text is clickable to toggle. + */ + @Stable + val All: ToggleArea = ToggleArea(1) + + /** + * 'More' and 'Less' area of the text is clickable to toggle. + */ + @Stable + val More: ToggleArea = ToggleArea(2) + } +} + +@JvmInline +value class ReadMoreTextOverflow private constructor(internal val value: Int) { + + override fun toString(): String { + return when (this) { + Clip -> "Clip" + Ellipsis -> "Ellipsis" + else -> "Invalid" + } + } + + companion object { + /** + * Clip the overflowing text to fix its container. + */ + @Stable + val Clip: ReadMoreTextOverflow = ReadMoreTextOverflow(1) + + /** + * Use an ellipsis to indicate that the text has overflowed. + */ + @Stable + val Ellipsis: ReadMoreTextOverflow = ReadMoreTextOverflow(2) + } +} \ No newline at end of file From 6c8285847fbeccf8ea0da9e272ca760008aac010 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 19:31:21 +0500 Subject: [PATCH 03/72] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 24 +++ .../com/tangem/common/routing/AppRoute.kt | 7 + features/send-v2/api/build.gradle.kts | 1 + .../features/send/v2/api/NFTSendComponent.kt | 17 +++ .../features/send/v2/di/SendFeatureModule.kt | 6 + .../features/send/v2/di/SendModelModule.kt | 12 -- .../send/v2/send/di/SendModelModule.kt | 26 ++++ .../v2/sendnft/DefaultNFTSendComponent.kt | 89 +++++++++++ .../send/v2/sendnft/di/NFTSendModelModule.kt | 20 +++ .../send/v2/sendnft/model/NFTSendModel.kt | 144 ++++++++++++++++++ .../send/v2/sendnft/ui/state/NFTSendUM.kt | 13 ++ 11 files changed, 347 insertions(+), 12 deletions(-) create mode 100644 features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/NFTSendComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/SendModelModule.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 3c0ac636bb..a8a9ba7d66 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -19,6 +19,7 @@ import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.PushNotificationsComponent import com.tangem.features.send.api.SendComponent +import com.tangem.features.send.v2.api.NFTSendComponent import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.staking.api.StakingComponent import com.tangem.features.swap.SwapComponent @@ -86,6 +87,7 @@ internal class ChildFactory @Inject constructor( private val nftCollectionsComponentFactory: NFTCollectionsComponent.Factory, private val nftReceiveComponentFactory: NFTReceiveComponent.Factory, private val nftDetailsComponentFactory: NFTDetailsComponent.Factory, + private val nftSendComponentFactory: NFTSendComponent.Factory, private val testerRouter: TesterRouter, private val routingFeatureToggles: RoutingFeatureToggles, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, @@ -423,6 +425,17 @@ internal class ChildFactory @Inject constructor( params = NFTDetailsComponent.Params(userWalletId = route.userWalletId, nftAsset = route.nftAsset), componentFactory = nftDetailsComponentFactory, ) + is AppRoute.NFTSend -> { + createComponentChild( + context = context, + params = NFTSendComponent.Params( + userWalletId = route.userWalletId, + nftAsset = route.nftAsset, + nftCollectionName = route.nftCollectionName, + ), + componentFactory = nftSendComponentFactory, + ) + } is AppRoute.OnboardingNote, is AppRoute.SaveWallet, is AppRoute.OnboardingOther, @@ -775,6 +788,17 @@ internal class ChildFactory @Inject constructor( params = NFTDetailsComponent.Params(userWalletId = route.userWalletId, nftAsset = route.nftAsset), componentFactory = nftDetailsComponentFactory, ) + is AppRoute.NFTSend -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = NFTSendComponent.Params( + userWalletId = route.userWalletId, + nftAsset = route.nftAsset, + nftCollectionName = route.nftCollectionName, + ), + componentFactory = nftSendComponentFactory, + ) + } } // endregion } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index cc4bda0a08..8a62fb9023 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -295,4 +295,11 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, val nftAsset: NFTAsset, ) : AppRoute(path = "/nft_details/${userWalletId.stringValue}/${nftAsset.collectionId}/${nftAsset.id.stringValue}") + + @Serializable + data class NFTSend( + val userWalletId: UserWalletId, + val nftAsset: NFTAsset, + val nftCollectionName: String, + ) : AppRoute(path = "/send/nft/${userWalletId.stringValue}/$nftCollectionName/${nftAsset.id}") } \ 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 7bf806b77c..fce40fb61c 100644 --- a/features/send-v2/api/build.gradle.kts +++ b/features/send-v2/api/build.gradle.kts @@ -16,4 +16,5 @@ dependencies { /** Domain models */ implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.nft.models) } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/NFTSendComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/NFTSendComponent.kt new file mode 100644 index 0000000000..2403e179e6 --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/NFTSendComponent.kt @@ -0,0 +1,17 @@ +package com.tangem.features.send.v2.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.wallets.models.UserWalletId + +interface NFTSendComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + val nftAsset: NFTAsset, + val nftCollectionName: String, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt index e8d18db28b..3c2f9bcf9f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt @@ -2,9 +2,11 @@ package com.tangem.features.send.v2.di import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.send.v2.DefaultSendFeatureToggles +import com.tangem.features.send.v2.api.NFTSendComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.send.DefaultSendComponent +import com.tangem.features.send.v2.sendnft.DefaultNFTSendComponent import dagger.Binds import dagger.Module import dagger.Provides @@ -29,4 +31,8 @@ internal interface SendFeatureModuleBinds { @Binds @Singleton fun provideSendComponentFactory(impl: DefaultSendComponent.Factory): SendComponent.Factory + + @Binds + @Singleton + fun provideNFTSendComponentFactory(impl: DefaultNFTSendComponent.Factory): NFTSendComponent.Factory } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt index 38ff838b69..3c21d1380d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt @@ -2,8 +2,6 @@ package com.tangem.features.send.v2.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model -import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel -import com.tangem.features.send.v2.send.model.SendModel import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel @@ -18,11 +16,6 @@ import dagger.multibindings.IntoMap @InstallIn(ModelComponent::class) internal interface SendModelModule { - @Binds - @IntoMap - @ClassKey(SendModel::class) - fun provideSendModel(model: SendModel): Model - @Binds @IntoMap @ClassKey(SendAmountModel::class) @@ -38,11 +31,6 @@ internal interface SendModelModule { @ClassKey(SendFeeModel::class) fun provideSendFeeModel(model: SendFeeModel): Model - @Binds - @IntoMap - @ClassKey(SendConfirmModel::class) - fun provideSendConfirmModel(model: SendConfirmModel): Model - @Binds @IntoMap @ClassKey(NotificationsModel::class) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/SendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/SendModelModule.kt new file mode 100644 index 0000000000..7405859941 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/SendModelModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.send.v2.send.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel +import com.tangem.features.send.v2.send.model.SendModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface SendModelModule { + + @Binds + @IntoMap + @ClassKey(SendModel::class) + fun provideSendModel(model: SendModel): Model + + @Binds + @IntoMap + @ClassKey(SendConfirmModel::class) + fun provideSendConfirmModel(model: SendConfirmModel): Model +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt new file mode 100644 index 0000000000..8074966261 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -0,0 +1,89 @@ +package com.tangem.features.send.v2.sendnft + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultNFTSendComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: NFTSendComponent.Params, +) : NFTSendComponent, AppComponentContext by appComponentContext { + + // private val stackNavigation = StackNavigation() + // + // private val innerRouter = InnerRouter( + // stackNavigation = stackNavigation, + // popCallback = { onChildBack() }, + // ) + + // private val initialRoute = NFTSendRoute.Empty + // private val currentRouteFlow = MutableStateFlow(initialRoute) + + private val model: NFTSendModel = getOrCreateModel(params = params/*, router = innerRouter*/) + + // private val childStack = childStack( + // key = "NFTSendInnerStack", + // source = stackNavigation, + // serializer = null, + // initialConfiguration = initialRoute, + // handleBackButton = true, + // childFactory = { configuration, factoryContext -> + // // todo + // ComposableContentComponent { } + // }, + // ) + + init { + // childStack.subscribe( + // lifecycle = lifecycle, + // mode = ObserveLifecycleMode.CREATE_DESTROY, + // ) { stack -> + // componentScope.launch { + // when (val activeComponent = stack.active.instance) { + // is SendDestinationComponent -> { + // // analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened) + // activeComponent.updateState(model.uiState.value.destinationUM) + // } + // is SendFeeComponent -> { + // // analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened) + // } + // } + // currentRouteFlow.emit(stack.active.configuration) + // } + // } + } + + @Composable + override fun Content(modifier: Modifier) { + // val stackState by childStack.subscribeAsState() + // val state by model.uiState.collectAsStateWithLifecycle() + + BackHandler(onBack = ::onChildBack) + // TODO + } + + private fun onChildBack() { + // TODO + // val isEmptyRoute = childStack.value.active.configuration == NFTSendRoute.Empty + // val isEmptyStack = childStack.value.backStack.isEmpty() + // val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success + // + // if (isEmptyRoute || isEmptyStack || isSuccess) { + // router.pop() + // } else { + // stackNavigation.pop() + // } + } + + @AssistedFactory + interface Factory : NFTSendComponent.Factory { + override fun create(context: AppComponentContext, params: NFTSendComponent.Params): DefaultNFTSendComponent + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt new file mode 100644 index 0000000000..cf70962383 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.send.v2.sendnft.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface NFTSendModelModule { + + @Binds + @IntoMap + @ClassKey(NFTSendModel::class) + fun provideNFTSendModel(model: NFTSendModel): Model +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt new file mode 100644 index 0000000000..a87005f006 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -0,0 +1,144 @@ +package com.tangem.features.send.v2.sendnft.model + +import androidx.compose.runtime.Stable +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.navigation.Router +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.common.NavigationUM +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.properties.Delegates + +internal interface SendNFTComponentCallback : + SendFeeComponent.ModelCallback, + SendDestinationComponent.ModelCallback + +@Suppress("LongParameterList") +@Stable +@ModelScoped +internal class NFTSendModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, +) : Model(), SendNFTComponentCallback { + + val params: NFTSendComponent.Params = paramsContainer.require() + private val userWalletId = params.userWalletId + + private val _uiState = MutableStateFlow(initialState()) + val uiState = _uiState.asStateFlow() + + private val _isBalanceHiddenFlow = MutableStateFlow(false) + val isBalanceHiddenFlow = _isBalanceHiddenFlow.asStateFlow() + + var cryptoCurrency: CryptoCurrency by Delegates.notNull() + var userWallet: UserWallet by Delegates.notNull() + var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + var appCurrency: AppCurrency = AppCurrency.Default + + init { + subscribeOnCurrencyStatusUpdates() + initAppCurrency() + } + + override fun onNavigationResult(navigationUM: NavigationUM) { + _uiState.update { it.copy(navigationUM = navigationUM) } + } + + override fun onDestinationResult(destinationUM: DestinationUM) { + _uiState.update { it.copy(destinationUM = destinationUM) } + + // todo + } + + override fun onFeeResult(feeUM: FeeUM) { + _uiState.update { it.copy(feeUM = feeUM) } + router.pop() + } + + private fun initAppCurrency() { + modelScope.launch { + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + } + } + + private fun subscribeOnCurrencyStatusUpdates() { + modelScope.launch { + getUserWalletUseCase(params.userWalletId).fold( + ifRight = { wallet -> + userWallet = wallet + + // cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId).getOrNull() + // ?.filterIsInstance() + // ?.firstOrNull { it.network == nftAsset.network } + // ?: return@launch + + getCurrenciesStatusUpdates( + isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + ) + }, + ifLeft = { + // sendConfirmAlertFactory.getGenericErrorState(::onFailedTxEmailClick) + return@launch + }, + ) + } + } + + private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean) { + getCurrencyStatusUpdatesUseCase( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + isSingleWalletWithTokens = isSingleWalletWithToken, + ).onEach { maybeCryptoCurrency -> + maybeCryptoCurrency.fold( + ifRight = { cryptoStatus -> + cryptoCurrencyStatus = cryptoStatus + feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoStatus, + ).getOrNull() ?: cryptoStatus + + // router.push(NFTSendRoute.Destination(isEditMode = false)) + }, + ifLeft = { + // sendConfirmAlertFactory.getGenericErrorState { + // onFailedTxEmailClick(it.toString()) + // } + }, + ) + }.launchIn(modelScope) + } + + private fun initialState(): NFTSendUM = NFTSendUM( + destinationUM = DestinationUM.Empty(), + feeUM = FeeUM.Empty(), + confirmUM = ConfirmUM.Empty, + navigationUM = NavigationUM.Empty, + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt new file mode 100644 index 0000000000..56f0d674d2 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt @@ -0,0 +1,13 @@ +package com.tangem.features.send.v2.sendnft.ui.state + +import com.tangem.features.send.v2.common.NavigationUM +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM + +internal data class NFTSendUM( + val destinationUM: DestinationUM, + val feeUM: FeeUM, + val confirmUM: ConfirmUM, + val navigationUM: NavigationUM, +) \ No newline at end of file From a398620b0d9331fb95d560872486a446c8a1dd21 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 5 Apr 2025 12:19:37 +0400 Subject: [PATCH 04/72] Updated on 2026-08-14 --- common/test/build.gradle.kts | 3 + .../data/quote/MockQuoteResponseFactory.kt | 19 +++ .../test/data/quote/QuoteResponseExt.kt | 14 ++ .../local/quote/converter/QuoteConverter.kt | 17 ++- ...NetworksStatusesStoreInitializationTest.kt | 2 +- data/quotes/.gitignore | 1 + data/quotes/build.gradle.kts | 30 ++++ .../data/quotes/store/DefaultQuotesStoreV2.kt | 89 ++++++++++++ .../tangem/data/quotes/store/QuotesStoreV2.kt | 22 +++ .../quotes/store/QuotesStoreGetMethodTest.kt | 63 +++++++++ .../store/QuotesStoreInitializationTest.kt | 82 +++++++++++ .../store/QuotesStoreUpdateMethodsTest.kt | 131 ++++++++++++++++++ .../com/tangem/domain/tokens/model/Quote.kt | 7 + settings.gradle.kts | 1 + 14 files changed, 477 insertions(+), 4 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/data/quote/MockQuoteResponseFactory.kt create mode 100644 common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt create mode 100644 data/quotes/.gitignore create mode 100644 data/quotes/build.gradle.kts create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt create 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/QuotesStoreGetMethodTest.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt diff --git a/common/test/build.gradle.kts b/common/test/build.gradle.kts index 00c1510373..4ab99487fb 100644 --- a/common/test/build.gradle.kts +++ b/common/test/build.gradle.kts @@ -9,6 +9,9 @@ android { } dependencies { + implementation(projects.core.datasource) + implementation(projects.core.utils) + implementation(projects.data.common) implementation(projects.domain.legacy) diff --git a/common/test/src/main/java/com/tangem/common/test/data/quote/MockQuoteResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/quote/MockQuoteResponseFactory.kt new file mode 100644 index 0000000000..2e3f56492e --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/data/quote/MockQuoteResponseFactory.kt @@ -0,0 +1,19 @@ +package com.tangem.common.test.data.quote + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +object MockQuoteResponseFactory { + + fun createSinglePrice(value: BigDecimal): QuotesResponse.Quote { + return QuotesResponse.Quote( + price = value, + priceChange24h = value, + priceChange1w = value, + priceChange30d = value, + ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..ac570f6d81 --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt @@ -0,0 +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.domain.models.StatusSource +import com.tangem.domain.tokens.model.Quote + +fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): Quote { + return QuoteConverter(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()) +} \ 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/QuoteConverter.kt index 120bea1730..54af10bcb2 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/QuoteConverter.kt @@ -10,13 +10,24 @@ import com.tangem.utils.extensions.orZero /** * Converter from [QuotesResponse.Quote] to [Quote.Value] * - * @property isCached flag that determines whether the quote is a cache + * @property source status source * [REDACTED_AUTHOR] */ -internal class QuoteConverter(private val isCached: Boolean) : +class QuoteConverter( + private val source: StatusSource, +) : Converter, Quote.Value> { + /** + * 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): Quote.Value { val (currencyId, quote) = value @@ -24,7 +35,7 @@ internal class QuoteConverter(private val isCached: Boolean) : rawCurrencyId = CryptoCurrency.RawID(currencyId), fiatRate = quote.price.orZero(), priceChange = quote.priceChange24h.orZero().movePointLeft(2), - source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, + source = source, ) } } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt index 08314ee68e..94d819b827 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt @@ -31,7 +31,7 @@ internal class NetworksStatusesStoreInitializationTest { DefaultNetworksStatusesStoreV2( runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, // local mock + persistenceDataStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), ) diff --git a/data/quotes/.gitignore b/data/quotes/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/quotes/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts new file mode 100644 index 0000000000..56a4b96552 --- /dev/null +++ b/data/quotes/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.quotes" +} + +dependencies { + implementation(projects.core.datasource) + implementation(projects.core.utils) + + implementation(projects.domain.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + implementation(deps.androidx.datastore) + + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(projects.common.test) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..2ae06d88e6 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt @@ -0,0 +1,89 @@ +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.domain.models.StatusSource +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch + +internal typealias CurrencyIdWithQuote = Map + +/** + * Default implementation of [QuotesStoreV2] + * + * @property runtimeStore runtime store + * @property persistenceDataStore persistence store + * @param dispatchers dispatchers + */ +internal class DefaultQuotesStoreV2( + private val runtimeStore: RuntimeSharedStore>, + private val persistenceDataStore: DataStore, + dispatchers: CoroutineDispatcherProvider, +) : QuotesStoreV2 { + + private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) + + init { + scope.launch { + val cachedStatuses = persistenceDataStore.data.firstOrNull() + + if (cachedStatuses.isNullOrEmpty()) return@launch + + runtimeStore.store( + value = QuoteConverter(isCached = true).convertSet(input = cachedStatuses.entries), + ) + } + } + + override fun get(): Flow> = runtimeStore.get() + + override suspend fun refresh(currenciesIds: Set) { + updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.CACHE) + } + + override suspend fun storeActual(values: Map) { + coroutineScope { + launch { + val quotes = QuoteConverter(isCached = false).convertSet(input = values.entries) + storeInRuntimeStore(values = quotes) + } + launch { storeInPersistenceStore(values = values) } + } + } + + override suspend fun storeError(currenciesIds: Set) { + updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.ONLY_CACHE) + } + + private suspend fun updateStatusSourceInRuntime(currenciesIds: Set, source: StatusSource) { + runtimeStore.update(default = emptySet()) { stored -> + val updatedQuotes = currenciesIds.mapTo(hashSetOf()) { id -> + val quote = stored.firstOrNull { it.rawCurrencyId == id } ?: Quote.Empty(id) + + quote.copySealed(source = source) + } + + stored.addOrReplace(items = updatedQuotes) { old, new -> old.rawCurrencyId == new.rawCurrencyId } + } + } + + private suspend fun storeInRuntimeStore(values: Set) { + runtimeStore.update(default = emptySet()) { saved -> + saved.addOrReplace(items = values) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId } + } + } + + private suspend fun storeInPersistenceStore(values: Map) { + persistenceDataStore.updateData { storedQuotes -> storedQuotes + values } + } +} \ 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 new file mode 100644 index 0000000000..b530a6559c --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt @@ -0,0 +1,22 @@ +package com.tangem.data.quotes.store + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import kotlinx.coroutines.flow.Flow + +/** Store of [Quote]'es set */ +internal interface QuotesStoreV2 { + + /** Get flow of quotes */ + fun get(): Flow> + + /** 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/store/QuotesStoreGetMethodTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt new file mode 100644 index 0000000000..ede1171964 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt @@ -0,0 +1,63 @@ +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.Quote +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 = backgroundScope.getEmittedValues(testScheduler, 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 = backgroundScope.getEmittedValues(testScheduler, 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 = backgroundScope.getEmittedValues(testScheduler, actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(setOf(btcQuote.toDomain(), ethQuote.toDomain()))) + } +} \ 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 new file mode 100644 index 0000000000..2452a66004 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt @@ -0,0 +1,82 @@ +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.Quote +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 new file mode 100644 index 0000000000..407b640fd9 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt @@ -0,0 +1,131 @@ +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.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +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) + + val runtimeExpected = currenciesIds.map(Quote::Empty).toSet() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + 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.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(Quote::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), + Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + ), + ) + + store.storeError( + currenciesIds = setOf( + CryptoCurrency.RawID(value = "BTC"), + CryptoCurrency.RawID(value = "ETH"), + ), + ) + + val runtimeExpected = setOf( + status.toDomain(rawCurrencyId = "BTC", source = StatusSource.ONLY_CACHE), + Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } +} \ 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 index ebfb2e7525..6e3d9532f0 100644 --- 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 @@ -7,6 +7,13 @@ 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. * diff --git a/settings.gradle.kts b/settings.gradle.kts index cbcae1d8bb..76403e5965 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -314,4 +314,5 @@ include(":data:manage-tokens") include(":data:networks") include(":data:nft") include(":data:onramp") +include(":data:quotes") // endregion Data modules \ No newline at end of file From 5e83a20d1a0d03f51dd7bc6c1c8927ee5b0687f4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 12:53:01 +0300 Subject: [PATCH 05/72] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 9 +- core/res/src/main/res/values-fr/strings.xml | 51 ++++++++++ core/res/src/main/res/values-ja/strings.xml | 16 ++++ core/res/src/main/res/values/strings.xml | 29 +++--- .../feedback/converters/CardInfoConverter.kt | 2 + domain/feedback/build.gradle.kts | 1 + .../domain/feedback/FeedbackDataBuilder.kt | 31 ++++++ .../feedback/SendFeedbackEmailUseCase.kt | 14 ++- .../tangem/domain/feedback/models/CardInfo.kt | 1 + .../feedback/models/FeedbackEmailType.kt | 13 +++ .../utils/EmailMessageBodyResolver.kt | 10 ++ .../utils/EmailMessageTitleResolver.kt | 3 + .../feedback/utils/EmailSubjectResolver.kt | 3 + .../feedback/utils/FeedbackConstants.kt | 3 +- domain/visa/models/build.gradle.kts | 1 + .../tangem/domain/visa/model/VisaTxDetails.kt | 0 .../preview/PreviewDetailsComponent.kt | 2 + .../features/details/entity/DetailsUM.kt | 2 + .../entity/SelectEmailFeedbackTypeBS.kt | 16 ++++ .../features/details/model/DetailsModel.kt | 85 ++++++++++++++++- .../features/details/ui/DetailsScreen.kt | 2 + .../ui/SelectFeedbackEmailTypeBottomSheet.kt | 58 +++++++++++ .../impl/DefaultOnboardingStepperComponent.kt | 9 +- .../wallet/model/intents/VisaWalletIntents.kt | 27 ++++++ .../model/VisaTxDetailsBottomSheetConfig.kt | 1 + .../VisaTxDetailsBottomSheetConverter.kt | 1 + .../visa/VisaTxDetailsBottomSheet.kt | 95 ++++++++----------- 27 files changed, 409 insertions(+), 76 deletions(-) rename domain/visa/{ => models}/src/main/kotlin/com/tangem/domain/visa/model/VisaTxDetails.kt (100%) create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/entity/SelectEmailFeedbackTypeBS.kt create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/ui/SelectFeedbackEmailTypeBottomSheet.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index b7a04e5add..7d9d38c56c 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -503,6 +503,12 @@ %d Stück %d Stücke + NFTs, die an Deine Wallet-Adresse gesendet werden, werden hier angezeigt. + Noch keine Kollektionen + NFT erhalten + NFT-Kollektionen + Einige Daten werden möglicherweise nicht geladen + Vorübergehende Ladeprobleme %1$d NFTs in der %2$d Sammlung Tippe hier, um das erste NFT zu erhalten NFT-Sammlungen @@ -522,7 +528,7 @@ Bitte wiederhole den Vorgang. Die Karte oder Ring wird auf Werkseinstellungen zurückgesetzt. Aktivierungsfehler Token hinzufügen - Du hast einee Backup-Karte oder einen Backup-Ring hinzugefügt. Wenn der Backup-Prozess abgeschlossen ist, kannst Du keine weiteren Backup-Geräte hinzufügen. Wenn Du noch eine Karte oder einen Ring hast, fügen diese(n) zum Backup hinzu. Möchtest Du den Backup-Prozess fortsetzen? + Du hast eine Backup-Karte oder einen Backup-Ring hinzugefügt. Nach Abschluss des Backup-Vorgangs kannst Du keine weiteren Backup-Geräte hinzufügen. Wenn Du eine weitere Karte oder einen weiteren Ring hast, fügen diesen dem Backup jetzt hinzu. Möchtest Du den Backup-Vorgang fortsetzen? Der Sicherungsvorgang ist teilweise abgeschlossen. Du kannst ihn jetzt nicht beenden. Die Passphrase ist eine fortschrittliche Sicherheitsfunktion, die von Krypto-Wallets verwendet wird. Sie fügt ein zusätzliches Wort oder eine Phrase deiner Wahl zu der bereits bestehenden Wiederherstellungsphrase hinzu, um einen brandneuen Satz von Adressen zu erzeugen. Hinzufügen einer Sicherungskarte oder Ring @@ -1165,6 +1171,7 @@ Der Kontostand ist möglicherweise veraltet. Aktualisiere bittre die Seite. Verbindungen Alle trennen + Text über die Trennung aller dApps Alle dApps trennen Neue Verbindung Verbinde Deine Wallet mit einer anderen dApp diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 01f05ff01c..47b464f247 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -140,6 +140,7 @@ Marché Lent Vitesse et frais + Terminer Synchroniser les adresses Aller au fournisseur Aller au jeton @@ -159,6 +160,7 @@ Aucune adresse Maintenant OK + Ouvrir dans le navigateur Carte principale Bague principale Passphrase @@ -182,6 +184,7 @@ Envoyer Le serveur n\'est pas disponible, veuillez réessayer plus tard Partager + Partager le lien Signez Signez et envoyez Stake @@ -496,6 +499,16 @@ 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 + 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 + %1$d NFT dans la collection %2$d + Appuyez ici pour recevoir le premier NFT + Collections NFT + Impossible de charger les données Vous devez définir un seul code d\'accès pour protéger tous vos appareils. Protéger Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard @@ -762,6 +775,8 @@ y compris des frais de réseau de %1$s La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps %1$s est un actif du réseau Tron. Pour calculer les frais et effectuer une transaction, déposez du Tron (TRX) sur votre compte. + Une balise de destination (mémo) est requise pour terminer cette transaction pour l\'adresse spécifiée. + Étiquette de destination requise Transaction envoyée Scannez la carte/ bague que vous souhaitez configurer. Oublier le portefeuille @@ -783,6 +798,7 @@ %s profit estimatif Cote du marché Métriques + Selon les règles du réseau %1$s, les réclamations sont possibles à partir de %2$s. Les montants ci-dessous seront crédités sur votre compte lors du déblocage. Minimum requis Aucune récompense à réclamer Réclamation de récompense @@ -820,11 +836,16 @@ Solde de staking faible Un minimum de %1$s %2$s est requis pour le re-staking. Veuillez recharger votre solde. Pas assez de %s + Solde insuffisant pour le staking + Un minimum de 3 ADA est requis pour le re-staking. Veuillez recharger votre solde. + ADA insuffisants + Le montant minimum requis pour le staking doit être supérieur à 5 ADA. Veuillez recharger votre solde pour commencer à staking. L\'option de staking n\'est actuellement pas disponible en raison des conditions du réseau. Veuillez réessayer plus tard. Le staking dans le réseau %1$s avec un nouveau validateur transférera automatiquement tous les fonds précédemment stakés vers ce validateur Réinvestissez vos récompenses gagnées dans le montant que vous avez staké, augmentant ainsi vos gains potentiels. L\'option de restaker vous permet de déplacer vos fonds d\'un validateur à un autre sans avoir besoin de les déstaker. Vous êtes sur le point de staker l\'intégralité de votre solde. Nous vous recommandons de laisser un petit montant pour couvrir les frais de réseau pour unstaking ou la réclamation des récompenses. + Pour commencer à staker TON, effectuez d\'abord une transaction sortante de n\'importe quel montant — cela activera votre portefeuille. Débloquez votre argent pour le retirer du processus de staking. Le déverrouillage prend %s. Vos fonds seront disponibles à l\'utilisation après la période de déblocage de 21 jours. La récompense sera retirée en même temps que vos fonds de déblocage. Vos fonds seront disponibles pour utilisation après la période de désengagement %s. @@ -853,6 +874,7 @@ Récompenses Stake verrouillé Staker plus + Lorsque vous stakez %1$s, la totalité de votre solde %2$s est stakeée. Tout dépôt supplémentaire de %2$s sur votre portefeuille Tangem sera également staké automatiquement. Montant staké Vous stakez %1$s et recevrez %2$s Appuyez pour déverrouiller @@ -887,6 +909,8 @@ Découvrez Tangem Échangez, achetez des NFT, faites des prêts et des dépôts dans plus de 100 services décentralisés différents Compatible avec Web 3.0 + Une transaction entrante d\'au moins de %1$s est requise pour continuer + Fonds insuffisants Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange. Échangez plus de jetons à de meilleurs taux directement dans votre portefeuille. Nouveau fournisseur d\'échange disponible ! @@ -926,6 +950,7 @@ Vous n\'avez pas de fonds à envoyer. Renflouez votre compte pour pouvoir envoyer des fonds à partir de celui-ci. Die Daten wurden noch nicht geladen. Dies kann einige Sekunden dauern. Bitte versuchen Sie es später noch einmal. Le service d\'échange %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter plus d\'options. + Le solde affiché peut être obsolète en raison de la mise en cache. La vente de fonds sera disponible une fois que la ou les transactions en attente dans le réseau %s seront terminées L\'envoi de fonds sera disponible une fois la ou les transactions en attente dans le réseau %s terminées. L\'achat de %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter plus d\'options. @@ -972,6 +997,7 @@ Tangem Twin Cette action est irréversible. Vous n\'aurez plus accès à l\'ancien portefeuille. Appuyez sur la carte jumelle avec le numéro %s et ne la retirez pas jusqu\'à la fin de l\'opération + Nous avons rencontré une erreur. Code d\'erreur : %s. Veuillez contacter notre équipe de support. Utilisez %s ou scannez une carte/bague pour avoir accès à votre portefeuille Échec de la connexion : Cette dApp utilise Wallet Connect version1.0, qui n\'est pas prise en charge. Veuillez vous assurer que la dApp prend en charge Wallet Connect version2.0 pour réussir la connexion. Restez à jour avec les dernières fonctionnalités et actualités @@ -986,6 +1012,24 @@ Renommer le portefeuille Tout déverrouiller Tout déverrouiller avec %s + + disponible pour %d jour + disponible pour %d jours + + Soldes et Limites + Êtes-vous sûr de vouloir quitter ? Vous pourrez reprendre plus tard là où vous vous étiez arrêté. + Cela ne prendra pas longtemps. Nous configurons votre compte. + Cela ne prendra pas longtemps. Nous terminons l\'activation. + Tout est en cours de préparation ! + Code PIN invalide : évitez les séquences ou les répétitions + Accéder le site Web + Continuons la configuration de votre compte. + Content de vous revoir ! + Suivez les étapes pour configurer votre compte. + Bienvenue ! + Déverrouiller + Scannez votre carte pour déverrouiller l\'accès + Déverrouillage nécessaire La blockchain n\'est pas accessible. Réessayez plus tard Scanner la carte ou la bague Ce portefeuille a déjà été activé auparavant.\nSi cela n\'a pas été fait par vous, veuillez contacter le support.\nTangem ne vend jamais de portefeuilles avec le code d\'accès pré-généré. @@ -1116,6 +1160,13 @@ Il s\'agit d\'une carte Testnet. Elle ne peut pas traiter les transactions et ne doit être utilisée qu\'à des fins de test et de développement. À des fins de test uniquement Le solde peut être obsolète. Rafraîchissez la page. + Connexions + Déconnecter tout + Texte sur la déconnexion de toutes les dApps + Déconnecter toutes les dApps + Nouvelle connexion + Connectez votre portefeuille à différentes dApps + Aucune séance Ignorer Vous avez une sauvegarde interrompue. Voulez-vous la reprendre ? Oui, reprendre diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 9b67c45675..874225b0fe 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -153,6 +153,7 @@ ネットワーク手数料 送金額は、選択された手数料レベルをカバーするため、%1$s (%2$s) 減額されます。 + NFT いいえ アドレスがありません @@ -175,6 +176,7 @@ 検索 トークンを検索 + すべて見る シードフレーズ アクションを選択 売る @@ -502,6 +504,19 @@ NFTコレクション 一部のデータが読み込まれない場合があります 一時的な読み込みの問題 + 基本情報 + チェーン + コントラクトアドレス + 最終販売価格 + レアリティ・ラベル + レアリティ・ランク + トークンアドレス + トークンID + トークン標準 + 特徴 + 結果がありません。別のリクエストをお試しください。 + 私のウォレットへ + NFTを受け取る %1$dコレクションの%2$dNFT ここをタップして最初のNFTを受け取ります NFTコレクション @@ -635,6 +650,7 @@ カメラへのアクセスが拒否されました メモ不要 %3$sネットワーク上の%1$s ( %2$s ) + %2$sネットワーク上の%1$s 他の暗号資産を送信すると、取り返しのつかない損失が発生します。 このアドレスには%sのみを送金してください。他のトークンを送信すると、取り返しのつかない損失が発生します。 %2$sネットワークの%1$sのみを送信してください diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index a23e1b705f..74f931332b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -110,13 +110,14 @@ Claim rewards Close Confirm + Contact Tangem Support + Contact Visa Support Continue Copy Copy address Create %1$s (%2$s) Custom - NFT %d day %d days @@ -157,6 +158,7 @@ Network fee Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level Next + NFT No No address Now @@ -512,24 +514,24 @@ NFT collections Some data may not load Temporary loading problems + Base information + Chain + Contract Address + Last sale price + Rarity label + Rarity rank + Token Address + Token ID + Token Standard + Traits No results. Please try another request. - Receive NFT + Choose network To My wallet + Receive NFT %1$d NFTs in %2$d collection Tap here to receive first NFT NFT collections Unable to load the data - Choose network - Last sale price - Rarity label - Rarity rank - Traits - Base information - Token Standard - Contract Address - Token ID - Token Address - Chain Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. @@ -1098,6 +1100,7 @@ Transaction request Transaction status Type + Dispute this transaction Unlock Scan your card to unlock access Needed unlock diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt index c1855597a2..7fc333d67d 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt @@ -1,6 +1,7 @@ package com.tangem.data.feedback.converters import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.TapWorkarounds.isVisa import com.tangem.domain.common.util.getBackupCardsCount import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.models.scan.CardDTO @@ -29,6 +30,7 @@ internal object CardInfoConverter : Converter { }, isImported = value.card.wallets.any(CardDTO.Wallet::isImported), isStart2Coin = value.card.isStart2Coin, + isVisa = value.card.isVisa, ) } } diff --git a/domain/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts index 28fe483e00..213c40c655 100644 --- a/domain/feedback/build.gradle.kts +++ b/domain/feedback/build.gradle.kts @@ -15,4 +15,5 @@ dependencies { implementation(projects.core.res) implementation(projects.domain.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.visa.models) } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index c2726a604f..7d0f5aa49f 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -2,12 +2,43 @@ package com.tangem.domain.feedback import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.utils.breakLine +import com.tangem.domain.visa.model.VisaTxDetails import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses internal class FeedbackDataBuilder { private val builder = StringBuilder() + fun addVisaTxInfo(txDetails: VisaTxDetails) { + builder.appendKeyValue("Type", txDetails.type) + builder.appendKeyValue("Status", txDetails.status) + builder.appendKeyValue("Blockchain amount", txDetails.blockchainAmount.toString()) + builder.appendKeyValue("Transaction amount", txDetails.transactionAmount.toString()) + builder.appendKeyValue("Currency code", txDetails.transactionCurrencyCode.toString()) + builder.appendKeyValue("Merchant name", txDetails.merchantName) + builder.appendKeyValue("Merchant city", txDetails.merchantCity) + builder.appendKeyValue("Merchant country code", txDetails.merchantCountryCode) + builder.appendKeyValue("Merchant category code", txDetails.merchantCategoryCode) + + builder.appendDelimiter() + builder.breakLine() + builder.append("Requests:") + + txDetails.requests.forEach { request -> + builder.appendKeyValue("Type", request.requestType) + builder.appendKeyValue("Status", request.requestStatus) + builder.appendKeyValue("Blockchain amount", request.blockchainAmount.toString()) + builder.appendKeyValue("Transaction amount", request.transactionAmount.toString()) + builder.appendKeyValue("Currency code", request.txCurrencyCode.toString()) + builder.appendKeyValue("Error code", request.errorCode.toString()) + builder.appendKeyValue("Date", request.requestDate.toString()) + builder.appendKeyValue("Transaction hash", request.txHash) + builder.appendKeyValue("Transaction status", request.txStatus) + builder.appendDelimiter() + builder.breakLine() + } + } + fun addUserWalletsInfo(userWalletsInfo: UserWalletsInfo) { builder.appendKeyValue("User Wallet ID", userWalletsInfo.selectedUserWalletId) builder.appendKeyValue("Total saved wallets", userWalletsInfo.totalUserWallets.toString()) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt index 7bb6e53a9b..a94664bc9a 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt @@ -2,7 +2,6 @@ package com.tangem.domain.feedback import android.content.res.Resources import com.tangem.core.res.getStringSafe -import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.feedback.models.FeedbackEmail import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.repository.FeedbackRepository @@ -27,7 +26,7 @@ class SendFeedbackEmailUseCase( suspend operator fun invoke(type: FeedbackEmailType) { val email = FeedbackEmail( - address = getAddress(type.cardInfo), + address = getAddress(type), subject = emailSubjectResolver.resolve(type), message = createMessage(type), // Temporally user data is not sent @@ -37,8 +36,12 @@ class SendFeedbackEmailUseCase( feedbackRepository.sendEmail(email) } - private fun getAddress(cardInfo: CardInfo?): String { - return if (cardInfo?.isStart2Coin == true) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL + private fun getAddress(type: FeedbackEmailType): String { + return when { + type is FeedbackEmailType.Visa || type.cardInfo?.isVisa == true -> TANGEM_VISA_SUPPORT_EMAIL + type.cardInfo?.isStart2Coin == true -> START2COIN_SUPPORT_EMAIL + else -> TANGEM_SUPPORT_EMAIL + } } private suspend fun createMessage(type: FeedbackEmailType): String { @@ -61,12 +64,15 @@ class SendFeedbackEmailUseCase( is FeedbackEmailType.CurrencyDescriptionError, is FeedbackEmailType.PreActivatedWallet, is FeedbackEmailType.CardAttestationFailed, + is FeedbackEmailType.Visa.Dispute, -> this is FeedbackEmailType.DirectUserRequest, is FeedbackEmailType.RateCanBeBetter, is FeedbackEmailType.StakingProblem, is FeedbackEmailType.SwapProblem, is FeedbackEmailType.TransactionSendingProblem, + is FeedbackEmailType.Visa.Activation, + is FeedbackEmailType.Visa.DirectUserRequest, -> { append(resources.getStringSafe(R.string.feedback_data_collection_message)) skipLine() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt index 681cb957c0..d6d83ea329 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt @@ -11,6 +11,7 @@ data class CardInfo( val signedHashesList: List, val isImported: Boolean, val isStart2Coin: Boolean, + val isVisa: Boolean, ) { data class SignedHashes(val curve: String, val total: String?) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 364c363194..7853427ca5 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -1,5 +1,7 @@ package com.tangem.domain.feedback.models +import com.tangem.domain.visa.model.VisaTxDetails + /** * Email feedback type * @@ -52,4 +54,15 @@ sealed interface FeedbackEmailType { data object CardAttestationFailed : FeedbackEmailType { override val cardInfo: CardInfo? = null } + + sealed class Visa : FeedbackEmailType { + data class DirectUserRequest(override val cardInfo: CardInfo) : Visa() + + data class Activation(override val cardInfo: CardInfo) : Visa() + + data class Dispute( + val visaTxDetails: VisaTxDetails, + override val cardInfo: CardInfo, + ) : Visa() + } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index e531f71d3f..7711abe1d1 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -4,6 +4,7 @@ import com.tangem.domain.feedback.FeedbackDataBuilder import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.visa.model.VisaTxDetails /** * Email message body resolver @@ -29,11 +30,20 @@ internal class EmailMessageBodyResolver( is FeedbackEmailType.ScanningProblem, is FeedbackEmailType.CardAttestationFailed, -> addPhoneInfoBody() + is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.cardInfo) + is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.cardInfo) + is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.cardInfo, type.visaTxDetails) } return build() } + private suspend fun FeedbackDataBuilder.addVisaRequestBody(cardInfo: CardInfo, visaTxDetails: VisaTxDetails) { + addUserRequestBody(cardInfo) + addDelimiter() + addVisaTxInfo(visaTxDetails) + } + private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) { addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(cardInfo.userWalletId)) addDelimiter() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 30058b611e..1708558099 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -20,6 +20,9 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { is FeedbackEmailType.DirectUserRequest, is FeedbackEmailType.CurrencyDescriptionError, is FeedbackEmailType.CardAttestationFailed, + is FeedbackEmailType.Visa.Activation, + is FeedbackEmailType.Visa.DirectUserRequest, + is FeedbackEmailType.Visa.Dispute, -> R.string.feedback_preface_support is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index cc2b6c379c..849d1ce0c4 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -37,6 +37,9 @@ internal class EmailSubjectResolver(private val resources: Resources) { resources.getStringSafe(R.string.feedback_token_description_error) } FeedbackEmailType.CardAttestationFailed -> "Card attestation failed" + is FeedbackEmailType.Visa.Activation -> "[Visa] [Activation] {auto-filled subject}" + is FeedbackEmailType.Visa.DirectUserRequest -> "[Visa] {auto-filled subject}" + is FeedbackEmailType.Visa.Dispute -> "[Visa] [DISPUTE] {auto-filled subject}" } } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt index aeaa917e5f..0e70883587 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt @@ -3,4 +3,5 @@ package com.tangem.domain.feedback.utils internal const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB internal const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com" -internal const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" \ No newline at end of file +internal const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" +internal const val TANGEM_VISA_SUPPORT_EMAIL = "visa-support@tangem.com" // [REDACTED_TODO_COMMENT] \ No newline at end of file diff --git a/domain/visa/models/build.gradle.kts b/domain/visa/models/build.gradle.kts index 0aafe4291a..2de3acff50 100644 --- a/domain/visa/models/build.gradle.kts +++ b/domain/visa/models/build.gradle.kts @@ -10,5 +10,6 @@ dependencies { ksp(deps.moshi.kotlin.codegen) implementation(deps.moshi.adapters) implementation(deps.kotlin.serialization) + implementation(deps.jodatime) implementation(projects.core.error) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaTxDetails.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaTxDetails.kt similarity index 100% rename from domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaTxDetails.kt rename to domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaTxDetails.kt diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 9a4ec03248..37abe05472 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.core.navigation.url.DummyUrlOpener +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsUM @@ -28,6 +29,7 @@ internal class PreviewDetailsComponent : DetailsComponent { val previewState = DetailsUM( items = previewBlocks, footer = previewFooter, + selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig.Empty, popBack = { /* no-op */ }, ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt index 15f0fead96..b7c390e3bd 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt @@ -1,9 +1,11 @@ package com.tangem.features.details.entity +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import kotlinx.collections.immutable.ImmutableList internal data class DetailsUM( val items: ImmutableList, val footer: DetailsFooterUM, + val selectFeedbackEmailTypeBSConfig: TangemBottomSheetConfig, val popBack: () -> Unit, ) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/SelectEmailFeedbackTypeBS.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/SelectEmailFeedbackTypeBS.kt new file mode 100644 index 0000000000..18363c2c30 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/SelectEmailFeedbackTypeBS.kt @@ -0,0 +1,16 @@ +package com.tangem.features.details.entity + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.details.impl.R + +internal data class SelectEmailFeedbackTypeBS( + val onOptionClick: (Option) -> Unit, +) : TangemBottomSheetConfigContent { + + enum class Option(val text: TextReference) { + General(resourceReference(R.string.common_contact_tangem_support)), + Visa(resourceReference(R.string.common_contact_visa_support)), + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index 4ccf6cdd01..c659204216 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -7,17 +7,22 @@ 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.navigation.url.UrlOpener +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.domain.common.TapWorkarounds.isVisa import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.entity.DetailsUM +import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS import com.tangem.features.details.utils.ItemsBuilder import com.tangem.features.details.utils.SocialsBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -47,6 +52,7 @@ internal class DetailsModel @Inject constructor( private val appStateHolder: ReduxStateHolder, private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val getWalletsUseCase: GetWalletsUseCase, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { @@ -84,6 +90,7 @@ internal class DetailsModel @Inject constructor( socials = socialsBuilder.buildAll(), appVersion = getAppVersion(), ), + selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig.Empty, popBack = router::pop, ), ) @@ -99,12 +106,88 @@ internal class DetailsModel @Inject constructor( private fun sendFeedback() { modelScope.launch { + val userWallets = getWalletsUseCase.invokeSync() + val scanResponse = getSelectedWalletSyncUseCase().getOrNull()?.scanResponse ?: error("Selected wallet is null") val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch - sendFeedbackEmailUseCase(type = FeedbackEmailType.DirectUserRequest(cardInfo = cardInfo)) + val feedbackType = when { + userWallets.all { it.scanResponse.card.isVisa } -> FeedbackEmailType.Visa.DirectUserRequest(cardInfo) + userWallets.all { it.scanResponse.card.isVisa.not() } -> FeedbackEmailType.DirectUserRequest(cardInfo) + else -> { + showFeedbackEmailTypeOptionBS(cardInfo) + return@launch + } + } + + sendFeedbackEmailUseCase(feedbackType) + } + } + + private fun showFeedbackEmailTypeOptionBS(selectedCardInfo: CardInfo) { + state.update { + it.copy( + selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = { + state.update { + it.copy( + selectFeedbackEmailTypeBSConfig = + it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), + ) + } + }, + content = SelectEmailFeedbackTypeBS( + onOptionClick = { option -> + onEmailFeedbackTypeOptionSelected( + selectedCardInfo = selectedCardInfo, + option = option, + ) + + state.update { + it.copy( + selectFeedbackEmailTypeBSConfig = + it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), + ) + } + }, + ), + ), + ) + } + } + + private fun onEmailFeedbackTypeOptionSelected( + selectedCardInfo: CardInfo, + option: SelectEmailFeedbackTypeBS.Option, + ) { + modelScope.launch { + val feedbackType = when (option) { + SelectEmailFeedbackTypeBS.Option.General -> { + if (selectedCardInfo.isVisa.not()) { + FeedbackEmailType.DirectUserRequest(selectedCardInfo) + } else { + val scanResponse = getWalletsUseCase.invokeSync() + .firstOrNull { it.scanResponse.card.isVisa.not() }?.scanResponse ?: return@launch + val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch + FeedbackEmailType.DirectUserRequest(cardInfo) + } + } + SelectEmailFeedbackTypeBS.Option.Visa -> { + if (selectedCardInfo.isVisa) { + FeedbackEmailType.Visa.DirectUserRequest(selectedCardInfo) + } else { + val scanResponse = getWalletsUseCase.invokeSync() + .firstOrNull { it.scanResponse.card.isVisa }?.scanResponse ?: return@launch + val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch + FeedbackEmailType.Visa.DirectUserRequest(cardInfo) + } + } + } + + sendFeedbackEmailUseCase(feedbackType) } } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 0c2faf9735..cf172f3a90 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -59,6 +59,8 @@ internal fun DetailsScreen( userWalletListBlockContent = userWalletListBlockContent, ) } + + SelectFeedbackEmailTypeBottomSheet(state.selectFeedbackEmailTypeBSConfig) } @Composable diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/SelectFeedbackEmailTypeBottomSheet.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/SelectFeedbackEmailTypeBottomSheet.kt new file mode 100644 index 0000000000..583b911d76 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/SelectFeedbackEmailTypeBottomSheet.kt @@ -0,0 +1,58 @@ +package com.tangem.features.details.ui + +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 com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.inputrow.InputRowChecked +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS +import com.tangem.features.details.impl.R + +@Composable +internal fun SelectFeedbackEmailTypeBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + titleText = resourceReference(R.string.common_choose_action), + containerColor = TangemTheme.colors.background.tertiary, + content = { Content(it) }, + ) +} + +@Composable +private fun Content(content: SelectEmailFeedbackTypeBS) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + SelectEmailFeedbackTypeBS.Option.entries.forEachIndexed { index, type -> + DividerContainer( + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = SelectEmailFeedbackTypeBS.Option.entries.lastIndex, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { content.onOptionClick(type) }, + showDivider = index != SelectEmailFeedbackTypeBS.Option.entries.lastIndex, + ) { + InputRowChecked( + text = type.text, + checked = false, + ) + } + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt index 640ac88a7f..5bedacb9ee 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt @@ -9,6 +9,7 @@ import com.arkivanov.essenty.instancekeeper.getOrCreateSimple import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.common.TapWorkarounds.isVisa import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType @@ -40,7 +41,13 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor( componentScope.launch { val cardInfo = getCardInfoUseCase(params.scanResponse).getOrNull() ?: return@launch - sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) + sendFeedbackEmailUseCase( + if (params.scanResponse.card.isVisa) { + FeedbackEmailType.Visa.Activation(cardInfo) + } else { + FeedbackEmailType.DirectUserRequest(cardInfo) + }, + ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt index 8fe27fb45f..5f66b3f918 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt @@ -2,8 +2,13 @@ package com.tangem.feature.wallet.child.wallet.model.intents import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.visa.GetVisaTxDetailsUseCase +import com.tangem.domain.visa.model.VisaTxDetails +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.BalancesAndLimitsBottomSheetConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.VisaTxDetailsBottomSheetConverter @@ -20,14 +25,20 @@ internal interface VisaWalletIntents { fun onVisaTransactionClick(id: String) fun onExploreClick(exploreUrl: String) + + fun onDisputeClick(txDetails: VisaTxDetails) } +@Suppress("LongParameterList") @ModelScoped internal class VisaWalletIntentsImplementor @Inject constructor( private val stateController: WalletStateController, private val eventSender: WalletEventSender, private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, private val getVisaTxDetailsUseCase: GetVisaTxDetailsUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val getUserWalletsUseCase: GetWalletsUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), VisaWalletIntents { @@ -78,4 +89,20 @@ internal class VisaWalletIntentsImplementor @Inject constructor( override fun onExploreClick(exploreUrl: String) { router.openUrl(exploreUrl) } + + override fun onDisputeClick(txDetails: VisaTxDetails) { + modelScope.launch { + val userWalletId = stateController.getSelectedWalletId() + val userWallet = getUserWalletsUseCase.invokeSync() + .firstOrNull { it.walletId == userWalletId } ?: return@launch + val cardInfo = getCardInfoUseCase.invoke(userWallet.scanResponse).getOrNull() ?: return@launch + + sendFeedbackEmailUseCase( + FeedbackEmailType.Visa.Dispute( + cardInfo = cardInfo, + visaTxDetails = txDetails, + ), + ) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/VisaTxDetailsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/VisaTxDetailsBottomSheetConfig.kt index bf64d76838..a43a0ef835 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/VisaTxDetailsBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/VisaTxDetailsBottomSheetConfig.kt @@ -6,6 +6,7 @@ import kotlinx.collections.immutable.ImmutableList internal data class VisaTxDetailsBottomSheetConfig( val transaction: Transaction, val requests: ImmutableList, + val onDisputeClick: () -> Unit, ) : TangemBottomSheetConfigContent { data class Transaction( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt index ba80bbf6a3..99b787897e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt @@ -24,6 +24,7 @@ internal class VisaTxDetailsBottomSheetConverter( return VisaTxDetailsBottomSheetConfig( transaction = createTransaction(value), requests = value.requests.map(::createRequest).toImmutableList(), + onDisputeClick = { clickIntents.onDisputeClick(value) }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt index 2f1df0fb3e..4007c48a5b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt @@ -6,6 +6,7 @@ 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.items import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -15,6 +16,7 @@ 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.components.SecondaryButtonIconStart import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -36,26 +38,45 @@ internal fun VisaTxDetailsBottomSheet(config: TangemBottomSheetConfig) { } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun VisaTxDetailsBottomSheetContent(config: VisaTxDetailsBottomSheetConfig, modifier: Modifier = Modifier) { - ContentContainer( - modifier = modifier, - blocksCount = config.requests.size.inc(), - title = { + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size44) + .background(TangemTheme.colors.background.secondary), + contentAlignment = Alignment.Center, + ) { Text( text = stringResourceSafe(R.string.visa_transaction_details_header), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) - }, - block = { index -> - if (index == 0) { + } + + LazyColumn( + modifier = modifier.background(TangemTheme.colors.background.secondary), + contentPadding = PaddingValues( + bottom = TangemTheme.dimens.spacing16, + ), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { TransactionBlock(config.transaction) - } else { - BlockchainRequestBlock(config.requests[index - 1]) } - }, - ) + + items(config.requests) { item -> + BlockchainRequestBlock(item) + } + + item { + DisputeButton(config.onDisputeClick) + } + } + } } @Composable @@ -173,38 +194,14 @@ private fun BlockchainRequestBlock(request: VisaTxDetailsBottomSheetConfig.Reque ) } -@OptIn(ExperimentalFoundationApi::class) @Composable -private fun ContentContainer( - blocksCount: Int, - title: @Composable BoxScope.() -> Unit, - block: @Composable ColumnScope.(Int) -> Unit, - modifier: Modifier = Modifier, -) { - LazyColumn( - modifier = modifier.background(TangemTheme.colors.background.secondary), - contentPadding = PaddingValues( - bottom = TangemTheme.dimens.spacing16, - ), - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - stickyHeader { - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size44) - .background(TangemTheme.colors.background.secondary), - contentAlignment = Alignment.Center, - content = title, - ) - } - items(blocksCount) { index -> - Column { - block(index) - } - } - } +private fun DisputeButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + SecondaryButtonIconStart( + modifier = modifier.fillMaxWidth(), + text = stringResourceSafe(R.string.visa_tx_dispute_button), + iconResId = R.drawable.ic_alert_triangle_20, + onClick = onClick, + ) } // region Preview @@ -249,20 +246,8 @@ private class VisaTxDetailsBottomSheetParameterProvider : txStatus = "confirmed", onExploreClick = {}, ), - VisaTxDetailsBottomSheetConfig.Request( - id = "524582128501966799", - type = "settlement", - status = "accepted", - blockchainAmount = "1.0614 USDT", - transactionAmount = "0.99 €", - currencyCode = "978", - errorCode = 0, - date = "2023-12-01 00:01:00.000 +0300", - txHash = "0x635841d5fbdf1087cdd929019c863ee88a7165e4340bc17ddd0b1d04dfb11daa", - txStatus = "confirmed", - onExploreClick = {}, - ), ), + onDisputeClick = {}, ), ), ) From 66e04bc0cd3ba8557cd110b88786159a5d68b626 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 5 Apr 2025 12:54:59 +0400 Subject: [PATCH 06/72] Updated on 2026-08-14 --- data/quotes/build.gradle.kts | 1 + .../single/DefaultSingleQuoteProducer.kt | 36 ++++ .../single/DefaultSingleQuoteProducerTest.kt | 175 ++++++++++++++++++ domain/quotes/.gitignore | 1 + domain/quotes/build.gradle.kts | 9 + .../quotes/single/SingleQuoteProducer.kt | 17 ++ .../quotes/single/SingleQuoteSupplier.kt | 17 ++ settings.gradle.kts | 1 + 8 files changed, 257 insertions(+) create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt create mode 100644 domain/quotes/.gitignore create mode 100644 domain/quotes/build.gradle.kts create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index 56a4b96552..1da7f7a50e 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.quotes) implementation(projects.domain.wallets.models) implementation(deps.androidx.datastore) 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 new file mode 100644 index 0000000000..346bce56ca --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt @@ -0,0 +1,36 @@ +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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapNotNull + +/** + * Default implementation of [SingleQuoteProducer] + * + * @property params params + * @property quotesStore quotes store + */ +internal class DefaultSingleQuoteProducer @AssistedInject constructor( + @Assisted val params: SingleQuoteProducer.Params, + private val quotesStore: QuotesStoreV2, +) : SingleQuoteProducer { + + override val fallback: Quote = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + + override fun produce(): Flow { + return quotesStore.get() + .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } } + .distinctUntilChanged() + } + + @AssistedFactory + interface Factory : SingleQuoteProducer.Factory { + override fun create(params: SingleQuoteProducer.Params): DefaultSingleQuoteProducer + } +} \ No newline at end of file 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/DefaultSingleQuoteProducerTest.kt new file mode 100644 index 0000000000..655c97fad6 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt @@ -0,0 +1,175 @@ +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.domain.models.StatusSource +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultSingleQuoteProducerTest { + + private val params = SingleQuoteProducer.Params( + rawCurrencyId = CryptoCurrency.RawID(value = "BTC"), + ) + + private val quotesStore = mockk() + + private val producer = DefaultSingleQuoteProducer( + params = params, + quotesStore = quotesStore, + ) + + @Test + fun `test that flow is mapped for network from params`() = runTest { + val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + val storeQuote = flowOf( + setOf( + status, + Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + ), + ) + + every { quotesStore.get() } returns storeQuote + + val actual = producer.produce() + + verify { quotesStore.get() } + + val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(status)) + } + + @Test + fun `test that flow is updated if quote is updated`() = runTest { + val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + + every { quotesStore.get() } returns storeQuote + + val actual = producer.produceWithFallback() + + verify { quotesStore.get() } + + // first emit + val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + storeQuote.emit(value = setOf(status)) + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(status)) + + // second emit + val updatedStatus = Quote.Value( + rawCurrencyId = params.rawCurrencyId, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ) + storeQuote.emit(value = setOf(updatedStatus)) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values2.size).isEqualTo(2) + Truth.assertThat(values2).isEqualTo(listOf(status, updatedStatus)) + } + + @Test + fun `test that flow is filtered the same status`() = runTest { + val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + + every { quotesStore.get() } returns storeQuote + + val actual = producer.produceWithFallback() + + verify { quotesStore.get() } + + // first emit + val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + storeQuote.emit(value = setOf(status)) + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(status)) + + // second emit + storeQuote.emit(value = setOf(status)) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(status)) + } + + @Test + fun `test if flow throws exception`() = runTest { + val exception = IllegalStateException() + val status = Quote.Value( + rawCurrencyId = params.rawCurrencyId, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ) + + val innerFlow = MutableStateFlow(value = false) + val storeQuote = flow { + if (innerFlow.value) { + emit(setOf(status)) + } else { + throw exception + } + } + .buffer(capacity = 5) + + every { quotesStore.get() } returns storeQuote + + val actual = producer.produceWithFallback() + + verify { quotesStore.get() } + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + val fallbackStatus = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus)) + + innerFlow.emit(value = true) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(status)) + } + + @Test + fun `test if flow doesn't contain network from params`() = runTest { + val storeFlow = flowOf( + setOf( + Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + ), + ) + + every { quotesStore.get() } returns storeFlow + + val actual = producer.produceWithFallback() + + verify { quotesStore.get() } + + val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values.size).isEqualTo(0) + } +} \ No newline at end of file diff --git a/domain/quotes/.gitignore b/domain/quotes/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/quotes/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/quotes/build.gradle.kts b/domain/quotes/build.gradle.kts new file mode 100644 index 0000000000..8f7c2b07d7 --- /dev/null +++ b/domain/quotes/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + api(projects.domain.core) + api(projects.domain.tokens.models) +} \ 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 new file mode 100644 index 0000000000..7185ea9577 --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.quotes.single + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote + +/** + * Producer of quote [CryptoCurrency.RawID] + * +[REDACTED_AUTHOR] + */ +interface SingleQuoteProducer : FlowProducer { + + data class Params(val rawCurrencyId: CryptoCurrency.RawID) + + interface Factory : FlowProducer.Factory +} \ 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 new file mode 100644 index 0000000000..e65dbb492d --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.quotes.single + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.tokens.model.Quote + +/** + * 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/settings.gradle.kts b/settings.gradle.kts index 76403e5965..68b2aa945c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -288,6 +288,7 @@ include(":domain:promo:models") include(":domain:nft") include(":domain:nft:models") include(":domain:networks") +include(":domain:quotes") // endregion Domain modules // region Data modules From 3249180a42ac86d9b51f0d29e9af7f3187a22c00 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 10:45:23 +0000 Subject: [PATCH 07/72] 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 f33bae32b2..8361e7d217 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "releases-5.23-1007" +tangemBlockchainSdk = "develop-1005" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-452" +tangemCardSdk = "develop-451" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From e514ca97d589cf0bccbf6f27d5919b2d217441f4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 12:32:10 +0000 Subject: [PATCH 08/72] 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 f33bae32b2..8361e7d217 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "releases-5.23-1007" +tangemBlockchainSdk = "develop-1005" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-452" +tangemCardSdk = "develop-451" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From c8c4f670187eb1fc8143753e82bca18759690a1a Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 6 Apr 2025 13:08:08 +0400 Subject: [PATCH 09/72] Updated on 2026-08-14 --- .../appcurrency/AppCurrencyResponseStore.kt | 13 ++ .../DefaultAppCurrencyResponseStore.kt | 22 +++ .../datasource/di/AppCurrencyDataModule.kt | 9 ++ data/quotes/build.gradle.kts | 5 + .../quotes/multi/DefaultMultiQuoteFetcher.kt | 62 ++++++++ .../multi/DefaultMultiQuoteFetcherTest.kt | 143 ++++++++++++++++++ .../QuotesUnsupportedCurrenciesIdAdapter.kt | 2 +- .../domain/quotes/multi/MultiQuoteFetcher.kt | 14 ++ 8 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt new file mode 100644 index 0000000000..15d4280a28 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.appcurrency + +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse + +/** + * Store of app currency data model [CurrenciesResponse.Currency] + * +[REDACTED_AUTHOR] + */ +interface AppCurrencyResponseStore { + + suspend fun getSyncOrNull(): CurrenciesResponse.Currency? +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt new file mode 100644 index 0000000000..6d1b36f2fd --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.appcurrency + +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull + +/** + * Default implementation of [AppCurrencyResponseStore] + * + * @property appPreferencesStore app preferences store + */ +internal class DefaultAppCurrencyResponseStore( + private val appPreferencesStore: AppPreferencesStore, +) : AppCurrencyResponseStore { + + override suspend fun getSyncOrNull(): CurrenciesResponse.Currency? { + return appPreferencesStore.getObjectSyncOrNull( + PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt index fade673123..70fb61dbea 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt @@ -1,8 +1,11 @@ package com.tangem.datasource.di +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore +import com.tangem.datasource.appcurrency.DefaultAppCurrencyResponseStore import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,4 +21,10 @@ internal object AppCurrencyDataModule { fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore { return DefaultAvailableAppCurrenciesStore(dataStore = RuntimeDataStore()) } + + @Provides + @Singleton + fun provideAppCurrencyResponseStore(appPreferencesStore: AppPreferencesStore): AppCurrencyResponseStore { + return DefaultAppCurrencyResponseStore(appPreferencesStore) + } } \ No newline at end of file diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index 1da7f7a50e..9005f39528 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -13,12 +13,17 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) + implementation(projects.data.common) + implementation(projects.data.tokens) + + implementation(projects.domain.appCurrency.models) implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.quotes) implementation(projects.domain.wallets.models) implementation(deps.androidx.datastore) + implementation(deps.timber) implementation(deps.hilt.android) kapt(deps.hilt.kapt) 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 new file mode 100644 index 0000000000..5fc337e08c --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt @@ -0,0 +1,62 @@ +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.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.tokens.model.CryptoCurrency +import timber.log.Timber + +/** + * Default implementation of [MultiQuoteFetcher] + * + * @property tangemTechApi tangemTech api + * @property appCurrencyResponseStore app currency response store + * @property quotesStore quotes store + * +[REDACTED_AUTHOR] + */ +internal class DefaultMultiQuoteFetcher( + private val tangemTechApi: TangemTechApi, + private val appCurrencyResponseStore: AppCurrencyResponseStore, + private val quotesStore: QuotesStoreV2, +) : MultiQuoteFetcher { + + private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter() + + override suspend fun invoke(params: MultiQuoteFetcher.Params): Either = Either.catch { + quotesStore.refresh(currenciesIds = params.currenciesIds) + + val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( + currenciesIds = params.currenciesIds.mapTo( + destination = hashSetOf(), + transform = CryptoCurrency.RawID::value, + ), + ) + + val appCurrency = appCurrencyResponseStore.getSyncOrNull() + ?: error(message = "Unable to get AppCurrency for updating quotes") + + safeApiCallWithTimeout( + call = { + val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") + val response = tangemTechApi.getQuotes(currencyId = appCurrency.id, coinIds = coinIds).bind() + + val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( + response = response, + filteredIds = replacementIdsResult.idsFiltered, + ) + + quotesStore.storeActual(values = updatedResponse.quotes) + }, + onError = { error -> throw error }, + ) + } + .onLeft { + Timber.e(it) + quotesStore.storeError(currenciesIds = params.currenciesIds) + } +} \ No newline at end of file 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/DefaultMultiQuoteFetcherTest.kt new file mode 100644 index 0000000000..b35a5c6f06 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt @@ -0,0 +1,143 @@ +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.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.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.tokens.model.CryptoCurrency +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiQuoteFetcherTest { + + private val tangemTechApi = mockk(relaxed = true) + private val appCurrencyResponseStore = mockk(relaxed = true) + private val quotesStore = mockk(relaxed = true) + + private val fetcher = DefaultMultiQuoteFetcher( + tangemTechApi = tangemTechApi, + appCurrencyResponseStore = appCurrencyResponseStore, + quotesStore = quotesStore, + ) + + @Test + fun `fetch quotes successfully`() = runTest { + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + + coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency + + val coinIds = "BTC,ETH" + coEvery { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + } returns ApiResponse.Success(successResponse) + + val actual = fetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = params.currenciesIds) + + appCurrencyResponseStore.getSyncOrNull() + + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + + quotesStore.storeActual(values = successResponse.quotes) + } + + coVerify(inverse = true) { + quotesStore.storeError(currenciesIds = any()) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch quotes failure because api request failed`() = runTest { + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + + 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 + + val actual = fetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = params.currenciesIds) + + appCurrencyResponseStore.getSyncOrNull() + + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + + quotesStore.storeError(currenciesIds = params.currenciesIds) + } + + coVerify(inverse = true) { + quotesStore.storeActual(values = any()) + } + + Truth.assertThat(actual.isLeft()).isTrue() + } + + @Test + fun `fetch quotes failure because app currency not found`() = runTest { + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + + coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null + + val actual = fetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = params.currenciesIds) + appCurrencyResponseStore.getSyncOrNull() + quotesStore.storeError(currenciesIds = params.currenciesIds) + } + + coVerify(inverse = true) { + tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) + quotesStore.storeActual(values = any()) + } + + Truth.assertThat(actual.isLeft()).isTrue() + } + + private companion object { + + val currenciesIds = setOf( + CryptoCurrency.RawID(value = "BTC"), + CryptoCurrency.RawID(value = "ETH"), + ) + + 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), + "ETH" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.TEN), + ), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index 66f0ea8b64..bf7779378a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -8,7 +8,7 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse /** * Adapter to replace unsupported currencies for quotes request if it necessary */ -internal class QuotesUnsupportedCurrenciesIdAdapter { +class QuotesUnsupportedCurrenciesIdAdapter { /** * Replaces unsupported currencies id to it replacements for request 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/MultiQuoteFetcher.kt new file mode 100644 index 0000000000..4520ad1fd6 --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.quotes.multi + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Fetcher of quotes + * +[REDACTED_AUTHOR] + */ +interface MultiQuoteFetcher : FlowFetcher { + + data class Params(val currenciesIds: Set) +} \ No newline at end of file From dfaf4e4e779b58845a1656467afd6ee921afb94f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Apr 2025 14:10:50 +0700 Subject: [PATCH 10/72] Updated on 2026-08-14 --- data/wallet-connect/build.gradle.kts | 7 + .../di/WalletConnectDataModule.kt | 5 +- .../network/ethereum/WcEthNetwork.kt | 5 +- .../request/DefaultWcRequestService.kt | 2 +- .../respond/DefaultWcRespondService.kt | 16 +- .../walletconnect/respond/WcRespondService.kt | 3 +- .../walletconnect/sign/BaseWcSignUseCase.kt | 65 +++++ .../walletconnect/sign/SignStateConverter.kt | 28 ++ .../sign/WcSignUseCaseDelegate.kt | 91 +++++++ .../WcSignUseCaseDelegateTest.kt | 254 ++++++++++++++++++ .../usecase/WcSimpleSignUseCase.kt | 25 -- .../domain/walletconnect/usecase/WcUseCase.kt | 10 +- .../ethereum/EthPersonalSignUseCase.kt | 72 ----- .../walletconnect/usecase/sign/WcSignState.kt | 14 + .../usecase/sign/WcSignUseCase.kt | 20 ++ gradle/dependencies.toml | 2 + 16 files changed, 511 insertions(+), 108 deletions(-) rename {domain/wallet-connect/src/main/kotlin/com/tangem/domain => data/wallet-connect/src/main/kotlin/com/tangem/data}/walletconnect/respond/WcRespondService.kt (72%) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/SignStateConverter.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt create mode 100644 data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt delete mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt delete mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignState.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignUseCase.kt diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index 159348516a..0aa064eccb 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { /* Tangem libraries */ implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) /* Reown - WalletConnect */ implementation(deps.reownCore) { @@ -47,4 +48,10 @@ dependencies { /* Other */ implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) + + /* Tests */ + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.turbine) } \ No newline at end of file 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 edab0d12af..e36fcd3b18 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 @@ -14,6 +14,7 @@ import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase import com.tangem.data.walletconnect.request.DefaultWcRequestService import com.tangem.data.walletconnect.request.WcMethodHandler import com.tangem.data.walletconnect.respond.DefaultWcRespondService +import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.datasource.di.SdkMoshi @@ -25,7 +26,6 @@ import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsReposit import com.tangem.domain.walletconnect.repository.WalletConnectRepository import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.request.WcRequestService -import com.tangem.domain.walletconnect.respond.WcRespondService import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -131,9 +131,8 @@ internal object WalletConnectDataModule { @Provides @Singleton - fun wcEthNetwork(@SdkMoshi moshi: Moshi, respondService: WcRespondService): WcEthNetwork = WcEthNetwork( + fun wcEthNetwork(@SdkMoshi moshi: Moshi): WcEthNetwork = WcEthNetwork( moshi = moshi, - respondService = respondService, ) @Provides diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index 69407681e0..ffe057712c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -8,10 +8,8 @@ import com.tangem.data.walletconnect.request.WcMethodHandler import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.WcRequest -import com.tangem.domain.walletconnect.respond.WcRespondService import com.tangem.domain.walletconnect.usecase.WcUseCase import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider -import com.tangem.domain.walletconnect.usecase.ethereum.EthPersonalSignUseCase import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod.SignMessage import kotlinx.coroutines.channels.Channel @@ -19,7 +17,6 @@ import kotlinx.coroutines.flow.receiveAsFlow internal class WcEthNetwork( private val moshi: Moshi, - private val respondService: WcRespondService, ) : WcMethodHandler, WcUseCasesFlowProvider, WcNamespaceConverter { private val _useCases: Channel = Channel(Channel.BUFFERED) @@ -46,7 +43,7 @@ internal class WcEthNetwork( override fun handle(wcRequest: WcRequest) { wcRequest as WcRequest val useCase = when (wcRequest.method) { - is SignMessage -> EthPersonalSignUseCase(wcRequest as WcRequest, respondService) + is SignMessage -> TODO() } _useCases.trySend(useCase) } 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 99f06b1f7a..08839fa987 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,13 +1,13 @@ 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.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkSessionRequestConverter import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.WcRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.request.WcRequestService -import com.tangem.domain.walletconnect.respond.WcRespondService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt index 7381e9b548..025e9834b2 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt @@ -6,7 +6,6 @@ import arrow.core.right import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest -import com.tangem.domain.walletconnect.respond.WcRespondService import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume @@ -50,4 +49,19 @@ internal class DefaultWcRespondService : WcRespondService { }, ) } + + override fun rejectRequestNonBlock(request: WcSdkSessionRequest, message: String) { + WalletKit.respondSessionRequest( + params = Wallet.Params.SessionRequestResponse( + sessionTopic = request.topic, + jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError( + id = request.request.id, + code = 0, + message = message, + ), + ), + onSuccess = {}, + onError = {}, + ) + } } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt similarity index 72% rename from domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt rename to data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt index c74ce0e017..0eb8948d59 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.walletconnect.respond +package com.tangem.data.walletconnect.respond import arrow.core.Either import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest @@ -6,4 +6,5 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest interface WcRespondService { suspend fun respond(request: WcSdkSessionRequest, response: String): Either suspend fun rejectRequest(request: WcSdkSessionRequest, message: String = ""): Either + fun rejectRequestNonBlock(request: WcSdkSessionRequest, message: String = "") } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt new file mode 100644 index 0000000000..dd432f1df6 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt @@ -0,0 +1,65 @@ +package com.tangem.data.walletconnect.sign + +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.domain.walletconnect.usecase.WcMethodUseCase +import com.tangem.domain.walletconnect.usecase.sign.WcSignState +import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase +import kotlinx.coroutines.flow.FlowCollector + +internal abstract class BaseWcSignUseCase : + WcMethodUseCase, + WcSignUseCase.FinalAction, + FinalActionCollector, + MiddleActionCollector { + + abstract val respondService: WcRespondService + + abstract val context: WcMethodUseCaseContext + override val session: WcSession get() = context.session + override val rawSdkRequest: WcSdkSessionRequest get() = context.rawSdkRequest + + protected val delegate by lazy { + WcSignUseCaseDelegate( + finalActionCollector = this, + middleActionCollector = this, + ) + } + + override val onCancel: suspend (currentState: WcSignState) -> Unit = { + defaultReject() + } + + override fun sign() = delegate.sign() + override fun cancel() = delegate.cancel() + protected fun middleAction(action: MiddleAction) = delegate.middleAction(action) + + protected fun defaultReject() { + respondService.rejectRequestNonBlock(rawSdkRequest) + } +} + +internal interface MiddleActionCollector { + + val onMiddleAction: OnMiddle get() = { _, _ -> } +} + +internal interface FinalActionCollector { + + val onSign: OnSign get() = {} + + val onCancel: OnCancel get() = {} +} + +internal class WcMethodUseCaseContext( + val session: WcSession, + val rawSdkRequest: WcSdkSessionRequest, +) + +internal typealias OnSign = + suspend FlowCollector>.(state: WcSignState) -> Unit +internal typealias OnCancel = + suspend (currentState: WcSignState) -> Unit +internal typealias OnMiddle = + suspend FlowCollector.(currentState: WcSignState, middleAction: MiddleAction) -> Unit \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/SignStateConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/SignStateConverter.kt new file mode 100644 index 0000000000..f77af76789 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/SignStateConverter.kt @@ -0,0 +1,28 @@ +package com.tangem.data.walletconnect.sign + +import arrow.core.Either +import com.tangem.domain.walletconnect.usecase.sign.WcSignState +import com.tangem.domain.walletconnect.usecase.sign.WcSignStep + +object SignStateConverter { + + internal fun preSign(signModel: M) = WcSignState(signModel, WcSignStep.PreSign) + internal fun signing(signModel: M) = WcSignState(signModel, WcSignStep.Signing) + internal fun result(result: Either, signModel: M) = + WcSignState(signModel, WcSignStep.Result(result)) + + internal fun WcSignState.toPreSign(signModel: M = this.signModel) = copy( + signModel = signModel, + domainStep = WcSignStep.PreSign, + ) + + internal fun WcSignState.toSigning(signModel: M = this.signModel) = copy( + domainStep = WcSignStep.Signing, + signModel = signModel, + ) + + internal fun WcSignState.toResult(result: Either, signModel: M = this.signModel) = copy( + domainStep = WcSignStep.Result(result), + signModel = signModel, + ) +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt new file mode 100644 index 0000000000..c84bc39ac7 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt @@ -0,0 +1,91 @@ +package com.tangem.data.walletconnect.sign + +import arrow.core.left +import com.tangem.data.walletconnect.sign.SignStateConverter.toPreSign +import com.tangem.data.walletconnect.sign.SignStateConverter.toResult +import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning +import com.tangem.domain.walletconnect.usecase.sign.WcSignState +import com.tangem.domain.walletconnect.usecase.sign.WcSignStep +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +internal class WcSignUseCaseDelegate( + private val finalActionCollector: FinalActionCollector, + private val middleActionCollector: MiddleActionCollector, +) : FinalActionCollector by finalActionCollector, + MiddleActionCollector by middleActionCollector { + + private val middleActionsChannel = Channel() + private val finalActionsChannel = Channel() + + fun cancel() { + finalActionsChannel.trySend(Action.Cancel) + } + + fun sign() { + finalActionsChannel.trySend(Action.Sign) + } + + fun middleAction(action: MiddleAction) { + middleActionsChannel.trySend(action) + } + + operator fun invoke(initModel: SignModel) = channelFlow { + val state = MutableStateFlow(WcSignState(initModel, WcSignStep.PreSign)) + + state + .onEach { newState -> channel.send(newState) } + .launchIn(this) + + fun listenMiddle() = middleActionsChannel.receiveAsFlow() + .buffer() + .transform { middleActions -> this.onMiddleAction(state.value, middleActions) } + .onEach { updatedModel -> state.update { it.toPreSign(updatedModel) } } + .launchIn(this) + + var listenMiddleJob: Job = listenMiddle() + + fun signFlow() = flow { onSign(state.updateAndGet { it.toSigning() }) } + .onEach { newState -> state.update { newState } } + .catch { exception -> + val errorResult = state.value.toResult(exception.left()) + state.update { errorResult } + } + + var signJob: Job? = null + + finalActionsChannel.receiveAsFlow() + .transformLatest { finalAction -> + when (finalAction) { + Action.Cancel -> { + onCancel.invoke(state.value) + channel.close() + } + Action.Sign -> { + val isSigningNow = signJob?.isActive == true + if (isSigningNow) return@transformLatest + listenMiddleJob.cancel() + signJob = launch { + signFlow().collect() + listenMiddleJob = listenMiddle() + } + } + } + } + .launchIn(this) + + /** + * keep flow running to attempt re-signing after an error + * or do something after a successful sign + */ + awaitClose() + } + + sealed interface Action { + data object Cancel : Action + data object Sign : Action + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt new file mode 100644 index 0000000000..e80bd9a4f8 --- /dev/null +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt @@ -0,0 +1,254 @@ +package com.tangem.domain.walletconnect + +import app.cash.turbine.test +import arrow.core.left +import arrow.core.right +import com.tangem.data.walletconnect.sign.FinalActionCollector +import com.tangem.data.walletconnect.sign.MiddleActionCollector +import com.tangem.data.walletconnect.sign.SignStateConverter.toResult +import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning +import com.tangem.data.walletconnect.sign.WcSignUseCaseDelegate +import com.tangem.domain.walletconnect.usecase.sign.WcSignState +import com.tangem.domain.walletconnect.usecase.sign.WcSignStep +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +internal class WcSignUseCaseDelegateTest { + + private val middleActionCollector = mockk>() + private val finalActionCollector = mockk>() + private val useCase = WcSignUseCaseDelegate( + finalActionCollector = finalActionCollector, + middleActionCollector = middleActionCollector, + ) + private val initSignModel = TestSignModel() + + private val initState = WcSignState(initSignModel, WcSignStep.PreSign) + private val signing = initState.toSigning() + private val result = signing.toResult(Unit.right()) + private val testException = RuntimeException("test") + + private val successSign: suspend FlowCollector>.( + currentState: WcSignState, + ) -> Unit = { state -> + delay(2) + emit(state.toResult(Unit.right())) + } + + private val failedSign: suspend FlowCollector>.( + currentState: WcSignState, + ) -> Unit + get() = { state -> + delay(2) + emit(state.toResult(testException.left())) + } + + @Before + fun setup() { + every { middleActionCollector.onMiddleAction } returns { _, _ -> } + every { finalActionCollector.onSign } returns { } + every { finalActionCollector.onCancel } returns { } + } + + @Test + fun `invoke and keep flow running`() = runTest { + every { finalActionCollector.onSign } returns successSign + + useCase.invoke(initSignModel).test { + assertEquals(initState, awaitItem()) + expectNoEvents() + } + } + + @Test + fun `success sign, keep flow running`() = runTest { + every { finalActionCollector.onSign } returns successSign + + useCase.invoke(initModel = initSignModel).test { + assertEquals(initState, awaitItem()) + useCase.sign() + assertEquals(signing, awaitItem()) + assertEquals(result, awaitItem()) + expectNoEvents() + } + } + + @Test + fun `failed sign, keep flow running`() = runTest { + val failedResult = signing.toResult(testException.left()) + every { finalActionCollector.onSign } returns failedSign + + useCase.invoke(initSignModel).test { + assertEquals(initState, awaitItem()) + useCase.sign() + assertEquals(signing, awaitItem()) + assertEquals(failedResult, awaitItem()) + expectNoEvents() + } + } + + @Test + fun `failed sign and catch unknown exception`() = runTest { + val exception = RuntimeException("asd") + val expectedErrorState = signing.toResult(exception.left()) + every { finalActionCollector.onSign } returns { + delay(2) + throw exception + } + + useCase.invoke(initSignModel).test { + assertEquals(initState, awaitItem()) + useCase.sign() + assertEquals(signing, awaitItem()) + assertEquals(expectedErrorState, awaitItem()) + expectNoEvents() + } + } + + @Test + fun `interrupt signing and complete flow on cancel call`() = runTest { + every { finalActionCollector.onSign } returns { + delay(5) + emit(result) + } + + useCase.invoke(initSignModel).test { + assertEquals(initState, awaitItem()) + useCase.sign() + assertEquals(signing, awaitItem()) + delay(2) + useCase.cancel() + awaitComplete() + } + } + + @Test + fun `ignore multi time sign call till signed`() = runTest { + var count = 0 + val startLoading = WcSignState(TestSignModel("startLoading 1"), WcSignStep.Signing) + val startLoading2 = WcSignState(TestSignModel("startLoading 2"), WcSignStep.Signing) + val expectedSignResult = result + + every { finalActionCollector.onSign } returns { + // should emit single time in this test + emit(if (count % 2 == 0) startLoading else startLoading2) + count = count.inc() + delay(10) + emit(expectedSignResult) + } + + useCase.invoke(initSignModel).test { + useCase.sign() + delay(2) + assertEquals(startLoading, expectMostRecentItem()) + + // should ignore + useCase.sign() + delay(2) + expectNoEvents() + + // should ignore + useCase.sign() + expectNoEvents() + + delay(8) + assertEquals(expectedSignResult, expectMostRecentItem()) + expectNoEvents() + } + } + + @Test + fun `ignore middle actions while signing, on failed collect middle actions again`() = runTest { + val firstTextMode = TestSignModel(TestMiddleAction.One().newTestStr) + val firstMiddleUpdate = WcSignState( + signModel = firstTextMode, + domainStep = WcSignStep.PreSign, + ) + val startLoading = firstMiddleUpdate.toSigning() + val failedSign = startLoading.toResult(testException.left()) + val thirdMiddleUpdate = WcSignState( + signModel = TestSignModel(TestMiddleAction.Three().newTestStr), + domainStep = WcSignStep.PreSign, + ) + + every { finalActionCollector.onSign } returns { + delay(6) + emit(failedSign) + } + + every { middleActionCollector.onMiddleAction } returns { currentState, middleAction -> + emit(currentState.signModel.copy(testStr = middleAction.newTestStr)) + } + + useCase.invoke(initSignModel).test { + delay(2) + useCase.middleAction(TestMiddleAction.One()) + assertEquals(firstMiddleUpdate, expectMostRecentItem()) + + useCase.sign() + assertEquals(startLoading, awaitItem()) + + // should ignore + delay(2) + useCase.middleAction(TestMiddleAction.Two()) + + delay(3) + assertEquals(failedSign, awaitItem()) + + // continue listen + delay(2) + useCase.middleAction(TestMiddleAction.Three()) + assertEquals(thirdMiddleUpdate, awaitItem()) + } + } + + @Test + fun `buffered middle actions and drop on sign call`() = runTest { + val firstTextMode = TestSignModel(TestMiddleAction.One().newTestStr) + val expectedFirst = initState.copy(signModel = firstTextMode) + val expectedSecond = initState.copy(signModel = TestSignModel(TestMiddleAction.Two().newTestStr)) + + every { finalActionCollector.onSign } returns successSign + every { middleActionCollector.onMiddleAction } returns { currentState, middleAction -> + emit(currentState.signModel.copy(testStr = middleAction.newTestStr)) + delay(4) + } + + useCase.invoke(initSignModel).test { + assertEquals(initState, awaitItem()) + useCase.middleAction(TestMiddleAction.One()) + useCase.middleAction(TestMiddleAction.Two()) + // must be dropped + useCase.middleAction(TestMiddleAction.Three()) + + // 0 - 4 -> "one" is emitted + // 4 - 8 -> "two" is emitted + // 8 - 12 -> "Signing" is emitted, "three" ignored + delay(2) + assertEquals(expectedFirst, awaitItem()) + delay(4) + assertEquals(expectedSecond, awaitItem()) + + useCase.sign() + delay(4) + assertEquals(expectedSecond.toSigning(), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + internal data class TestSignModel(val testStr: String = "testStr") + + internal sealed interface TestMiddleAction { + val newTestStr: String + + data class One(override val newTestStr: String = "Middle Action One") : TestMiddleAction + data class Two(override val newTestStr: String = "Middle Action Two") : TestMiddleAction + data class Three(override val newTestStr: String = "Middle Action Three") : TestMiddleAction + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt deleted file mode 100644 index e0bb5239a4..0000000000 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.domain.walletconnect.usecase - -import arrow.core.Either -import kotlinx.coroutines.flow.Flow - -interface WcSimpleSignUseCase : WcUseCase { - - fun signFlow(initModel: SignModel): Flow> - - fun action(action: MiddleAction) - - fun cancel() - fun sign(toSign: SignModel) - - sealed interface State { - val model: SignModel - - data class PreSign(override val model: SignModel) : State - data class Signing(override val model: SignModel) : State - data class Result( - val result: Either, - override val model: SignModel, - ) : State - } -} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt index a2efe639cd..6ff2c38c73 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt @@ -1,3 +1,11 @@ package com.tangem.domain.walletconnect.usecase -interface WcUseCase \ No newline at end of file +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest + +interface WcUseCase + +interface WcMethodUseCase : WcUseCase { + val session: WcSession + val rawSdkRequest: WcSdkSessionRequest +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt deleted file mode 100644 index 5e86f84006..0000000000 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.domain.walletconnect.usecase.ethereum - -import arrow.core.Either -import com.tangem.domain.walletconnect.model.WcRequest -import com.tangem.domain.walletconnect.respond.WcRespondService -import com.tangem.domain.walletconnect.usecase.WcUseCase -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* - -class EthPersonalSignUseCase( - private val wcRequest: WcRequest, - private val respondService: WcRespondService, -) : WcUseCase { - - private val onCallTerminalAction = Channel() - - fun signFlow(): Flow = flow { - val model = SignModel(wcRequest.method.raw) - emit(State.PreSign(model)) - - when (val action = onCallTerminalAction.receiveAsFlow().first()) { - TerminalAction.Cancel -> { - val result = respondService.rejectRequest(wcRequest.rawSdkRequest) - emit(State.Result(result, model)) - } - is TerminalAction.Sign -> { - emit(State.Signing(action.toSign)) - val signed = signAndPrepareForSend() - val result = - if (signed != null) { - respondService.respond(wcRequest.rawSdkRequest, signed) - } else { - respondService.rejectRequest(wcRequest.rawSdkRequest) - } - emit(State.Result(result, model)) - } - } - } - - @Suppress("FunctionOnlyReturningConstant") // todo(wc) remove later - private suspend fun signAndPrepareForSend(): String? { - return null // todo(wc) - } - - fun sign(toSign: SignModel) { - onCallTerminalAction.trySend(TerminalAction.Sign(toSign)) - } - - fun cancel() { - onCallTerminalAction.trySend(TerminalAction.Cancel) - } - - sealed interface TerminalAction { - data class Sign(val toSign: SignModel) : TerminalAction - data object Cancel : TerminalAction - } - - sealed interface State { - val model: SignModel - - data class PreSign(override val model: SignModel) : State - data class Signing(override val model: SignModel) : State - data class Result( - val result: Either, - override val model: SignModel, - ) : State - } - - data class SignModel( - val raw: List, - ) -} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignState.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignState.kt new file mode 100644 index 0000000000..047148fade --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignState.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.walletconnect.usecase.sign + +import arrow.core.Either + +data class WcSignState( + val signModel: SignModel, + val domainStep: WcSignStep, +) + +sealed interface WcSignStep { + data object PreSign : WcSignStep + data object Signing : WcSignStep + data class Result(val result: Either) : WcSignStep +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignUseCase.kt new file mode 100644 index 0000000000..a0db5aa0e7 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.walletconnect.usecase.sign + +import com.tangem.domain.walletconnect.usecase.WcMethodUseCase +import kotlinx.coroutines.flow.Flow + +interface WcSignUseCase : WcMethodUseCase { + + interface FinalAction { + fun cancel() + fun sign() + } + + interface SimpleRun { + operator fun invoke(): Flow> + } + + interface ArgsRun { + operator fun invoke(args: Args): Flow> + } +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 8fbee2f5e1..3f1defd17d 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -102,6 +102,7 @@ espresso-intents = "3.5.1" junit = "4.13.2" 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" @@ -199,6 +200,7 @@ test-junit = { module = "junit:junit", version.ref = "junit" } test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitAndroidExt" } 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" } From ce4df5bee65dd6d70dfca72cdf1bc4121444b299 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Apr 2025 12:32:41 +0500 Subject: [PATCH 11/72] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 14 +++ .../com/tangem/common/routing/AppRoute.kt | 5 + core/res/src/main/res/values/strings.xml | 1 + .../nft/component/NFTAssetTraitsComponent.kt | 14 +++ .../nft/details/entity/NFTDetailsUM.kt | 2 +- .../entity/NFTInfoBottomSheetConfig.kt | 9 ++ .../nft/details/model/NFTDetailsModel.kt | 11 ++- .../features/nft/details/ui/NFTDetails.kt | 14 ++- .../nft/details/ui/NFTDetailsAsset.kt | 6 +- .../ui/bottomsheet/NFTInfoBottomSheet.kt | 73 ++++++++++++++ .../features/nft/di/NFTFeatureModule.kt | 12 +++ .../traits/DefaultNFTAssetTraitsComponent.kt | 37 ++++++++ .../nft/traits/entity/NFTAssetTraitUM.kt | 7 ++ .../nft/traits/entity/NFTAssetTraitsUM.kt | 8 ++ .../nft/traits/model/NFTAssetTraitsModel.kt | 48 ++++++++++ .../features/nft/traits/ui/NFTAssetTrait.kt | 29 ++++++ .../features/nft/traits/ui/NFTAssetTraits.kt | 41 ++++++++ .../nft/traits/ui/NFTAssetTraitsContent.kt | 94 +++++++++++++++++++ 18 files changed, 417 insertions(+), 8 deletions(-) create mode 100644 features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTAssetTraitsComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/bottomsheet/NFTInfoBottomSheet.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/DefaultNFTAssetTraitsComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/entity/NFTAssetTraitUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/entity/NFTAssetTraitsUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/model/NFTAssetTraitsModel.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTrait.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraitsContent.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index c6d5caa0da..005557d07b 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -15,6 +15,7 @@ import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.nft.component.NFTCollectionsComponent import com.tangem.features.nft.component.NFTDetailsComponent import com.tangem.features.nft.component.NFTReceiveComponent +import com.tangem.features.nft.component.NFTAssetTraitsComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.PushNotificationsComponent @@ -87,6 +88,7 @@ internal class ChildFactory @Inject constructor( private val nftCollectionsComponentFactory: NFTCollectionsComponent.Factory, private val nftReceiveComponentFactory: NFTReceiveComponent.Factory, private val nftDetailsComponentFactory: NFTDetailsComponent.Factory, + private val nftAssetTraitsComponentFactory: NFTAssetTraitsComponent.Factory, private val nftSendComponentFactory: NFTSendComponent.Factory, private val testerRouter: TesterRouter, private val routingFeatureToggles: RoutingFeatureToggles, @@ -429,6 +431,12 @@ internal class ChildFactory @Inject constructor( ), componentFactory = nftDetailsComponentFactory, ) + is AppRoute.NFTAssetTraits -> + createComponentChild( + context = context, + params = NFTAssetTraitsComponent.Params(nftAsset = route.nftAsset), + componentFactory = nftAssetTraitsComponentFactory, + ) is AppRoute.NFTSend -> { createComponentChild( context = context, @@ -796,6 +804,12 @@ internal class ChildFactory @Inject constructor( ), componentFactory = nftDetailsComponentFactory, ) + is AppRoute.NFTAssetTraits -> + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = NFTAssetTraitsComponent.Params(nftAsset = route.nftAsset), + componentFactory = nftAssetTraitsComponentFactory, + ) is AppRoute.NFTSend -> { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 07d02c6a0a..a21f44aa02 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -297,6 +297,11 @@ sealed class AppRoute(val path: String) : Route { val collectionName: String, ) : AppRoute(path = "/nft_details/${userWalletId.stringValue}/${nftAsset.collectionId}/${nftAsset.id.stringValue}") + @Serializable + data class NFTAssetTraits( + val nftAsset: NFTAsset, + ) : AppRoute(path = "/nft_traits/${nftAsset.collectionId}/${nftAsset.id.stringValue}") + @Serializable data class NFTSend( val userWalletId: UserWalletId, diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 74f931332b..b2b7ab6c75 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -532,6 +532,7 @@ Tap here to receive first NFT NFT collections Unable to load the data + Traits Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTAssetTraitsComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTAssetTraitsComponent.kt new file mode 100644 index 0000000000..2be81fbcef --- /dev/null +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTAssetTraitsComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.nft.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.nft.models.NFTAsset + +interface NFTAssetTraitsComponent : ComposableContentComponent { + + data class Params( + val nftAsset: NFTAsset, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt index 55df42f2df..67955452f9 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt @@ -6,7 +6,7 @@ internal data class NFTDetailsUM( val nftAsset: NFTAssetUM, val onBackClick: () -> Unit, val onReadMoreClick: () -> Unit, - val onSeeAllClick: () -> Unit, + val onSeeAllTraitsClick: () -> Unit, val onExploreClick: () -> Unit, val onSendClick: () -> Unit, val bottomSheetConfig: TangemBottomSheetConfig?, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt new file mode 100644 index 0000000000..3cb7f5273c --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.features.nft.details.entity + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference + +internal data class NFTInfoBottomSheetConfig( + val title: TextReference, + val text: TextReference, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index 77fc4b7e96..446c289c09 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.nft.details.model +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -36,7 +37,7 @@ internal class NFTDetailsModel @Inject constructor( ), onBackClick = ::navigateBack, onReadMoreClick = ::onReadMoreClick, - onSeeAllClick = ::onSeeAllClick, + onSeeAllTraitsClick = ::onSeeAllTraitsClick, onExploreClick = ::onExploreClick, onSendClick = ::onSendClick, bottomSheetConfig = null, @@ -47,8 +48,12 @@ internal class NFTDetailsModel @Inject constructor( // TODO implement } - private fun onSeeAllClick() { - // TODO implement + private fun onSeeAllTraitsClick() { + router.push( + AppRoute.NFTAssetTraits( + nftAsset = params.nftAsset, + ), + ) } private fun onExploreClick() { diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt index 31af37f744..1fba75b717 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt @@ -11,9 +11,12 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.nft.details.entity.NFTDetailsUM +import com.tangem.features.nft.details.entity.NFTInfoBottomSheetConfig +import com.tangem.features.nft.details.ui.bottomsheet.NFTInfoBottomSheet import com.tangem.features.nft.impl.R @Composable @@ -37,11 +40,12 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { NFTDetailsAsset( state = state.nftAsset, onReadMoreClick = state.onReadMoreClick, - onSeeAllClick = state.onSeeAllClick, + onSeeAllTraitsClick = state.onSeeAllTraitsClick, onExploreClick = state.onExploreClick, modifier = Modifier .padding(innerPadding), ) + ShowBottomSheet(state.bottomSheetConfig) }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { @@ -54,4 +58,12 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { ) }, ) +} + +@Composable +fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { + if (bottomSheetConfig == null) return + when (bottomSheetConfig.content) { + is NFTInfoBottomSheetConfig -> NFTInfoBottomSheet(bottomSheetConfig) + } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt index b3806888f4..b077835ec8 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt @@ -24,7 +24,7 @@ import kotlinx.collections.immutable.persistentListOf internal fun NFTDetailsAsset( state: NFTAssetUM, onReadMoreClick: () -> Unit, - onSeeAllClick: () -> Unit, + onSeeAllTraitsClick: () -> Unit, onExploreClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -72,7 +72,7 @@ internal fun NFTDetailsAsset( NFTBlocksGroupAction( text = resourceReference(R.string.common_see_all), startIcon = { }, - onClick = onSeeAllClick, + onClick = onSeeAllTraitsClick, ) }, ) @@ -103,7 +103,7 @@ private fun Preview_NFTDetailsAssetAsset(@PreviewParameter(NFTAssetProvider::cla NFTDetailsAsset( state = state, onReadMoreClick = { }, - onSeeAllClick = { }, + onSeeAllTraitsClick = { }, onExploreClick = { }, ) } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/bottomsheet/NFTInfoBottomSheet.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/bottomsheet/NFTInfoBottomSheet.kt new file mode 100644 index 0000000000..979a5e989f --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/bottomsheet/NFTInfoBottomSheet.kt @@ -0,0 +1,73 @@ +package com.tangem.features.nft.details.ui.bottomsheet + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle +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 +import com.tangem.features.nft.details.entity.NFTInfoBottomSheetConfig + +@Composable +fun NFTInfoBottomSheet(config: TangemBottomSheetConfig) { + val scrollState = rememberScrollState() + + TangemBottomSheet( + config = config, + title = { content -> + TangemBottomSheetTitle(title = content.title) + }, + ) { content -> + Column( + modifier = Modifier.verticalScroll(scrollState), + ) { + Text( + text = content.text.resolveReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) + } + } +} + +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_StakingInfoBottomSheet() { + TangemThemePreview { + NFTInfoBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = NFTInfoBottomSheetConfig( + title = stringReference("Title"), + text = stringReference( + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius neque vel ligula + tincidunt, nec faucibus nulla ultricies. Maecenas euismod arcu in nunc volutpat, + at bibendum eros lacinia. Proin hendrerit massa non velit congue, + in volutpat nisi consequat. Sed vitae justo nec orci tincidunt malesuada. + Nullam feugiat purus vel lectus efficitur, vel fringilla urna volutpat. + Donec sagittis enim in metus lacinia, vel tempor nunc bibendum. + """.trimIndent(), + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt index 0d8889e2e8..af79399a92 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt @@ -10,11 +10,14 @@ import com.tangem.features.nft.component.NFTCollectionsComponent import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.nft.component.NFTDetailsComponent import com.tangem.features.nft.component.NFTReceiveComponent +import com.tangem.features.nft.component.NFTAssetTraitsComponent import com.tangem.features.nft.details.DefaultNFTDetailsComponent import com.tangem.features.nft.details.block.DefaultNFTDetailsBlockComponent import com.tangem.features.nft.details.model.NFTDetailsModel import com.tangem.features.nft.receive.DefaultNFTReceiveComponent import com.tangem.features.nft.receive.model.NFTReceiveModel +import com.tangem.features.nft.traits.DefaultNFTAssetTraitsComponent +import com.tangem.features.nft.traits.model.NFTAssetTraitsModel import dagger.Binds import dagger.Module import dagger.Provides @@ -72,4 +75,13 @@ internal interface NFTFeatureModuleBinds { fun bindNFTDetailsBlockComponentFactory( impl: DefaultNFTDetailsBlockComponent.Factory, ): NFTDetailsBlockComponent.Factory + + @Binds + @Singleton + fun bindNFTTraitsComponentFactory(impl: DefaultNFTAssetTraitsComponent.Factory): NFTAssetTraitsComponent.Factory + + @Binds + @IntoMap + @ClassKey(NFTAssetTraitsModel::class) + fun bindNFTTraitsModel(model: NFTAssetTraitsModel): Model } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/DefaultNFTAssetTraitsComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/DefaultNFTAssetTraitsComponent.kt new file mode 100644 index 0000000000..ce9f6af262 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/DefaultNFTAssetTraitsComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.nft.traits + +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.nft.component.NFTAssetTraitsComponent +import com.tangem.features.nft.traits.ui.NFTAssetTraits +import com.tangem.features.nft.traits.model.NFTAssetTraitsModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultNFTAssetTraitsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: NFTAssetTraitsComponent.Params, +) : NFTAssetTraitsComponent, AppComponentContext by context { + + private val model: NFTAssetTraitsModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + NFTAssetTraits(state) + } + + @AssistedFactory + interface Factory : NFTAssetTraitsComponent.Factory { + override fun create( + context: AppComponentContext, + params: NFTAssetTraitsComponent.Params, + ): DefaultNFTAssetTraitsComponent + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/entity/NFTAssetTraitUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/entity/NFTAssetTraitUM.kt new file mode 100644 index 0000000000..1837fa3f30 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/entity/NFTAssetTraitUM.kt @@ -0,0 +1,7 @@ +package com.tangem.features.nft.traits.entity + +data class NFTAssetTraitUM( + val id: String, + val name: String, + val value: String, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/entity/NFTAssetTraitsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/entity/NFTAssetTraitsUM.kt new file mode 100644 index 0000000000..11b3fd9eea --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/entity/NFTAssetTraitsUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.nft.traits.entity + +import kotlinx.collections.immutable.ImmutableList + +data class NFTAssetTraitsUM( + val onBackClick: () -> Unit, + val traits: ImmutableList, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/model/NFTAssetTraitsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/model/NFTAssetTraitsModel.kt new file mode 100644 index 0000000000..73c030f059 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/model/NFTAssetTraitsModel.kt @@ -0,0 +1,48 @@ +package com.tangem.features.nft.traits.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.core.decompose.navigation.Router +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.features.nft.component.NFTAssetTraitsComponent +import com.tangem.features.nft.traits.entity.NFTAssetTraitUM +import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@ModelScoped +internal class NFTAssetTraitsModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: NFTAssetTraitsComponent.Params = paramsContainer.require() + + val state: StateFlow get() = _state + + private val _state = MutableStateFlow( + value = NFTAssetTraitsUM( + traits = params.nftAsset.transform(), + onBackClick = ::navigateBack, + ), + ) + + private fun NFTAsset.transform(): ImmutableList = this + .traits + .mapIndexed { index, trait -> + NFTAssetTraitUM( + id = index.toString(), + name = trait.name, + value = trait.value, + ) + }.toPersistentList() + + private fun navigateBack() { + router.pop() + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTrait.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTrait.kt new file mode 100644 index 0000000000..dc9135f37c --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTrait.kt @@ -0,0 +1,29 @@ +package com.tangem.features.nft.traits.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.nft.traits.entity.NFTAssetTraitUM + +@Composable +internal fun NFTAssetTrait(state: NFTAssetTraitUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + ) { + Text( + text = state.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4), + text = state.value, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt new file mode 100644 index 0000000000..f6e27276f2 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt @@ -0,0 +1,41 @@ +package com.tangem.features.nft.traits.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.nft.impl.R +import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM + +@Composable +internal fun NFTAssetTraits(state: NFTAssetTraitsUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.onBackClick) + + Scaffold( + modifier = modifier, + containerColor = TangemTheme.colors.background.secondary, + topBar = { + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + startButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_back_24, + onIconClicked = state.onBackClick, + ), + title = stringResourceSafe(R.string.nft_traits_title), + ) + }, + content = { innerPadding -> + NFTAssetTraitsContent( + modifier = Modifier + .padding(innerPadding), + state = state, + ) + }, + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraitsContent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraitsContent.kt new file mode 100644 index 0000000000..c51f428b21 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraitsContent.kt @@ -0,0 +1,94 @@ +package com.tangem.features.nft.traits.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +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.block.information.InformationBlock +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.traits.entity.NFTAssetTraitUM +import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun NFTAssetTraitsContent(state: NFTAssetTraitsUM, modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier + .padding(TangemTheme.dimens.spacing16), + title = null, + contentHorizontalPadding = 0.dp, + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()), + ) { + state.traits.forEach { trait -> + key(trait.id) { + NFTAssetTrait( + modifier = Modifier + .padding( + horizontal = TangemTheme.dimens.spacing12, + vertical = TangemTheme.dimens.spacing8, + ), + state = trait, + ) + } + } + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTDetailsAssetAsset(@PreviewParameter(NFTAssetTraitsProvider::class) state: NFTAssetTraitsUM) { + TangemThemePreview { + NFTAssetTraitsContent( + state = state, + ) + } +} + +private class NFTAssetTraitsProvider : CollectionPreviewParameterProvider( + collection = listOf( + NFTAssetTraitsUM( + traits = persistentListOf( + NFTAssetTraitUM( + id = "1", + name = "Trait 1", + value = "Value", + ), + NFTAssetTraitUM( + id = "2", + name = "Trait 2", + value = "Value", + ), + NFTAssetTraitUM( + id = "3", + name = "Trait 3", + value = "Value", + ), + NFTAssetTraitUM( + id = "4", + name = "Trait 4", + value = "Value", + ), + NFTAssetTraitUM( + id = "5", + name = "Trait 5", + value = "Value", + ), + ), + onBackClick = { }, + ), + ), +) \ No newline at end of file From 6fab6d1ce354e0fbaa9b961d6ae2e77bc2c1abc8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Apr 2025 15:05:06 +0700 Subject: [PATCH 12/72] Updated on 2026-08-14 --- .../di/WalletConnectDataModule.kt | 3 + .../pair/CaipNamespaceDelegate.kt | 6 +- .../pair/DefaultWcPairUseCase.kt | 12 ++-- .../sessions/DefaultWcSessionsManager.kt | 61 +++++++++++-------- .../domain/walletconnect/model/WcSession.kt | 4 +- .../walletconnect/model/WcSessionApprove.kt | 4 +- .../repository/WcSessionsManager.kt | 8 +-- .../usecase/WcSessionsUseCase.kt | 6 +- .../usecase/disconnect/WcDisconnectUseCase.kt | 2 +- 9 files changed, 59 insertions(+), 47 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 e36fcd3b18..ee70b52018 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 @@ -29,6 +29,7 @@ import com.tangem.domain.walletconnect.request.WcRequestService import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -88,6 +89,7 @@ internal object WalletConnectDataModule { store: WalletConnectStore, dispatchers: CoroutineDispatcherProvider, legacyStore: WalletConnectSessionsRepository, + getUserWallet: GetUserWalletUseCase, getWallets: GetWalletsUseCase, ): DefaultWcSessionsManager { val scope = CoroutineScope(SupervisorJob() + dispatchers.io) @@ -96,6 +98,7 @@ internal object WalletConnectDataModule { dispatchers = dispatchers, legacyStore = legacyStore, getWallets = getWallets, + getUserWallet = getUserWallet, scope = scope, ) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt index 98e5e6be32..2361baa676 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt @@ -7,7 +7,7 @@ import com.tangem.data.walletconnect.model.NamespaceKey import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet internal class CaipNamespaceDelegate constructor( private val namespaceConverters: Map, @@ -16,7 +16,7 @@ internal class CaipNamespaceDelegate constructor( suspend fun associate( sessionProposal: Wallet.Model.SessionProposal, - userWalletId: UserWalletId, + userWallet: UserWallet, networks: List, ): Map { val converters = namespaceConverters.values @@ -25,7 +25,7 @@ internal class CaipNamespaceDelegate constructor( networks.map { network -> val blockchain = Blockchain.fromId(network.id.value) - val address = walletManagersFacade.getDefaultAddress(userWalletId, network) + val address = walletManagersFacade.getDefaultAddress(userWallet.walletId, network) val chainId = converters.firstOrNull { it.toCAIP2(blockchain) != null }?.toCAIP2(blockchain) requireNotNull(chainId) requireNotNull(address) 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 65f7b110ee..98dcdf4de2 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 @@ -15,7 +15,7 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -93,8 +93,8 @@ internal class DefaultWcPairUseCase( ).left() is Wallet.Model.SettledSessionResponse.Result -> { - val newSession = settledSession.session.toDomain(sessionForApprove.walletId) - sessionsManager.saveSession(sessionForApprove.walletId, newSession) + val newSession = settledSession.session.toDomain(sessionForApprove.wallet) + sessionsManager.saveSession(newSession) newSession.right() } } @@ -148,7 +148,7 @@ internal class DefaultWcPairUseCase( ): Either { val namespaces = caipNamespaceDelegate.associate( sdkSessionProposal, - sessionForApprove.walletId, + sessionForApprove.wallet, sessionForApprove.network.map { it.network }, ) val sessionApprove = Wallet.Params.SessionApprove( @@ -209,8 +209,8 @@ internal class DefaultWcPairUseCase( } },) - private fun Wallet.Model.Session.toDomain(walletId: UserWalletId): WcSession = WcSession( - userWalletId = walletId, + private fun Wallet.Model.Session.toDomain(wallet: UserWallet): WcSession = WcSession( + wallet = wallet, sdkModel = WcSdkSessionConverter.convert(this), ) 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 90ccec710d..6e31f2783d 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 @@ -12,7 +12,8 @@ import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionDTO import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.repository.WcSessionsManager -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope @@ -29,6 +30,7 @@ internal class DefaultWcSessionsManager constructor( private val store: WalletConnectStore, private val legacyStore: WalletConnectSessionsRepository, private val getWallets: GetWalletsUseCase, + private val getUserWallet: GetUserWalletUseCase, private val dispatchers: CoroutineDispatcherProvider, private val scope: CoroutineScope, ) : WcSessionsManager, WcSdkObserver { @@ -36,20 +38,22 @@ internal class DefaultWcSessionsManager constructor( private val onSessionDelete = Channel(capacity = Channel.BUFFERED) private val oneTimeMigration = MutableStateFlow(true) - override val sessions: Flow>> - get() = store.sessions - .transform { inStore -> + override val sessions: Flow>> + get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } + .transform { pair -> + val (wallets, inStore) = pair + val inSdk: List = WalletKit.getListOfActiveSessions() if (oneTimeMigration.value) { oneTimeMigration.value = false - val someMigrated = migrateLegacyStore(inStore) + val someMigrated = migrateLegacyStore(inStore, inSdk, wallets) if (someMigrated) return@transform // ignore emit, wait next one } - val inSdk: List = WalletKit.getListOfActiveSessions() - val associatedSessions: List = associateWithSdk(inSdk, inStore) + val associatedSessions: List = associate(inSdk, inStore, wallets) val someRemove = removeUnknownSessions(inStore, associatedSessions) if (someRemove) return@transform // ignore emit, wait next one - emit(associatedSessions.groupBy { it.userWalletId }) + emit(associatedSessions.groupBy { it.wallet }) } + .distinctUntilChanged() .flowOn(dispatchers.io) override fun onWcSdkInit() { @@ -57,11 +61,11 @@ internal class DefaultWcSessionsManager constructor( listenOnSessionDelete() } - override suspend fun saveSession(userWalletId: UserWalletId, session: WcSession) { - store.saveSession(WcSessionDTO(session.sdkModel.topic, session.userWalletId)) + override suspend fun saveSession(session: WcSession) { + store.saveSession(WcSessionDTO(session.sdkModel.topic, session.wallet.walletId)) } - override suspend fun removeSession(userWalletId: UserWalletId, session: WcSession): Either { + override suspend fun removeSession(session: WcSession): Either { val topic = session.sdkModel.topic val sdkCall = sdkDisconnectSession(topic) sdkCall.onLeft { return it.left() } @@ -82,7 +86,8 @@ internal class DefaultWcSessionsManager constructor( override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) { val storedSessions = store.findSessionByTopic(topic) ?: return@withContext null val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null - WcSession(userWalletId = storedSessions.walletId, sdkModel = WcSdkSessionConverter.convert(sdkSession)) + val wallet = getUserWallet.invoke(storedSessions.walletId).getOrNull() ?: return@withContext null + WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession)) } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { @@ -90,28 +95,35 @@ internal class DefaultWcSessionsManager constructor( onSessionDelete.trySend(sessionDelete) } - private suspend fun migrateLegacyStore(inNewStoreSessions: Set): Boolean { - val walletIds = getWallets.invokeSync().mapTo(mutableSetOf()) { it.walletId } - val inLegacyStoreSessions = walletIds + private suspend fun migrateLegacyStore( + inNewStore: Set, + inSdk: List, + wallets: List, + ): Boolean { + val walletIds = wallets.map { wallet -> wallet.walletId } + val inLegacyStore = walletIds .map { walletId -> flow { emit(legacyStore.loadSessions(walletId.stringValue).map { WcSessionDTO(it.topic, walletId) }) } } .merge() .reduce { accumulator, value -> accumulator.plus(value) } + // migrate only active legacySessions + .filter { legacySession -> inSdk.any { inSdkSession -> inSdkSession.topic == legacySession.topic } } - val mustSaveInNewStore = inLegacyStoreSessions.subtract(inNewStoreSessions) + val mustSaveInNewStore = inLegacyStore.subtract(inNewStore) if (mustSaveInNewStore.isNotEmpty()) store.saveSessions(mustSaveInNewStore) return mustSaveInNewStore.isNotEmpty() } - private fun associateWithSdk( - sdkSessions: List, - storeSessions: Set, + private fun associate( + inSdk: List, + inStore: Set, + wallets: List, ): List { - val wcSessions = sdkSessions.mapNotNull { sdkSession -> - val storedSessions = storeSessions.find { it.topic == sdkSession.topic } - ?: return@mapNotNull null - WcSession(userWalletId = storedSessions.walletId, sdkModel = WcSdkSessionConverter.convert(sdkSession)) + val wcSessions = inStore.mapNotNull { session -> + val wallet = wallets.find { it.walletId == session.walletId } ?: return@mapNotNull null + val sdkSession = inSdk.find { it.topic == session.topic } ?: return@mapNotNull null + WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession)) } return wcSessions } @@ -122,9 +134,6 @@ internal class DefaultWcSessionsManager constructor( val haveSomeUnknown = unknownStoredSessions.isNotEmpty() if (haveSomeUnknown) { - unknownStoredSessions.forEach { unknown -> - legacyStore.removeSession(unknown.walletId.stringValue, unknown.topic) - } store.removeSessions(unknownStoredSessions.toSet()) } return haveSomeUnknown diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt index 92501d8bba..cc898e4618 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt @@ -1,9 +1,9 @@ package com.tangem.domain.walletconnect.model import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet data class WcSession( - val userWalletId: UserWalletId, + val wallet: UserWallet, val sdkModel: WcSdkSession, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt index 06f4291649..a8f7d6b70a 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt @@ -1,8 +1,8 @@ package com.tangem.domain.walletconnect.model -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet data class WcSessionApprove( - val walletId: UserWalletId, + val wallet: UserWallet, val network: List, ) \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt index 6c2a996c91..fa207f0263 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt @@ -2,12 +2,12 @@ package com.tangem.domain.walletconnect.repository import arrow.core.Either import com.tangem.domain.walletconnect.model.WcSession -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow interface WcSessionsManager { - val sessions: Flow>> - suspend fun saveSession(userWalletId: UserWalletId, session: WcSession) - suspend fun removeSession(userWalletId: UserWalletId, session: WcSession): Either + val sessions: Flow>> + suspend fun saveSession(session: WcSession) + suspend fun removeSession(session: WcSession): Either suspend fun findSessionByTopic(topic: String): WcSession? } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt index 0adbf1dff6..138c4a419f 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt @@ -2,16 +2,16 @@ package com.tangem.domain.walletconnect.usecase import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.repository.WcSessionsManager -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first class WcSessionsUseCase(private val sessionsManager: WcSessionsManager) { - operator fun invoke(): Flow>> { + operator fun invoke(): Flow>> { return sessionsManager.sessions } - suspend fun invokeSync(): Map> { + suspend fun invokeSync(): Map> { return sessionsManager.sessions.first() } } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt index a34bd8e9f6..707dc20180 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt @@ -25,6 +25,6 @@ class WcDisconnectUseCase( } suspend fun disconnect(session: WcSession) { - sessionsManager.removeSession(session.userWalletId, session) + sessionsManager.removeSession(session) } } \ No newline at end of file From 263e9b2df808d462df388358bdfbeb82b167869f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Apr 2025 08:46:47 +0000 Subject: [PATCH 13/72] 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 f33bae32b2..8361e7d217 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "releases-5.23-1007" +tangemBlockchainSdk = "develop-1005" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-452" +tangemCardSdk = "develop-451" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From a2894c1a9005cc306909d90c40f89fa0e5652fe8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 10:22:28 +0500 Subject: [PATCH 14/72] Updated on 2026-08-14 --- .../src/main/java/com/tangem/datasource/di/MoshiModule.kt | 2 ++ .../local/nft/converter/NFTSdkAssetIdentifierConverter.kt | 8 ++++++++ .../nft/converter/NFTSdkCollectionIdentifierConverter.kt | 6 ++++++ .../main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt | 8 ++++++++ .../kotlin/com/tangem/domain/nft/models/NFTCollection.kt | 3 +++ gradle/tangem_dependencies.toml | 2 +- 6 files changed, 28 insertions(+), 1 deletion(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index 700b510390..fad4677392 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -51,12 +51,14 @@ class MoshiModule { PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc") .withSubtype(NFTCollection.Identifier.EVM::class.java, "evm") .withSubtype(NFTCollection.Identifier.TON::class.java, "ton") + .withSubtype(NFTCollection.Identifier.Solana::class.java, "sol") .withDefaultValue(NFTCollection.Identifier.Unknown), ) .add( PolymorphicJsonAdapterFactory.of(NFTAsset.Identifier::class.java, "bc") .withSubtype(NFTAsset.Identifier.EVM::class.java, "evm") .withSubtype(NFTAsset.Identifier.TON::class.java, "ton") + .withSubtype(NFTAsset.Identifier.Solana::class.java, "sol") .withDefaultValue(NFTAsset.Identifier.Unknown), ) .addLast(KotlinJsonAdapterFactory()) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetIdentifierConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetIdentifierConverter.kt index b6a045de7e..090e711d37 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetIdentifierConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetIdentifierConverter.kt @@ -13,6 +13,10 @@ object NFTSdkAssetIdentifierConverter : TwoWayConverter NFTAsset.Identifier.TON( tokenAddress = value.tokenAddress, ) + is SdkNFTAsset.Identifier.Solana -> NFTAsset.Identifier.Solana( + tokenAddress = value.tokenAddress, + cnft = value.cnft, + ) is SdkNFTAsset.Identifier.Unknown -> NFTAsset.Identifier.Unknown } @@ -24,6 +28,10 @@ object NFTSdkAssetIdentifierConverter : TwoWayConverter SdkNFTAsset.Identifier.TON( tokenAddress = value.tokenAddress, ) + is NFTAsset.Identifier.Solana -> SdkNFTAsset.Identifier.Solana( + tokenAddress = value.tokenAddress, + cnft = value.cnft, + ) is NFTAsset.Identifier.Unknown -> SdkNFTAsset.Identifier.Unknown } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionIdentifierConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionIdentifierConverter.kt index 98d8e0ee73..44f1df3f89 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionIdentifierConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionIdentifierConverter.kt @@ -12,6 +12,9 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter NFTCollection.Identifier.TON( contractAddress = value.contractAddress, ) + is SdkNFTCollection.Identifier.Solana -> NFTCollection.Identifier.Solana( + collection = value.collection, + ) is SdkNFTCollection.Identifier.Unknown -> NFTCollection.Identifier.Unknown } @@ -22,6 +25,9 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter SdkNFTCollection.Identifier.TON( contractAddress = value.contractAddress, ) + is NFTCollection.Identifier.Solana -> SdkNFTCollection.Identifier.Solana( + collection = value.collection, + ) is NFTCollection.Identifier.Unknown -> SdkNFTCollection.Identifier.Unknown } } \ No newline at end of file diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt index 3e2558f928..2c75ead2f4 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt @@ -55,6 +55,14 @@ data class NFTAsset( override val stringValue: String = tokenAddress } + @Serializable + data class Solana( + val tokenAddress: String, + val cnft: Boolean, + ) : Identifier() { + override val stringValue: String = tokenAddress + } + @Serializable data object Unknown : Identifier() { override val stringValue: String = "" diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt index d75d39c6de..eee0569d01 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt @@ -40,6 +40,9 @@ data class NFTCollection( @Serializable data class TON(val contractAddress: String?) : Identifier() + @Serializable + data class Solana(val collection: String?) : Identifier() + @Serializable data object Unknown : Identifier() } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 8361e7d217..e6912e07bc 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,5 +1,5 @@ [versions] -tangemBlockchainSdk = "develop-1005" +tangemBlockchainSdk = "develop-1009" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-451" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 13c1ca54d2263753c0f934b2a3c74201b6a6b336 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 19:48:40 +0500 Subject: [PATCH 15/72] Updated on 2026-08-14 --- .../send/v2/common/SendNavigationModelCallback.kt | 2 ++ .../send/v2/{send => common}/ui/SendContent.kt | 12 ++++++------ .../{send/confirm => common}/ui/state/ConfirmUM.kt | 2 +- .../send/v2/common/{ => ui/state}/NavigationUM.kt | 2 +- .../features/send/v2/send/DefaultSendComponent.kt | 6 +++--- .../send/v2/send/confirm/SendConfirmComponent.kt | 2 +- .../send/v2/send/confirm/model/SendConfirmModel.kt | 4 ++-- .../SendConfirmInitialStateTransformer.kt | 2 +- .../SendConfirmSendingStateTransformer.kt | 2 +- .../transformers/SendConfirmSentStateTransformer.kt | 2 +- .../SendConfirmationNotificationsTransformer.kt | 2 +- .../send/v2/send/confirm/ui/SendConfirmContent.kt | 2 +- .../tangem/features/send/v2/send/model/SendModel.kt | 4 ++-- .../send/v2/send/ui/SendNavigationButtons.kt | 2 +- .../tangem/features/send/v2/send/ui/state/SendUM.kt | 4 ++-- .../features/send/v2/sendnft/model/NFTSendModel.kt | 4 ++-- .../features/send/v2/sendnft/ui/state/NFTSendUM.kt | 4 ++-- .../v2/subcomponents/amount/model/SendAmountModel.kt | 2 +- .../destination/model/SendDestinationModel.kt | 2 +- .../send/v2/subcomponents/fee/model/SendFeeModel.kt | 2 +- 20 files changed, 33 insertions(+), 31 deletions(-) rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/{send => common}/ui/SendContent.kt (82%) rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/{send/confirm => common}/ui/state/ConfirmUM.kt (93%) rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/{ => ui/state}/NavigationUM.kt (93%) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt index 1c2c1800c4..0ce1403ae8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt @@ -1,5 +1,7 @@ package com.tangem.features.send.v2.common +import com.tangem.features.send.v2.common.ui.state.NavigationUM + internal interface SendNavigationModelCallback { fun onNavigationResult(navigationUM: NavigationUM) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt similarity index 82% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendContent.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index 8451e8d50d..d5ac9cd311 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.v2.send.ui +package com.tangem.features.send.v2.common.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -14,11 +14,11 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.send.SendRoute -import com.tangem.features.send.v2.common.NavigationUM -import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.common.ui.state.NavigationUM +import com.tangem.features.send.v2.send.ui.SendNavigationButtons @Composable -internal fun SendContent(state: SendUM, stackState: ChildStack) { +internal fun SendContent(navigationUM: NavigationUM, stackState: ChildStack) { Column( modifier = Modifier.Companion .background(color = TangemTheme.colors.background.tertiary) @@ -27,7 +27,7 @@ internal fun SendContent(state: SendUM, stackState: ChildStack Date: Tue, 8 Apr 2025 14:44:51 +0000 Subject: [PATCH 16/72] 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 f33bae32b2..e6912e07bc 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "releases-5.23-1007" +tangemBlockchainSdk = "develop-1009" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-452" +tangemCardSdk = "develop-451" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From c21b3f1231db4a5bf2990923a581bdd3d77721fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 13:24:16 +0700 Subject: [PATCH 17/72] Updated on 2026-08-14 --- data/wallet-connect/build.gradle.kts | 1 + .../di/WalletConnectDataModule.kt | 11 +- .../initialize/DefaultWcInitializeUseCase.kt | 6 +- .../pair/AssociateNetworksDelegate.kt | 34 ++- .../pair/DefaultWcPairUseCase.kt | 122 ++------- .../walletconnect/pair/WcPairSdkDelegate.kt | 98 ++++++++ .../walletconnect/DefaultWcPairUseCaseTest.kt | 232 ++++++++++++++++++ .../domain/walletconnect/model/WcNetwork.kt | 14 -- .../domain/walletconnect/model/WcPairError.kt | 2 +- .../walletconnect/model/WcSessionApprove.kt | 3 +- .../walletconnect/model/WcSessionProposal.kt | 15 +- 11 files changed, 390 insertions(+), 148 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt create mode 100644 data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt delete mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index 0aa064eccb..59061799bf 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(deps.arrow.core) /* Tests */ + testImplementation(projects.common.test) testImplementation(deps.test.coroutine) testImplementation(deps.test.junit) testImplementation(deps.test.mockk) 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 ee70b52018..8c735d8bad 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 @@ -11,6 +11,7 @@ import com.tangem.data.walletconnect.network.solana.WcSolanaNetwork import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase +import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.request.DefaultWcRequestService import com.tangem.data.walletconnect.request.WcMethodHandler import com.tangem.data.walletconnect.respond.DefaultWcRespondService @@ -59,12 +60,12 @@ internal object WalletConnectDataModule { application: Application, sessionsManager: DefaultWcSessionsManager, networkService: DefaultWcRequestService, - wcPairFlow: DefaultWcPairUseCase, + pairSdkDelegate: WcPairSdkDelegate, ): WcInitializeUseCase = DefaultWcInitializeUseCase( application = application, sessionsManager = sessionsManager, networkService = networkService, - wcPairFlow = wcPairFlow, + pairSdkDelegate = pairSdkDelegate, ) @Provides @@ -73,16 +74,22 @@ internal object WalletConnectDataModule { sessionsManager: WcSessionsManager, associateNetworksDelegate: AssociateNetworksDelegate, caipNamespaceDelegate: CaipNamespaceDelegate, + sdkDelegate: WcPairSdkDelegate, ): DefaultWcPairUseCase = DefaultWcPairUseCase( sessionsManager = sessionsManager, associateNetworksDelegate = associateNetworksDelegate, caipNamespaceDelegate = caipNamespaceDelegate, + sdkDelegate = sdkDelegate, ) @Provides @Singleton fun wcPairUseCase(default: DefaultWcPairUseCase): WcPairUseCase = default + @Provides + @Singleton + fun sdkDelegate(): WcPairSdkDelegate = WcPairSdkDelegate() + @Provides @Singleton fun defaultWcSessionsManager( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt index 6330859a84..91db31c6c8 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -6,7 +6,7 @@ import com.reown.android.CoreClient import com.reown.android.relay.ConnectionType import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit -import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase +import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.request.DefaultWcRequestService import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager import com.tangem.data.walletconnect.utils.WcSdkObserver @@ -17,13 +17,13 @@ internal class DefaultWcInitializeUseCase( private val application: Application, private val sessionsManager: DefaultWcSessionsManager, private val networkService: DefaultWcRequestService, - private val wcPairFlow: DefaultWcPairUseCase, + private val pairSdkDelegate: WcPairSdkDelegate, ) : WcInitializeUseCase { private val wcSdkObservers = mutableSetOf( sessionsManager, networkService, - wcPairFlow, + pairSdkDelegate, ) override fun init(projectId: String) { 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 e24b2641ac..ce3ec70378 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 @@ -8,12 +8,11 @@ import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.data.walletconnect.model.NamespaceKey import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletconnect.model.WcNetwork import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcSessionProposal.ProposalNetwork import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase internal class AssociateNetworksDelegate constructor( @@ -24,14 +23,13 @@ internal class AssociateNetworksDelegate constructor( ) { @Throws(WcPairError.UnsupportedNetworks::class) - suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { + suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() val optionalNamespaces: Set = sessionProposal.optionalNamespaces.setOfChainId() - return userWallets.associate { wallet -> - wallet.walletId to mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces) - } + return userWallets + .associateWith { wallet -> mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces) } } private suspend fun mapNetworksForWallet( @@ -43,11 +41,11 @@ internal class AssociateNetworksDelegate constructor( .filterIsInstance() .map { it.network } - val unknownRequired = mutableSetOf() - val missingRequired = mutableSetOf() - val required = mutableSetOf() - val available = mutableSetOf() - val notAdded = mutableSetOf() + val unknownRequired = mutableSetOf() + val missingRequired = mutableSetOf() + val required = mutableSetOf() + val available = mutableSetOf() + val notAdded = mutableSetOf() fun CAIP2.toBlockchain() = namespaceConverters[NamespaceKey(this.namespace)]?.toBlockchain(this) fun Blockchain.toNetwork() = getNetwork( @@ -60,33 +58,33 @@ internal class AssociateNetworksDelegate constructor( requiredNamespaces.forEach { chainId -> val blockchain = chainId.toBlockchain() if (blockchain == null) { - unknownRequired.add(WcNetwork.Unknown(missingNetworkName(chainId))) + unknownRequired.add(missingNetworkName(chainId)) return@forEach } val wcNetwork = blockchain.toNetwork() if (wcNetwork == null) { - unknownRequired.add(WcNetwork.Unknown(missingNetworkName(blockchain))) + unknownRequired.add(missingNetworkName(blockchain)) return@forEach } val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } if (walletNetwork == null) { - missingRequired.add(WcNetwork.Supported(wcNetwork)) + missingRequired.add(wcNetwork) } else { - required.add(WcNetwork.Supported(walletNetwork)) + required.add(walletNetwork) } } optionalNamespaces.forEach { chainId -> val wcNetwork = chainId.toBlockchain()?.toNetwork() ?: return@forEach val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } if (walletNetwork == null) { - available.add(WcNetwork.Supported(wcNetwork)) + available.add(wcNetwork) } else { - notAdded.add(WcNetwork.Supported(walletNetwork)) + notAdded.add(walletNetwork) } } if (unknownRequired.isNotEmpty()) throw WcPairError.UnsupportedNetworks(unknownRequired) return ProposalNetwork( - walletId = wallet.walletId, + wallet = wallet, missingRequired = missingRequired, required = required, available = available, 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 98dcdf4de2..28776d97c2 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 @@ -4,8 +4,6 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.reown.walletkit.client.Wallet -import com.reown.walletkit.client.WalletKit -import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcSession @@ -21,9 +19,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber -import kotlin.coroutines.resume val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") @@ -31,27 +27,18 @@ internal class DefaultWcPairUseCase( private val sessionsManager: WcSessionsManager, private val associateNetworksDelegate: AssociateNetworksDelegate, private val caipNamespaceDelegate: CaipNamespaceDelegate, -) : WcPairUseCase, WcSdkObserver { + private val sdkDelegate: WcPairSdkDelegate, +) : WcPairUseCase { private val onCallTerminalAction = Channel() - private val onSessionProposal = - Channel>(Channel.BUFFERED) - private val onSessionSettleResponse = Channel(Channel.BUFFERED) override fun pairFlow(uri: String, source: WcPairUseCase.Source): Flow { return flow { emit(WcPairState.Loading) - // call sdk.pair and wait result, finish flow on error - walletKitPair(uri).onLeft { throwable -> - emit(WcPairState.Error(WcPairError.Unknown(throwable.localizedMessage.orEmpty()))) - return@flow - } - // wait for sdk onSessionProposal callback - val (sdkSessionProposal, verifyContext) = onSessionProposal.receiveAsFlow() - .first { (sessionProposal, verifyContext) -> - true // todo(wc) check verifyContext? compare uri? - } + val sdkSessionProposal = sdkDelegate.pair(uri) + .onLeft { emit(WcPairState.Error(it)) } + .getOrNull() ?: return@flow // check unsupported dApps, just local constant for now, finish if unsupported if (sdkSessionProposal.name in unsupportedDApps) { @@ -62,44 +49,32 @@ internal class DefaultWcPairUseCase( } val proposalState = buildProposalState(sdkSessionProposal) - .fold(ifLeft = { WcPairState.Error(it) }, ifRight = { it }) + .onLeft { emit(WcPairState.Error(it)) } + .getOrNull() ?: return@flow emit(proposalState) // wait first terminal action and continue WC pair flow val terminalAction = onCallTerminalAction.receiveAsFlow().first() val sessionForApprove: WcSessionApprove? = when (terminalAction) { is TerminalAction.Approve -> terminalAction.sessionForApprove - TerminalAction.Reject -> { - // non suspending WalletKit.rejectSession call - rejectSession(sdkSessionProposal.proposerPublicKey) - null - } + TerminalAction.Reject -> null } // finish flow if rejected above - sessionForApprove ?: return@flow + if (sessionForApprove == null) { + sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey) + return@flow + } // start flow of approving in wc sdk emit(WcPairState.Approving.Loading(sessionForApprove)) - // call sdk approve and wait result val either = walletKitApproveSession( sessionForApprove = sessionForApprove, sdkSessionProposal = sdkSessionProposal, - ).fold( - ifLeft = { WcPairError.ExternalApprovalError(it.localizedMessage.orEmpty()).left() }, - ifRight = { - when (val settledSession = onSessionSettleResponse.receiveAsFlow().first()) { - is Wallet.Model.SettledSessionResponse.Error -> WcPairError.ExternalApprovalError( - settledSession.errorMessage, - ).left() - - is Wallet.Model.SettledSessionResponse.Result -> { - val newSession = settledSession.session.toDomain(sessionForApprove.wallet) - sessionsManager.saveSession(newSession) - newSession.right() - } - } - }, - ) + ).map { settledSession -> + val newSession = settledSession.session.toDomain(sessionForApprove.wallet) + sessionsManager.saveSession(newSession) + newSession + } emit(WcPairState.Approving.Result(sessionForApprove, either)) } } @@ -112,77 +87,20 @@ internal class DefaultWcPairUseCase( onCallTerminalAction.trySend(TerminalAction.Reject) } - override fun onSessionProposal( - sessionProposal: Wallet.Model.SessionProposal, - verifyContext: Wallet.Model.VerifyContext, - ) { - // Triggered when wallet receives the session proposal sent by a Dapp - Timber.i("sessionProposal: $sessionProposal") - onSessionProposal.trySend(sessionProposal to verifyContext) - } - - override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { - // Triggered when wallet receives the session settlement response from Dapp - Timber.i("onSessionSettleResponse: $settleSessionResponse") - onSessionSettleResponse.trySend(settleSessionResponse) - } - - private suspend fun walletKitPair(uri: String): Either = - suspendCancellableCoroutine { continuation -> - WalletKit.pair( - params = Wallet.Params.Pair(uri), - onSuccess = { - Timber.i("Paired successfully: $it") - continuation.resume(Unit.right()) - }, - onError = { - Timber.e("Error while pairing: $it") - continuation.resume(it.throwable.left()) - }, - ) - } - private suspend fun walletKitApproveSession( sessionForApprove: WcSessionApprove, sdkSessionProposal: Wallet.Model.SessionProposal, - ): Either { + ): Either { val namespaces = caipNamespaceDelegate.associate( sdkSessionProposal, sessionForApprove.wallet, - sessionForApprove.network.map { it.network }, + sessionForApprove.network, ) val sessionApprove = Wallet.Params.SessionApprove( proposerPublicKey = sdkSessionProposal.proposerPublicKey, namespaces = namespaces, ) - return suspendCancellableCoroutine { continuation -> - WalletKit.approveSession( - params = sessionApprove, - onSuccess = { - Timber.i("Approved successfully: $it") - continuation.resume(Unit.right()) - }, - onError = { - Timber.e("Error while approving: $it") - continuation.resume(it.throwable.left()) - }, - ) - } - } - - private fun rejectSession(proposerPublicKey: String) { - WalletKit.rejectSession( - params = Wallet.Params.SessionReject( - proposerPublicKey = proposerPublicKey, - reason = "", - ), - onSuccess = { - Timber.i("Rejected successfully: $it") - }, - onError = { - Timber.e("Error while rejecting: $it") - }, - ) + return sdkDelegate.approve(sessionApprove) } private suspend fun buildProposalState( 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 new file mode 100644 index 0000000000..423eeb9ea3 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -0,0 +1,98 @@ +package com.tangem.data.walletconnect.pair + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.WalletKit +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.domain.walletconnect.model.WcPairError +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +class WcPairSdkDelegate : WcSdkObserver { + + private val onSessionProposal = Channel() + private val onSessionSettleResponse = Channel() + + suspend fun pair(url: String): Either = coroutineScope { + suspend fun proposalCallback() = onSessionProposal + .receiveAsFlow() + .filter { proposal -> proposal.url == url } + .first() + + val pairCall = async { sdkPair(url) } + val proposal = async { proposalCallback() } + pairCall.await().map { proposal.await() } + } + + suspend fun approve( + sessionApprove: Wallet.Params.SessionApprove, + ): Either = coroutineScope { + suspend fun approveCallback() = onSessionSettleResponse + .receiveAsFlow() + .first() + + val approveCall = async { sdkApprove(sessionApprove) } + val approveCallback = async { approveCallback() } + approveCall.await() + .onLeft { 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() + } + } + + fun rejectSession(proposerPublicKey: String) { + WalletKit.rejectSession( + params = Wallet.Params.SessionReject( + proposerPublicKey = proposerPublicKey, + reason = "", + ), + onSuccess = {}, + onError = {}, + ) + } + + override fun onSessionProposal( + sessionProposal: Wallet.Model.SessionProposal, + verifyContext: Wallet.Model.VerifyContext, + ) { + // Triggered when wallet receives the session proposal sent by a Dapp + onSessionProposal.trySend(sessionProposal) + } + + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { + // Triggered when wallet receives the session settlement response from Dapp + onSessionSettleResponse.trySend(settleSessionResponse) + } + + private suspend fun sdkApprove(sessionApprove: Wallet.Params.SessionApprove): Either { + return suspendCancellableCoroutine { continuation -> + WalletKit.approveSession( + params = sessionApprove, + onSuccess = { continuation.resume(Unit.right()) }, + onError = { continuation.resume(it.throwable.toPairError()) }, + ) + } + } + + private suspend fun sdkPair(uri: String): Either { + return suspendCancellableCoroutine { continuation -> + WalletKit.pair( + params = Wallet.Params.Pair(uri), + onSuccess = { continuation.resume(Unit.right()) }, + onError = { continuation.resume(it.throwable.toPairError()) }, + ) + } + } + + private fun Throwable.toPairError() = WcPairError.ExternalApprovalError(this.localizedMessage.orEmpty()).left() +} \ 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 new file mode 100644 index 0000000000..86ed85e03e --- /dev/null +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -0,0 +1,232 @@ +package com.tangem.domain.walletconnect + +import app.cash.turbine.test +import arrow.core.left +import arrow.core.right +import com.reown.walletkit.client.Wallet +import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate +import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate +import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase +import com.tangem.data.walletconnect.pair.WcPairSdkDelegate +import com.tangem.data.walletconnect.utils.WcSdkSessionConverter +import com.tangem.domain.walletconnect.model.WcPairError +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSessionApprove +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.walletconnect.usecase.pair.WcPairState +import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import io.mockk.coEvery +import io.mockk.coVerifyOrder +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +internal class DefaultWcPairUseCaseTest { + + private val sessionsManager: WcSessionsManager = mockk() + private val associateNetworksDelegate: AssociateNetworksDelegate = mockk() + private val caipNamespaceDelegate: CaipNamespaceDelegate = mockk() + private val sdkDelegate: WcPairSdkDelegate = mockk() + + private val url = "testUrl" + private val source = WcPairUseCase.Source.QR + private val loading = WcPairState.Loading + private val sdkProposal: Wallet.Model.SessionProposal + get() = Wallet.Model.SessionProposal( + pairingTopic = "", + name = "", + description = "", + url = "", + icons = listOf(), + redirect = "", + requiredNamespaces = mapOf(), + optionalNamespaces = mapOf(), + properties = mapOf(), + proposerPublicKey = "", + relayProtocol = "", + relayData = "", + ) + + private val unsupportedDApp = "Apex Pro" + private val unsupportedSdkProposal get() = sdkProposal.copy(name = unsupportedDApp) + + private val sessionForApprove: WcSessionApprove + get() = WcSessionApprove( + wallet = MockUserWalletFactory.create(), + network = listOf(), + ) + + private val sdkApprove: Wallet.Params.SessionApprove + get() = Wallet.Params.SessionApprove( + proposerPublicKey = "", + namespaces = mapOf(), + ) + + private val sdkApproveSuccess: Wallet.Model.SettledSessionResponse.Result + get() = Wallet.Model.SettledSessionResponse.Result( + session = sdkSession, + ) + + private val sdkSession: Wallet.Model.Session + get() = Wallet.Model.Session( + pairingTopic = "", + topic = "", + expiry = 0L, + requiredNamespaces = mapOf(), + optionalNamespaces = mapOf(), + namespaces = mapOf(), + metaData = null, + ) + + private val Wallet.Model.Session.sessionForSave: WcSession + get() = WcSession( + wallet = sessionForApprove.wallet, + sdkModel = WcSdkSessionConverter.convert(this), + ) + + private val useCase = DefaultWcPairUseCase( + sessionsManager = sessionsManager, + associateNetworksDelegate = associateNetworksDelegate, + caipNamespaceDelegate = caipNamespaceDelegate, + sdkDelegate = sdkDelegate, + ) + + @Before + fun setup() { + coEvery { associateNetworksDelegate.associate(sdkProposal) } returns mapOf() + coEvery { + caipNamespaceDelegate.associate( + sessionProposal = sdkProposal, + userWallet = sessionForApprove.wallet, + networks = sessionForApprove.network, + ) + } returns mapOf() + } + + @Test + fun `pair, emmit proposal state and wait actions`() = runTest { + coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + } + assert(awaitItem() is WcPairState.Proposal) + expectNoEvents() + } + } + + @Test + fun `success pair and approve flow`() = runTest { + val approveLoading = WcPairState.Approving.Loading(sessionForApprove) + val sessionForSave = sdkSession.sessionForSave + val result = WcPairState.Approving.Result(sessionForApprove, sessionForSave.right()) + + coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() + coEvery { sdkDelegate.approve(sdkApprove) } returns sdkApproveSuccess.right() + coEvery { sessionsManager.saveSession(sessionForSave) } returns Unit + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + } + assert(awaitItem() is WcPairState.Proposal) + useCase.approve(sessionForApprove) + // ignore + useCase.reject() + + assertEquals(approveLoading, awaitItem()) + coVerifyOrder { + sdkDelegate.approve(sdkApprove) + sessionsManager.saveSession(sessionForSave) + } + assertEquals(result, awaitItem()) + awaitComplete() + } + } + + @Test + fun `success pair and reject approving`() = runTest { + val proposerPublicKey = sdkProposal.proposerPublicKey + + coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() + coEvery { sdkDelegate.rejectSession(proposerPublicKey) } returns Unit + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + } + assert(awaitItem() is WcPairState.Proposal) + useCase.reject() + coVerifyOrder { + sdkDelegate.rejectSession(sdkProposal.proposerPublicKey) + } + awaitComplete() + } + } + + @Test + fun `success pair and reject unsupported dApp`() = runTest { + coEvery { sdkDelegate.pair(url) } returns unsupportedSdkProposal.right() + val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDApp) + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + } + assertEquals(unsupportedDAppError, awaitItem()) + awaitComplete() + } + } + + @Test + fun `complete on pair error`() = runTest { + val error = WcPairError.ExternalApprovalError("error") + coEvery { sdkDelegate.pair(url) } returns error.left() + val errorState = WcPairState.Error(error) + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + } + assertEquals(errorState, awaitItem()) + awaitComplete() + } + } + + @Test + fun `complete on approve error`() = runTest { + val approveLoading = WcPairState.Approving.Loading(sessionForApprove) + val error = WcPairError.ExternalApprovalError("error").left() + coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() + coEvery { sdkDelegate.approve(sdkApprove) } returns error + + val errorResult = WcPairState.Approving.Result(sessionForApprove, error) + + useCase.pairFlow(url, source).test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + associateNetworksDelegate.associate(sdkProposal) + } + assert(awaitItem() is WcPairState.Proposal) + useCase.approve(sessionForApprove) + + assertEquals(approveLoading, awaitItem()) + coVerifyOrder { + sdkDelegate.approve(sdkApprove) + } + + assertEquals(errorResult, awaitItem()) + awaitComplete() + } + } +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt deleted file mode 100644 index 1ef5352c31..0000000000 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.walletconnect.model - -import com.tangem.domain.tokens.model.Network - -sealed interface WcNetwork { - - val name: String - - data class Supported(val network: Network) : WcNetwork { - override val name: String get() = network.name - } - - data class Unknown(override val name: String) : WcNetwork -} \ No newline at end of file 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 8f90d64512..5a060eaaba 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 @@ -4,7 +4,7 @@ sealed class WcPairError(override val message: String) : Exception(message) { data object UnsupportedDApp : WcPairError("UnsupportedDApp") data class UnsupportedNetworks( - val chains: Set, + val chains: Set, ) : WcPairError("ApprovalErrorMissingNetworks") data class ExternalApprovalError( diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt index a8f7d6b70a..427c4a9d89 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt @@ -1,8 +1,9 @@ package com.tangem.domain.walletconnect.model +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet data class WcSessionApprove( val wallet: UserWallet, - val network: List, + val network: List, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt index 648163817b..50d299303f 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt @@ -1,19 +1,20 @@ package com.tangem.domain.walletconnect.model +import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet data class WcSessionProposal( val dAppMetaData: WcAppMetaData, - val proposalNetwork: Map, + val proposalNetwork: Map, val securityStatus: Any, ) { data class ProposalNetwork( - val walletId: UserWalletId, - val missingRequired: Set, - val required: Set, - val available: Set, - val notAdded: Set, + val wallet: UserWallet, + val missingRequired: Set, + val required: Set, + val available: Set, + val notAdded: Set, ) } \ No newline at end of file From 1337033fbe5d935f4069f1df24197fc821f6253a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 07:32:51 +0000 Subject: [PATCH 18/72] 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 f33bae32b2..e6912e07bc 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "releases-5.23-1007" +tangemBlockchainSdk = "develop-1009" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-452" +tangemCardSdk = "develop-451" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 47c32fe112e77b68e2ace334e8a4f27d8319fd03 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 13:59:18 +0500 Subject: [PATCH 19/72] Updated on 2026-08-14 --- .../presentation/state/utils/StakingRewardsUtils.kt | 12 ++++++++++++ .../java/com/tangem/lib/crypto/BlockchainUtils.kt | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt index 510266948e..9ddc12135c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt @@ -5,8 +5,10 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.COSMOS_SCHEDULE import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.SOLANA_SCHEDULE +import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.TON_SCHEDULE import com.tangem.lib.crypto.BlockchainUtils.isCosmos import com.tangem.lib.crypto.BlockchainUtils.isSolana +import com.tangem.lib.crypto.BlockchainUtils.isTon import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.StringsSigns.MINUS import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE @@ -14,6 +16,7 @@ import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE private data object StakingRewardSchedule { val COSMOS_SCHEDULE = 5 to 12 val SOLANA_SCHEDULE = 2 to 3 + val TON_SCHEDULE = 1 to 2 } internal fun getRewardScheduleText( @@ -71,6 +74,15 @@ private fun getCustomRewardSchedule(networkId: String, decapitalize: Boolean = f ) } isTron(networkId) -> resourceReference(id = R.string.staking_reward_schedule_day, decapitalize = decapitalize) + isTon(networkId) -> combinedReference( + resourceReference(id = R.string.staking_reward_schedule_each_plural, decapitalize = decapitalize), + stringReference(NON_BREAKING_SPACE.toString()), + stringReference("${TON_SCHEDULE.first}$MINUS${TON_SCHEDULE.second}$NON_BREAKING_SPACE"), + pluralReference( + id = R.plurals.common_days_no_param, + count = TON_SCHEDULE.second, + ), + ) else -> null } } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 3decd72093..71205a6291 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -72,6 +72,11 @@ object BlockchainUtils { return blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet } + fun isTon(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) + return blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet + } + fun isSupportedNetworkId(blockchainId: String, excludedBlockchains: ExcludedBlockchains): Boolean { val blockchain = Blockchain.fromNetworkId(blockchainId) From ea780f563e86cbb545fa7f5ae267d0d4b22237af Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 12:44:22 +0300 Subject: [PATCH 20/72] Updated on 2026-08-14 --- .../java/com/tangem/domain/feedback/utils/FeedbackConstants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt index 0e70883587..e1ec372cc9 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt @@ -4,4 +4,4 @@ internal const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB internal const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com" internal const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" -internal const val TANGEM_VISA_SUPPORT_EMAIL = "visa-support@tangem.com" // [REDACTED_TODO_COMMENT] \ No newline at end of file +internal const val TANGEM_VISA_SUPPORT_EMAIL = "pay@tangem.com" \ No newline at end of file From 42c8be234e547dd77e4c7cdb4e2b58b49dfca3fd Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 15:03:19 +0500 Subject: [PATCH 21/72] Updated on 2026-08-14 --- .../java/com/tangem/tap/TangemApplication.kt | 59 ++++-- .../tap/data/DefaultVisaAuthTokenStorage.kt | 24 ++- .../tangem/tap/data/DefaultVisaOTPStorage.kt | 14 +- ...tUserWalletsPublicInformationRepository.kt | 9 +- ...erWalletsSensitiveInformationRepository.kt | 15 +- .../feature/impl/DevFeatureTogglesManager.kt | 2 +- .../manager/DevTogglesManagerTest.kt | 8 +- .../storage/LocalTogglesStorageTest.kt | 7 +- .../datasource/asset/loader/AssetLoader.kt | 96 +++++----- .../asset/reader/AndroidAssetReader.kt | 8 +- .../di/AppPreferencesStoreModule.kt | 2 +- .../tangem/datasource/di/AssetReaderModule.kt | 8 +- .../datasource/local/logs/AppLogsStore.kt | 24 ++- .../local/preferences/AppPreferencesStore.kt | 2 + .../utils/AppPreferencesStoreExt.kt | 176 ++++++++++-------- .../utils/SharedPreferencesKeyMigration.kt | 4 +- .../asset/loader/AssetLoaderTest.kt | 7 +- .../asset/reader/AndroidAssetReaderTest.kt | 3 +- 18 files changed, 275 insertions(+), 193 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 4cc52e4409..bb4aa08278 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -1,6 +1,9 @@ package com.tangem.tap import android.app.Application +import android.os.StrictMode +import android.os.StrictMode.ThreadPolicy +import android.os.StrictMode.VmPolicy import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import coil.ImageLoader @@ -71,9 +74,7 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import org.rekotlin.Store import kotlin.collections.set import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository @@ -228,12 +229,32 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat // endregion + private val appScope = MainScope() + override fun onCreate() { + enableStrictModeInDebug() super.onCreate() - init() + } - updateLogFiles() + private fun enableStrictModeInDebug() { + if (BuildConfig.DEBUG) { + StrictMode.setThreadPolicy( + ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectAll() + .penaltyLog() + .build(), + ) + StrictMode.setVmPolicy( + VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + .build(), + ) + } } private fun updateLogFiles() { @@ -260,18 +281,31 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat foregroundActivityObserver = ForegroundActivityObserver() registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) - // TODO: Try to performance and user experience. - // [REDACTED_JIRA] + // We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them. runBlocking { awaitAll( - async { featureTogglesManager.init() }, - async { excludedBlockchainsManager.init() }, - async { initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) }, + async { + featureTogglesManager.init() + }, + async { + excludedBlockchainsManager.init() + }, ) } - loadNativeLibraries() + appScope.launch { + initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) + launch(Dispatchers.IO) { + loadNativeLibraries() + walletConnect2Repository.init( + projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId, + ) + updateLogFiles() + } + } + ExceptionHandler.append(blockchainExceptionHandler) + if (LogConfig.network.blockchainSdkNetwork) { BlockchainSdkRetrofitBuilder.interceptors = listOf( createNetworkLoggingInterceptor(), @@ -287,9 +321,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, ) - appStateHolder.mainStore = store - walletConnect2Repository.init(projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId) + appStateHolder.mainStore = store } private fun createReduxStore(): Store { 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 a6128ddfe6..9859e3e4be 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt @@ -20,18 +20,22 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor( private val dispatcherProvider: CoroutineDispatcherProvider, ) : VisaAuthTokenStorage { - private val secureStorage = AndroidSecureStorage( - preferences = SecureStorage.createEncryptedSharedPreferences( - context = applicationContext, - storageName = "visa_auth_storage", - ), - ) + private val secureStorage by lazy { + AndroidSecureStorage( + preferences = SecureStorage.createEncryptedSharedPreferences( + context = applicationContext, + storageName = "visa_auth_storage", + ), + ) + } - private val moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() + private val moshi by lazy { + Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + } - private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java) + private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) } override suspend fun store(cardId: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) { val json = tokensAdapter.toJson(tokens) 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 f752d10dea..b87d3fbfdf 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt @@ -20,12 +20,14 @@ class DefaultVisaOTPStorage @Inject constructor( private val dispatcherProvider: CoroutineDispatcherProvider, ) : VisaOTPStorage { - private val secureStorage = AndroidSecureStorage( - preferences = SecureStorage.createEncryptedSharedPreferences( - context = applicationContext, - storageName = "visa_otp_storage", - ), - ) + private val secureStorage by lazy { + AndroidSecureStorage( + preferences = SecureStorage.createEncryptedSharedPreferences( + context = applicationContext, + storageName = "visa_otp_storage", + ), + ) + } override suspend fun saveOTP(cardId: String, data: VisaOtpData) = withContext(dispatcherProvider.io) { secureStorage.store(data.rootOTP, VISA_ROOT_OTP_KEY_PREFIX + cardId) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt index 9ee2498fa8..d963dc5794 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt @@ -20,9 +20,12 @@ internal class DefaultUserWalletsPublicInformationRepository( moshi: Moshi, private val secureStorage: SecureStorage, ) : UserWalletsPublicInformationRepository { - private val publicInformationAdapter: JsonAdapter> = moshi.adapter( - Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java), - ) + + private val publicInformationAdapter: JsonAdapter> by lazy { + moshi.adapter( + Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java), + ) + } override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { return withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt index 13c56f4f22..a1888ea494 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt @@ -24,12 +24,15 @@ internal class DefaultUserWalletsSensitiveInformationRepository( private val secureStorage: SecureStorage, ) : UserWalletsSensitiveInformationRepository { - private val sensitiveInformationAdapter: JsonAdapter = moshi.adapter( - UserWalletSensitiveInformation::class.java, - ) - private val encryptedSensitiveInformationMapAdapter: JsonAdapter> = moshi.adapter( - Types.newParameterizedType(Map::class.java, String::class.java, ByteArray::class.java), - ) + private val sensitiveInformationAdapter: JsonAdapter by lazy { + moshi.adapter(UserWalletSensitiveInformation::class.java) + } + + private val encryptedSensitiveInformationMapAdapter: JsonAdapter> by lazy { + moshi.adapter( + Types.newParameterizedType(Map::class.java, String::class.java, ByteArray::class.java), + ) + } override suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult { if (encryptionKey == null) { diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index 750dbada43..7aca4511ea 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -32,7 +32,7 @@ internal class DevFeatureTogglesManager( val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull>( key = PreferencesKeys.FEATURE_TOGGLES_KEY, - ) ?: emptyMap() + ) ?: emptyMap() val localFeatureToggles = localTogglesStorage.toggles .associateToggles(currentVersion = versionProvider.get().orEmpty()) diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt index b1822c84d9..a4c3d6d7aa 100644 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt @@ -2,6 +2,7 @@ package com.tangem.core.configtoggle.manager import android.annotation.SuppressLint import com.google.common.truth.Truth +import com.squareup.moshi.Moshi import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants import com.tangem.core.configtoggle.storage.ConfigToggle @@ -12,6 +13,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.getSyncOrNull +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.Test @@ -24,7 +26,11 @@ import kotlin.collections.set internal class DevTogglesManagerTest { private val localTogglesStorage = mockk() - private val appPreferenceStore = mockk(relaxed = true) + private val appPreferenceStore = AppPreferencesStore( + moshi = Moshi.Builder().build(), + dispatchers = TestingCoroutineDispatcherProvider(), + preferencesDataStore = mockk(relaxed = true), + ) private val versionProvider = mockk() private val manager = DevFeatureTogglesManager( localTogglesStorage = localTogglesStorage, diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt index 4a8e6550b8..b2f5bc4be7 100644 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt @@ -8,6 +8,7 @@ import com.squareup.moshi.Types import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.asset.reader.AssetReader +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.Test @@ -24,7 +25,11 @@ internal class LocalTogglesStorageTest { private val jsonAdapter = mockk>>() // Impossible to mockk AssetLoader because it implement inline functions - private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi) + private val assetLoader = AssetLoader( + assetReader = assetReader, + moshi = moshi, + dispatchers = TestingCoroutineDispatcherProvider(), + ) private val storage = LocalTogglesStorage(assetLoader) diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt index 6c04362a3c..3f47419a51 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt @@ -3,8 +3,10 @@ package com.tangem.datasource.asset.loader import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.moshi.adapter +import com.tangem.utils.coroutines.runCatching import com.tangem.datasource.asset.reader.AssetReader import com.tangem.datasource.di.NetworkMoshi +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -23,69 +25,63 @@ import javax.inject.Singleton class AssetLoader @Inject constructor( val assetReader: AssetReader, @NetworkMoshi val moshi: Moshi, + val dispatchers: CoroutineDispatcherProvider, ) { - /** Load content [Content] of asset file [fileName] */ @OptIn(ExperimentalStdlibApi::class) - suspend inline fun load(fileName: String): Content? { - return runCatching { - val json = assetReader.read(fullFileName = "$fileName.json") + suspend inline fun load(fileName: String): Content? = runCatching(dispatchers.io) { + val json = assetReader.read(fullFileName = "$fileName.json") - moshi.adapter().fromJson(json) - } - .fold( - onSuccess = { parsedConfig -> - if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) - parsedConfig - }, - onFailure = { - Timber.e(it, "Failed to load config [$fileName] from assets") - null - }, - ) + moshi.adapter().fromJson(json) } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + parsedConfig + }, + onFailure = { + Timber.e(it, "Failed to load config [$fileName] from assets") + null + }, + ) /** Load list [V] values of asset file [fileName] */ - suspend inline fun loadList(fileName: String): List { - return runCatching { - val json = assetReader.read(fullFileName = "$fileName.json") + suspend inline fun loadList(fileName: String): List = runCatching(dispatchers.io) { + val json = assetReader.read(fullFileName = "$fileName.json") - val type = Types.newParameterizedType(List::class.java, V::class.java) - val adapter = moshi.adapter>(type) + val type = Types.newParameterizedType(List::class.java, V::class.java) + val adapter = moshi.adapter>(type) - adapter.fromJson(json) - } - .fold( - onSuccess = { parsedConfig -> - if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) - parsedConfig.orEmpty() - }, - onFailure = { - Timber.e(it, "Failed to load config [$fileName] from assets") - emptyList() - }, - ) + adapter.fromJson(json) } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + parsedConfig.orEmpty() + }, + onFailure = { + Timber.e(it, "Failed to load config [$fileName] from assets") + emptyList() + }, + ) /** Load map [String] keys and [V] values of asset file [fileName] */ - suspend inline fun loadMap(fileName: String): Map { - return runCatching { - val json = assetReader.read(fullFileName = "$fileName.json") + suspend inline fun loadMap(fileName: String): Map = runCatching(dispatchers.io) { + val json = assetReader.read(fullFileName = "$fileName.json") - val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) - val adapter = moshi.adapter>(type) + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) - adapter.fromJson(json) - } - .fold( - onSuccess = { parsedConfig -> - if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) - parsedConfig.orEmpty() - }, - onFailure = { - Timber.e(it, "Failed to load config [$fileName] from assets") - emptyMap() - }, - ) + adapter.fromJson(json) } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + parsedConfig.orEmpty() + }, + onFailure = { + Timber.e(it, "Failed to load config [$fileName] from assets") + emptyMap() + }, + ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt index 139e082aa9..3847d29980 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt @@ -1,23 +1,19 @@ package com.tangem.datasource.asset.reader import android.content.res.AssetManager -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import java.io.BufferedReader /** * Implementation of asset file reader * * @property assetManager asset manager - * @property dispatchers dispatchers */ internal class AndroidAssetReader( private val assetManager: AssetManager, - private val dispatchers: CoroutineDispatcherProvider, ) : AssetReader { - override suspend fun read(fullFileName: String): String = withContext(dispatchers.io) { - assetManager.open(fullFileName).bufferedReader() + override suspend fun read(fullFileName: String): String { + return assetManager.open(fullFileName).bufferedReader() .use(BufferedReader::readText) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferencesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferencesStoreModule.kt index bc295ea874..d572234840 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferencesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferencesStoreModule.kt @@ -2,7 +2,6 @@ package com.tangem.datasource.di import android.content.Context import com.squareup.moshi.Moshi -import com.tangem.datasource.local.* import com.tangem.datasource.local.preferences.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -26,6 +25,7 @@ internal object AppPreferencesStoreModule { return AppPreferencesStore( preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io), moshi = moshi, + dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AssetReaderModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AssetReaderModule.kt index 196ec8d836..3075e3f403 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AssetReaderModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AssetReaderModule.kt @@ -3,7 +3,6 @@ package com.tangem.datasource.di import android.content.Context import com.tangem.datasource.asset.reader.AndroidAssetReader import com.tangem.datasource.asset.reader.AssetReader -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,10 +16,7 @@ internal object AssetReaderModule { @Singleton @Provides - fun providesAsserReader( - @ApplicationContext context: Context, - dispatchers: CoroutineDispatcherProvider, - ): AssetReader { - return AndroidAssetReader(context.assets, dispatchers) + fun providesAsserReader(@ApplicationContext context: Context): AssetReader { + return AndroidAssetReader(context.assets) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt index 60e30ca5e7..56c6e52028 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -37,8 +37,12 @@ class AppLogsStore @Inject constructor( private val mutex = Mutex() private val zipMutex = Mutex() - private val file = File(applicationContext.filesDir, PERMITTED_FILE_NAME) - private val fileZip = File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP) + private val logFile by lazy { + File(applicationContext.filesDir, PERMITTED_FILE_NAME) + } + private val logFileZip by lazy { + File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP) + } private val formatter = DateTimeFormatterBuilder() .appendDayOfMonth(2) @@ -55,12 +59,12 @@ class AppLogsStore @Inject constructor( .toFormatter() /** Get log file */ - fun getFile(): File? = if (file.exists()) file else null + fun getFile(): File? = if (logFile.exists()) logFile else null suspend fun getZipFile(): File? { return zipMutex.withLock { - if (file.exists()) { - zip(listOf(file), fileZip) + if (logFile.exists()) { + zip(listOf(logFile), logFileZip) } else { null } @@ -98,8 +102,8 @@ class AppLogsStore @Inject constructor( /** Delete deprecated logs if file size exceeds [maxSize] */ fun deleteDeprecatedLogs(maxSize: Int) { launchWithLock { - if (file.exists() && file.length() > maxSize) { - file.delete() + if (logFile.exists() && logFile.length() > maxSize) { + logFile.delete() } } } @@ -117,7 +121,7 @@ class AppLogsStore @Inject constructor( } private fun writeMessage(tag: String, vararg messages: String) { - BufferedWriter(FileWriter(file, true)).use { writer -> + BufferedWriter(FileWriter(logFile, true)).use { writer -> writer.append(formatter.print(DateTime.now())) writer.append(": $tag ") messages.forEach(writer::append) @@ -126,8 +130,8 @@ class AppLogsStore @Inject constructor( } private fun createFileIfNotExist() { - if (!file.exists()) { - runCatching { file.createNewFile() } + if (!logFile.exists()) { + runCatching { logFile.createNewFile() } .onFailure(Timber::e) } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt index 905fe4f767..d30b6e20ef 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt @@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import com.squareup.moshi.Moshi import com.squareup.moshi.Types +import com.tangem.utils.coroutines.CoroutineDispatcherProvider /** * Application preferences store. @@ -19,6 +20,7 @@ import com.squareup.moshi.Types */ class AppPreferencesStore( val moshi: Moshi, + val dispatchers: CoroutineDispatcherProvider, private val preferencesDataStore: DataStore, ) : DataStore by preferencesDataStore { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index 0369cf22cd..647ff92c89 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -5,23 +5,25 @@ import androidx.datastore.preferences.core.edit import com.squareup.moshi.JsonDataException import com.squareup.moshi.Types import com.tangem.datasource.local.preferences.AppPreferencesStore -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.withContext /** Get flow of nullable data [T] by string [key] */ inline fun AppPreferencesStore.getObject(key: Preferences.Key): Flow { - val adapter = moshi.adapter(T::class.java) - return data.map { preferences -> - preferences[key]?.let { - try { - adapter.fromJson(it) - } catch (e: JsonDataException) { - null - } - } - }.distinctUntilChanged() + return flow { + val adapter = moshi.adapter(T::class.java) + emitAll( + data.map { preferences -> + preferences[key]?.let { + try { + adapter.fromJson(it) + } catch (e: JsonDataException) { + null + } + } + }.distinctUntilChanged(), + ) + } } /** @@ -32,16 +34,19 @@ inline fun AppPreferencesStore.getObject(key: Preferences.Key AppPreferencesStore.getObject(key: Preferences.Key, default: T): Flow { - val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types - return data.map { - try { - it[key]?.let(adapter::fromJson) ?: default - } catch (e: JsonDataException) { - default - } - }.distinctUntilChanged() + return flow { + val adapter = moshi.adapter(T::class.java) + emitAll( + data.map { + try { + it[key]?.let(adapter::fromJson) ?: default + } catch (e: JsonDataException) { + default + } + }.distinctUntilChanged(), + ) + } } - /** * Get nullable data [T] by string [key] * @@ -49,26 +54,27 @@ inline fun AppPreferencesStore.getObject(key: Preferences.Key AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key): T? { - val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types - return data.firstOrNull() - ?.get(key) - ?.let { - try { - adapter.fromJson(it) - } catch (e: JsonDataException) { - null +suspend inline fun AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key): T? = + withContext(dispatchers.io) { + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types + data.firstOrNull() + ?.get(key) + ?.let { + try { + adapter.fromJson(it) + } catch (e: JsonDataException) { + null + } } - } -} + } /** Get data [T] by string [key]. If data is not found, it returns [default] */ suspend inline fun AppPreferencesStore.getObjectSyncOrDefault( key: Preferences.Key, default: T, -): T { +): T = withContext(dispatchers.io) { val adapter = moshi.adapter(T::class.java) - return data.firstOrNull() + data.firstOrNull() ?.get(key) ?.let { try { @@ -87,37 +93,47 @@ suspend inline fun AppPreferencesStore.getObjectSyncOrDefault( * * @see storeObjectList * */ -suspend inline fun AppPreferencesStore.storeObject(key: Preferences.Key, value: T) { - val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types - edit { it[key] = adapter.toJson(value) } -} +@Suppress("OptionalUnit") +suspend inline fun AppPreferencesStore.storeObject(key: Preferences.Key, value: T): Unit = + withContext(dispatchers.io) { + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types + edit { it[key] = adapter.toJson(value) } + } /** Store list of data [value] by string [key] */ -suspend inline fun AppPreferencesStore.storeObjectList(key: Preferences.Key, value: List) { - val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) - edit { it[key] = adapter.toJson(value) } -} +suspend inline fun AppPreferencesStore.storeObjectList(key: Preferences.Key, value: List) = + withContext(dispatchers.io) { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + edit { it[key] = adapter.toJson(value) } + } /** Get flow of list of data [T] by string [key]. If data is not found, it returns `null` */ inline fun AppPreferencesStore.getObjectList(key: Preferences.Key): Flow?> { - val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) - return data.map { it[key]?.let(adapter::fromJson) }.distinctUntilChanged() + return flow { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + emitAll( + data.map { + it[key]?.let(adapter::fromJson) + }.distinctUntilChanged(), + ) + } } /** Get list of data [T] by string [key], or empty if data is not found */ -suspend inline fun AppPreferencesStore.getObjectListSync(key: Preferences.Key): List { - val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) - return data.firstOrNull() - ?.get(key) - ?.let(adapter::fromJson) - .orEmpty() -} +suspend inline fun AppPreferencesStore.getObjectListSync(key: Preferences.Key): List = + withContext(dispatchers.io) { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + data.firstOrNull() + ?.get(key) + ?.let(adapter::fromJson) + .orEmpty() + } /** Store map with [String] key and value [V] by string [key] */ suspend inline fun AppPreferencesStore.storeObjectMap( key: Preferences.Key, value: Map, -) { +) = withContext(dispatchers.io) { val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) val adapter = moshi.adapter>(type) @@ -125,37 +141,47 @@ suspend inline fun AppPreferencesStore.storeObjectMap( } /** Get map with [String] key and value [V] by string [key], or empty if data is not found */ -suspend inline fun AppPreferencesStore.getObjectMapSync(key: Preferences.Key): Map { - val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) - val adapter = moshi.adapter>(type) +suspend inline fun AppPreferencesStore.getObjectMapSync(key: Preferences.Key): Map = + withContext(dispatchers.io) { + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) - return data.firstOrNull() - ?.get(key) - ?.let(adapter::fromJson) - .orEmpty() -} + data.firstOrNull() + ?.get(key) + ?.let(adapter::fromJson) + .orEmpty() + } /** Get flow of map with [String] key and value [V] by string [key], or empty if data is not found */ inline fun AppPreferencesStore.getObjectMap(key: Preferences.Key): Flow> { - val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) - val adapter = moshi.adapter>(type) + return flow { + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) - return data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() } + emitAll( + data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() }, + ) + } } /** Get set of data [T] by string [key], or empty if data is not found */ -suspend inline fun AppPreferencesStore.getObjectSetSync(key: Preferences.Key): Set { - val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) - return data.firstOrNull() - ?.get(key) - ?.let(adapter::fromJson) - .orEmpty() -} +suspend inline fun AppPreferencesStore.getObjectSetSync(key: Preferences.Key): Set = + withContext(dispatchers.io) { + val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) + data.firstOrNull() + ?.get(key) + ?.let(adapter::fromJson) + .orEmpty() + } /** Get flow of set of [T] by string [key], or empty if data is not found */ inline fun AppPreferencesStore.getObjectSet(key: Preferences.Key): Flow> { - val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) - return data.map { - it[key]?.let(adapter::fromJson) ?: emptySet() + return flow { + val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) + emitAll( + data.map { + it[key]?.let(adapter::fromJson) ?: emptySet() + }, + ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt index a049816e5c..386887a13b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt @@ -22,7 +22,9 @@ internal class SharedPreferencesKeyMigration( private val keyName: String, ) : DataMigration { - private val legacyPrefs = context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE) + private val legacyPrefs by lazy { + context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE) + } override suspend fun cleanUp() { val sharedPrefsEditor = legacyPrefs.edit() diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt index 49924b9fe8..1bbee64573 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt @@ -7,6 +7,7 @@ import com.squareup.moshi.Types import com.squareup.moshi.adapter import com.tangem.datasource.api.express.models.response.Asset import com.tangem.datasource.asset.reader.AssetReader +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerifyOrder import io.mockk.every @@ -22,7 +23,11 @@ class AssetLoaderTest { private val assetReader = mockk() private val moshi = mockk() - private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi) + private val assetLoader = AssetLoader( + assetReader = assetReader, + moshi = moshi, + dispatchers = TestingCoroutineDispatcherProvider(), + ) @Test fun load() = runTest { diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt index c1beef670e..651d234c8d 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt @@ -2,7 +2,6 @@ package com.tangem.datasource.asset.reader import android.content.res.AssetManager import com.google.common.truth.Truth -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest @@ -15,7 +14,7 @@ import java.io.IOException internal class AndroidAssetReaderTest { private val assetManager = mockk() - private val assetReader = AndroidAssetReader(assetManager, TestingCoroutineDispatcherProvider()) + private val assetReader = AndroidAssetReader(assetManager) @Test fun read_content() = runTest { From 6043c59c9cd2d26d6a844026336015a6f9f5fe59 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 12:39:49 +0000 Subject: [PATCH 22/72] 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 f33bae32b2..e6912e07bc 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "releases-5.23-1007" +tangemBlockchainSdk = "develop-1009" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-452" +tangemCardSdk = "develop-451" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 844f84cb35ffe20919a9e49344d61beb5693d122 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 10:23:18 +0500 Subject: [PATCH 23/72] Updated on 2026-08-14 --- .../features/send/v2/common/SendNavigationModelCallback.kt | 2 +- .../java/com/tangem/features/send/v2/common/ui/SendContent.kt | 3 +-- .../send/v2/{send => common}/ui/SendNavigationButtons.kt | 4 ++-- .../send/v2/common/ui/state/{ => ui/state}/NavigationUM.kt | 2 +- .../v2/di/{SendModelModule.kt => CommonSendModelModule.kt} | 2 +- .../java/com/tangem/features/send/v2/send/ui/state/SendUM.kt | 2 +- .../com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt | 2 +- .../com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt | 2 +- .../features/send/v2/subcomponents/fee/model/SendFeeModel.kt | 2 +- 9 files changed, 10 insertions(+), 11 deletions(-) rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/{send => common}/ui/SendNavigationButtons.kt (97%) rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/{ => ui/state}/NavigationUM.kt (92%) rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/{SendModelModule.kt => CommonSendModelModule.kt} (96%) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt index 0ce1403ae8..b004bbe3cb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt @@ -1,6 +1,6 @@ package com.tangem.features.send.v2.common -import com.tangem.features.send.v2.common.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM internal interface SendNavigationModelCallback { fun onNavigationResult(navigationUM: NavigationUM) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index d5ac9cd311..10e15ec143 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -14,8 +14,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.send.SendRoute -import com.tangem.features.send.v2.common.ui.state.NavigationUM -import com.tangem.features.send.v2.send.ui.SendNavigationButtons +import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM @Composable internal fun SendContent(navigationUM: NavigationUM, stackState: ChildStack) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendNavigationButtons.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt similarity index 97% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendNavigationButtons.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt index 7edc75815f..801e9063b7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendNavigationButtons.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.v2.send.ui +package com.tangem.features.send.v2.common.ui import androidx.compose.animation.* import androidx.compose.foundation.background @@ -29,7 +29,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.v2.common.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM @Composable internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/NavigationUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ui/state/NavigationUM.kt similarity index 92% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/NavigationUM.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ui/state/NavigationUM.kt index 404f1a3e73..59f2e9ba3d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/NavigationUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ui/state/NavigationUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.v2.common.ui.state +package com.tangem.features.send.v2.common.ui.state.ui.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/CommonSendModelModule.kt similarity index 96% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/CommonSendModelModule.kt index 3c21d1380d..bb0fc819ab 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/CommonSendModelModule.kt @@ -14,7 +14,7 @@ import dagger.multibindings.IntoMap @Module @InstallIn(ModelComponent::class) -internal interface SendModelModule { +internal interface CommonSendModelModule { @Binds @IntoMap 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..51cd9a1ae2 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 @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.send.ui.state import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.features.send.v2.common.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 3d5d6d178a..7e9b3a29bd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -16,7 +16,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.NFTSendComponent -import com.tangem.features.send.v2.common.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt index 526bfdd621..f67a071468 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt @@ -1,6 +1,6 @@ package com.tangem.features.send.v2.sendnft.ui.state -import com.tangem.features.send.v2.common.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM 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 3a8d73d809..e82500c413 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 @@ -11,7 +11,7 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.features.send.v2.common.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger From b5f664d2250cbc15a39c8ec7c819088ffc48f2c1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 18:35:40 +0500 Subject: [PATCH 24/72] Updated on 2026-08-14 --- .../impl/presentation/model/StakingModel.kt | 6 ++++-- .../helpers/StakingFeeTransactionLoader.kt | 20 ++++++++++--------- .../state/helpers/StakingTransactionSender.kt | 15 +++++++++----- .../SetConfirmationStateAssentTransformer.kt | 3 ++- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 63648d1762..d122cd48bf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -302,12 +302,13 @@ internal class StakingModel @Inject constructor( ) modelScope.launch { feeLoader.getFee( - onStakingFee = { gasEstimate -> + onStakingFee = { gasEstimate, isFeeApproximate -> stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = gasEstimate, + isFeeApproximate = isFeeApproximate, cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) @@ -363,13 +364,14 @@ internal class StakingModel @Inject constructor( stakingEventFactory.createSendTransactionErrorAlert(error) stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus)) }, - onFeeIncreased = { increasedFee -> + onFeeIncreased = { increasedFee, isFeeApproximate -> stateController.updateAll( SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus), SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = increasedFee, + isFeeApproximate = isFeeApproximate, cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt index 1688f31bdf..8043052fe7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -18,6 +18,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.staking.getCurrentToken import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -38,13 +39,14 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( private val stateController: StakingStateController, private val getFeeUseCase: GetFeeUseCase, private val estimateGasUseCase: EstimateGasUseCase, + private val isFeeApproximateUseCase: IsFeeApproximateUseCase, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val userWallet: UserWallet, @Assisted private val yield: Yield, ) { suspend fun getFee( - onStakingFee: (Fee) -> Unit, + onStakingFee: (Fee, Boolean) -> Unit, onStakingFeeError: (StakingError) -> Unit, onApprovalFee: (TransactionFee) -> Unit, onFeeError: (GetFeeError) -> Unit, @@ -91,7 +93,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( amount: BigDecimal, validatorAddress: String, onStakingFeeError: (StakingError) -> Unit, - onStakingFee: (Fee) -> Unit, + onStakingFee: (Fee, Boolean) -> Unit, ) { val sourceAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("No available address") @@ -144,14 +146,14 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( } } + val amount = Amount( + currencySymbol = gasEstimate.token.symbol, + value = gasEstimate.amount, + decimals = gasEstimate.token.decimals, + ) onStakingFee( - Fee.Common( - Amount( - currencySymbol = gasEstimate.token.symbol, - value = gasEstimate.amount, - decimals = gasEstimate.token.decimals, - ), - ), + Fee.Common(amount), + isFeeApproximateUseCase(networkId = cryptoCurrencyStatus.currency.network.id, amountType = amount.type), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt index 27d87225e0..aa0afb181b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -5,7 +5,10 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.domain.staking.* +import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase +import com.tangem.domain.staking.GetStakingTransactionsUseCase +import com.tangem.domain.staking.SaveUnsubmittedHashUseCase +import com.tangem.domain.staking.SubmitHashUseCase import com.tangem.domain.staking.model.SubmitHashData import com.tangem.domain.staking.model.stakekit.NetworkType import com.tangem.domain.staking.model.stakekit.PendingAction @@ -19,6 +22,7 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionTy import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.staking.getCurrentToken import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount @@ -49,6 +53,7 @@ internal class StakingTransactionSender @AssistedInject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val submitHashUseCase: SubmitHashUseCase, private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, + private val isFeeApproximateUseCase: IsFeeApproximateUseCase, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val userWallet: UserWallet, @Assisted private val yield: Yield, @@ -63,7 +68,7 @@ internal class StakingTransactionSender @AssistedInject constructor( onConstructError: (StakingError) -> Unit, onSendSuccess: (String) -> Unit, onSendError: (SendTransactionError?) -> Unit, - onFeeIncreased: (Fee) -> Unit, + onFeeIncreased: (Fee, Boolean) -> Unit, ) { val state = stateController.value @@ -101,10 +106,10 @@ internal class StakingTransactionSender @AssistedInject constructor( onSendError = onSendError, ) } else { + val amount = fee.amount.copy(value = totalFee) onFeeIncreased( - Fee.Common( - fee.amount.copy(value = totalFee), - ), + Fee.Common(amount), + isFeeApproximateUseCase(networkId = cryptoCurrencyStatus.currency.network.id, amountType = amount.type), ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 56e01d3a83..c9a8054cc5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -13,6 +13,7 @@ internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val fee: Fee, + private val isFeeApproximate: Boolean, private val cryptoCurrencyStatus: CryptoCurrencyStatus, ) : Transformer { @@ -31,7 +32,7 @@ internal class SetConfirmationStateAssentTransformer( rate = feeCryptoCurrencyStatus?.value?.fiatRate, isFeeConvertibleToFiat = isFeeConvertibleToFiat, appCurrency = appCurrencyProvider(), - isFeeApproximate = false, + isFeeApproximate = isFeeApproximate, ), isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() From 1794605e26455c76874bcc7ba233f14a9cfde699 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 10:23:30 +0500 Subject: [PATCH 25/72] Updated on 2026-08-14 --- .../send/v2/common/PredefinedValues.kt | 24 ++++ .../send/v2/common/SendBalanceUpdater.kt | 87 +++++++++++++ .../v2/common/SendNavigationModelCallback.kt | 2 +- .../features/send/v2/common/ui/SendContent.kt | 2 +- .../v2/common/ui/SendNavigationButtons.kt | 2 +- .../ui/state/{ui/state => }/NavigationUM.kt | 2 +- .../send/v2/send/DefaultSendComponent.kt | 9 +- .../v2/send/confirm/SendConfirmComponent.kt | 45 +++---- .../send/v2/send/confirm/model/ConfirmData.kt | 15 +++ .../v2/send/confirm/model/SendConfirmModel.kt | 119 +++++------------- .../features/send/v2/send/model/SendModel.kt | 13 +- .../features/send/v2/send/ui/state/SendUM.kt | 2 +- .../send/v2/sendnft/model/NFTSendModel.kt | 2 +- .../send/v2/sendnft/ui/state/NFTSendUM.kt | 2 +- .../amount/SendAmountBlockComponent.kt | 3 +- .../amount/SendAmountComponentParams.kt | 8 +- .../amount/model/SendAmountModel.kt | 6 +- .../SendDestinationBlockComponent.kt | 3 +- .../SendDestinationComponentParams.kt | 5 +- .../destination/model/SendDestinationModel.kt | 13 +- .../subcomponents/fee/model/SendFeeModel.kt | 2 +- .../notifications/NotificationsComponent.kt | 12 +- .../notifications/model/NotificationsModel.kt | 39 ++---- 23 files changed, 227 insertions(+), 190 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/{ui/state => }/NavigationUM.kt (92%) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/ConfirmData.kt diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt new file mode 100644 index 0000000000..adbfd84dfc --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt @@ -0,0 +1,24 @@ +package com.tangem.features.send.v2.common + +sealed class PredefinedValues { + data object Empty : PredefinedValues() + + sealed class Content : PredefinedValues() { + abstract val amount: String + abstract val address: String + abstract val memo: String? + + data class Deeplink( + override val amount: String, + override val address: String, + override val memo: String?, + val transactionId: String, + ) : Content() + + data class QrCode( + override val amount: String, + override val address: String, + override val memo: String?, + ) : Content() + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt new file mode 100644 index 0000000000..69254405cf --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt @@ -0,0 +1,87 @@ +package com.tangem.features.send.v2.common + +import com.tangem.domain.tokens.FetchPendingTransactionsUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.txhistory.TxHistoryFeatureToggles +import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter +import com.tangem.utils.coroutines.DelayedWork +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.* + +@Suppress("LongParameterList") +internal class SendBalanceUpdater @AssistedInject constructor( + private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val txHistoryFeatureToggles: TxHistoryFeatureToggles, + private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter, + @DelayedWork private val coroutineScope: CoroutineScope, + @Assisted private val userWallet: UserWallet, + @Assisted private val cryptoCurrency: CryptoCurrency, +) { + fun scheduleUpdates() { + coroutineScope.launch { + listOf( + // we should update network to find pending tx after 1 sec + async { + fetchPendingTransactionsUseCase( + userWalletId = userWallet.walletId, + networks = setOf(cryptoCurrency.network), + ) + }, + // we should update tx history and network for new balances + async { + updateTxHistory() + }, + async { + updateNetworkStatuses() + }, + ).awaitAll() + } + } + + private suspend fun updateNetworkStatuses(delay: Long = BALANCE_UPDATE_DELAY) { + updateDelayedNetworkStatusUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + delayMillis = delay, + refresh = true, + ) + } + + private suspend fun updateTxHistory() { + delay(BALANCE_UPDATE_DELAY) + val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + ) + + txHistoryItemsCountEither.onRight { + if (txHistoryFeatureToggles.isFeatureEnabled) { + txHistoryContentUpdateEmitter.triggerUpdate() + } else { + getTxHistoryItemsUseCase( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + refresh = true, + ) + } + } + } + + private companion object { + const val BALANCE_UPDATE_DELAY = 11_000L + } + + @AssistedFactory + interface Factory { + fun create(cryptoCurrency: CryptoCurrency, userWallet: UserWallet): SendBalanceUpdater + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt index b004bbe3cb..0ce1403ae8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt @@ -1,6 +1,6 @@ package com.tangem.features.send.v2.common -import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.NavigationUM internal interface SendNavigationModelCallback { fun onNavigationResult(navigationUM: NavigationUM) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index 10e15ec143..265067a7df 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -14,7 +14,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.send.SendRoute -import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.NavigationUM @Composable internal fun SendContent(navigationUM: NavigationUM, stackState: ChildStack) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt index 801e9063b7..581e14ba92 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt @@ -29,7 +29,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.NavigationUM @Composable internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ui/state/NavigationUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/NavigationUM.kt similarity index 92% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ui/state/NavigationUM.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/NavigationUM.kt index 59f2e9ba3d..404f1a3e73 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ui/state/NavigationUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/NavigationUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.v2.common.ui.state.ui.state +package com.tangem.features.send.v2.common.ui.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable 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 d54221cbd4..07a7427df4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -19,6 +19,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.ui.SendContent import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents @@ -152,7 +153,7 @@ internal class DefaultSendComponent @AssistedInject constructor( cryptoCurrencyStatus = model.cryptoCurrencyStatus, callback = model, isEditMode = route.isEditMode, - predefinedAmountValue = model.predefinedAmountValue, + predefinedValues = model.predefinedValues, ), ) @@ -187,14 +188,14 @@ internal class DefaultSendComponent @AssistedInject constructor( val predefinedAddress = params.destinationAddress val predefinedValues = if (predefinedAmount != null && predefinedTxId != null && predefinedAddress != null) { - SendConfirmComponent.Params.PredefinedValues.Content( + PredefinedValues.Content.Deeplink( amount = predefinedAmount, address = predefinedAddress, - tag = params.tag, + memo = params.tag, transactionId = predefinedTxId, ) } else { - SendConfirmComponent.Params.PredefinedValues.Empty + PredefinedValues.Empty } return SendConfirmComponent( appComponentContext = factoryContext, 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 0d5c803926..3acce0b9c5 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 @@ -11,6 +11,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel import com.tangem.features.send.v2.send.confirm.ui.SendConfirmContent @@ -19,10 +20,11 @@ import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent -import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData import com.tangem.utils.extensions.orZero import kotlinx.coroutines.flow.* @@ -38,15 +40,13 @@ internal class SendConfirmComponent( private val destinationBlockComponent = SendDestinationBlockComponent( appComponentContext = child("sendConfirmDestinationBlock"), - params = SendDestinationComponentParams.DestinationBlockParams( + params = DestinationBlockParams( state = model.uiState.value.destinationUM, analyticsCategoryName = params.analyticsCategoryName, userWalletId = params.userWallet.walletId, cryptoCurrency = params.cryptoCurrencyStatus.currency, blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), - isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, - predefinedAddressValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.address, - predefinedMemoValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.tag, + predefinedValues = params.predefinedValues, ), onResult = model::onDestinationResult, onClick = model::showEditDestination, @@ -61,8 +61,7 @@ internal class SendConfirmComponent( cryptoCurrencyStatus = params.cryptoCurrencyStatus, appCurrency = params.appCurrency, blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), - isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, - predefinedAmountValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.amount, + predefinedValues = params.predefinedValues, ), onResult = model::onAmountResult, onClick = model::showEditAmount, @@ -77,8 +76,8 @@ internal class SendConfirmComponent( cryptoCurrencyStatus = params.cryptoCurrencyStatus, feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, appCurrency = params.appCurrency, - sendAmount = model.enteredAmount.orZero(), - destinationAddress = model.enteredDestination.orEmpty(), + sendAmount = model.confirmData.enteredAmount.orZero(), + destinationAddress = model.confirmData.enteredDestination.orEmpty(), blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), ), onResult = model::onFeeResult, @@ -93,13 +92,15 @@ internal class SendConfirmComponent( cryptoCurrencyStatus = params.cryptoCurrencyStatus, feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, appCurrency = params.appCurrency, - destinationAddress = model.enteredDestination.orEmpty(), - memo = model.enteredMemo, - amountValue = model.enteredAmount.orZero(), - reduceAmountBy = model.reduceAmountBy.orZero(), - isIgnoreReduce = model.isIgnoreReduce, - fee = model.fee, - feeError = model.feeError, + notificationData = NotificationData( + destinationAddress = model.confirmData.enteredDestination.orEmpty(), + memo = model.confirmData.enteredMemo, + amountValue = model.confirmData.enteredAmount.orZero(), + reduceAmountBy = model.confirmData.reduceAmountBy.orZero(), + isIgnoreReduce = model.confirmData.isIgnoreReduce, + fee = model.confirmData.fee, + feeError = model.confirmData.feeError, + ), ), ) @@ -143,17 +144,7 @@ internal class SendConfirmComponent( val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, val predefinedValues: PredefinedValues, - ) { - sealed class PredefinedValues { - data object Empty : PredefinedValues() - data class Content( - val transactionId: String, - val amount: String, - val address: String, - val tag: String?, - ) : PredefinedValues() - } - } + ) interface ModelCallback { fun onResult(sendUM: SendUM) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/ConfirmData.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/ConfirmData.kt new file mode 100644 index 0000000000..e6aa732445 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/ConfirmData.kt @@ -0,0 +1,15 @@ +package com.tangem.features.send.v2.send.confirm.model + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.transaction.error.GetFeeError +import java.math.BigDecimal + +data class ConfirmData( + val enteredAmount: BigDecimal?, + val reduceAmountBy: BigDecimal, + val isIgnoreReduce: Boolean, + val enteredDestination: String?, + val enteredMemo: String?, + val fee: Fee?, + val feeError: GetFeeError?, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index ebad9698e0..cb79ab86b1 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 @@ -5,7 +5,6 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.routing.AppRouter import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -25,17 +24,13 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase -import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.utils.convertToSdkAmount +import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.impl.R @@ -57,17 +52,13 @@ import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData -import com.tangem.features.txhistory.TxHistoryFeatureToggles -import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.DelayedWork import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.utils.transformer.update -import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber -import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -88,20 +79,14 @@ internal class SendConfirmModel @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, - private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, - private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, - private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, - private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger, - private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter, private val notificationsUpdateTrigger: NotificationsUpdateTrigger, private val alertFactory: SendConfirmAlertFactory, private val sendAnalyticHelper: SendAnalyticHelper, - private val txHistoryFeatureToggles: TxHistoryFeatureToggles, - @DelayedWork private val coroutineScope: CoroutineScope, private val urlOpener: UrlOpener, private val shareManager: ShareManager, + sendBalanceUpdaterFactory: SendBalanceUpdater.Factory, ) : Model(), SendConfirmClickIntents { private val params: SendConfirmComponent.Params = paramsContainer.require() @@ -111,6 +96,8 @@ internal class SendConfirmModel @Inject constructor( private val cryptoCurrencyStatus = params.cryptoCurrencyStatus private val cryptoCurrency = cryptoCurrencyStatus.currency + private val sendBalanceUpdater = sendBalanceUpdaterFactory.create(cryptoCurrency, userWallet) + private val _uiState = MutableStateFlow(params.state) val uiState = _uiState.asStateFlow() @@ -123,20 +110,16 @@ internal class SendConfirmModel @Inject constructor( private val feeSelectorUM get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content - val enteredAmount: BigDecimal? - get() = amountState?.amountTextField?.cryptoAmount?.value - val reduceAmountBy: BigDecimal - get() = amountState?.reduceAmountBy.orZero() - val isIgnoreReduce: Boolean - get() = amountState?.isIgnoreReduce == true - val enteredDestination: String? - get() = destinationUM?.addressTextField?.value - val enteredMemo: String? - get() = destinationUM?.memoTextField?.value - val fee: Fee? - get() = feeSelectorUM?.selectedFee - val feeError: GetFeeError? - get() = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error + val confirmData: ConfirmData + get() = ConfirmData( + enteredAmount = amountState?.amountTextField?.cryptoAmount?.value, + enteredMemo = destinationUM?.memoTextField?.value, + reduceAmountBy = amountState?.reduceAmountBy.orZero(), + isIgnoreReduce = amountState?.isIgnoreReduce == true, + enteredDestination = destinationUM?.addressTextField?.value, + fee = feeSelectorUM?.selectedFee, + feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error, + ) private var sendIdleTimer: Long = 0L private var isAmountSubtractAvailable = false @@ -234,15 +217,15 @@ internal class SendConfirmModel @Inject constructor( override fun onFailedTxEmailClick(errorMessage: String) { val amountValue = amountState?.amountTextField?.cryptoAmount?.value - val feeValue = fee?.amount?.value + val feeValue = confirmData.fee?.amount?.value val receivingAmount = if (amountValue != null && feeValue != null) { checkAndCalculateSubtractedAmount( isAmountSubtractAvailable = isAmountSubtractAvailable, cryptoCurrencyStatus = cryptoCurrencyStatus, - amountValue = enteredAmount.orZero(), + amountValue = confirmData.enteredAmount.orZero(), feeValue = feeValue, - reduceAmountBy = reduceAmountBy, + reduceAmountBy = confirmData.reduceAmountBy, ) } else { null @@ -255,7 +238,7 @@ internal class SendConfirmModel @Inject constructor( errorMessage = errorMessage, blockchainId = cryptoCurrency.network.id.value, derivationPath = cryptoCurrency.network.derivationPath.value, - destinationAddress = enteredDestination.orEmpty(), + destinationAddress = confirmData.enteredDestination.orEmpty(), tokenSymbol = if (amount?.type is AmountType.Token) { amount.currencySymbol } else { @@ -320,7 +303,7 @@ internal class SendConfirmModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, amountValue = amountValue, feeValue = feeValue, - reduceAmountBy = reduceAmountBy.orZero(), + reduceAmountBy = confirmData.reduceAmountBy.orZero(), ) modelScope.launch { @@ -367,7 +350,7 @@ internal class SendConfirmModel @Inject constructor( ifRight = { updateTransactionStatus(txData) addTokenToWalletIfNeeded() - scheduleUpdates() + sendBalanceUpdater.scheduleUpdates() sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value) }, ) @@ -378,7 +361,7 @@ internal class SendConfirmModel @Inject constructor( val wallets = destinationUM?.wallets ?: return val receivingUserWallet = wallets - .firstOrNull { it.address == enteredDestination } + .firstOrNull { it.address == confirmData.enteredDestination } ?: return val userWalletId = receivingUserWallet.userWalletId ?: return @@ -401,49 +384,6 @@ internal class SendConfirmModel @Inject constructor( _uiState.update(SendConfirmSentStateTransformer(txData, txUrl)) } - private fun scheduleUpdates() { - coroutineScope.launch { - listOf( - // we should update network to find pending tx after 1 sec - async { - fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network)) - }, - // we should update tx history and network for new balance - async { - updateTxHistory() - }, - async { - updateDelayedCurrencyStatusUseCase( - userWalletId = userWallet.walletId, - network = cryptoCurrency.network, - delayMillis = BALANCE_UPDATE_DELAY, - refresh = true, - ) - }, - ).awaitAll() - } - } - - private suspend fun updateTxHistory() { - delay(BALANCE_UPDATE_DELAY) - val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase( - userWalletId = userWallet.walletId, - currency = cryptoCurrency, - ) - - txHistoryItemsCountEither.onRight { - if (txHistoryFeatureToggles.isFeatureEnabled) { - txHistoryContentUpdateEmitter.triggerUpdate() - } else { - getTxHistoryItemsUseCase( - userWalletId = userWallet.walletId, - currency = cryptoCurrency, - refresh = true, - ) - } - } - } - private fun subscribeOnCheckFeeResultUpdates() { sendFeeCheckReloadTrigger.checkReloadResultFlow.onEach { isFeeResultSuccess -> if (isFeeResultSuccess) { @@ -460,13 +400,13 @@ internal class SendConfirmModel @Inject constructor( modelScope.launch { notificationsUpdateTrigger.triggerUpdate( data = NotificationData( - destinationAddress = enteredDestination.orEmpty(), - memo = enteredMemo, - amountValue = enteredAmount.orZero(), - reduceAmountBy = reduceAmountBy.orZero(), - isIgnoreReduce = isIgnoreReduce, - fee = fee, - feeError = feeError, + destinationAddress = confirmData.enteredDestination.orEmpty(), + memo = confirmData.enteredMemo, + amountValue = confirmData.enteredAmount.orZero(), + reduceAmountBy = confirmData.reduceAmountBy.orZero(), + isIgnoreReduce = confirmData.isIgnoreReduce, + fee = confirmData.fee, + feeError = confirmData.feeError, ), ) _uiState.update { @@ -552,6 +492,5 @@ internal class SendConfirmModel @Inject constructor( private companion object { const val CHECK_FEE_UPDATE_DELAY = 10_000L - const val BALANCE_UPDATE_DELAY = 11_000L } } \ 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 44a2787062..ad59d24108 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 @@ -29,11 +29,12 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.common.PredefinedValues +import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.send.SendRoute -import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory import com.tangem.features.send.v2.send.confirm.SendConfirmComponent -import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent @@ -90,7 +91,7 @@ internal class SendModel @Inject constructor( var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() var appCurrency: AppCurrency = AppCurrency.Default - var predefinedAmountValue: String? = null + var predefinedValues: PredefinedValues = PredefinedValues.Empty private var balanceHidingJobHolder = JobHolder() @@ -227,7 +228,11 @@ internal class SendModel @Inject constructor( private fun onQrCodeScanned(address: String) { val parsedQrCode = parseQrCodeUseCase(address, cryptoCurrency).getOrNull() - predefinedAmountValue = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals) + predefinedValues = PredefinedValues.Content.QrCode( + amount = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals).orEmpty(), + address = parsedQrCode?.address.orEmpty(), + memo = parsedQrCode?.memo, + ) } private fun onFailedTxEmailClick(errorMessage: String? = null) { 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 51cd9a1ae2..8359df5247 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 @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.send.ui.state import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 7e9b3a29bd..3d5d6d178a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -16,7 +16,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.NFTSendComponent -import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt index f67a071468..526bfdd621 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt @@ -1,6 +1,6 @@ package com.tangem.features.send.v2.sendnft.ui.state -import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM 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 b616b506f7..b05e56c031 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 @@ -9,6 +9,7 @@ import com.tangem.common.ui.amountScreen.ui.AmountBlock import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountBlockParams import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel import kotlinx.coroutines.flow.launchIn @@ -39,7 +40,7 @@ internal class SendAmountBlockComponent( AmountBlock( amountState = state, isClickDisabled = !isClickEnabled, - isEditingDisabled = params.isPredefinedValues, + isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink, onClick = onClick, ) } 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 0df4012c9d..05f4e2a828 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 @@ -4,6 +4,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback import kotlinx.coroutines.flow.Flow @@ -16,7 +17,7 @@ internal sealed class SendAmountComponentParams { abstract val userWallet: UserWallet abstract val appCurrency: AppCurrency abstract val cryptoCurrencyStatus: CryptoCurrencyStatus - abstract val predefinedAmountValue: String? + abstract val predefinedValues: PredefinedValues data class AmountParams( override val state: AmountState, @@ -24,7 +25,7 @@ internal sealed class SendAmountComponentParams { override val userWallet: UserWallet, override val appCurrency: AppCurrency, override val cryptoCurrencyStatus: CryptoCurrencyStatus, - override val predefinedAmountValue: String?, + override val predefinedValues: PredefinedValues, val isEditMode: Boolean, val callback: ModelCallback, val currentRoute: Flow, @@ -37,8 +38,7 @@ internal sealed class SendAmountComponentParams { override val userWallet: UserWallet, override val appCurrency: AppCurrency, override val cryptoCurrencyStatus: CryptoCurrencyStatus, - override val predefinedAmountValue: String?, + override val predefinedValues: PredefinedValues, val blockClickEnableFlow: StateFlow, - val isPredefinedValues: Boolean, ) : SendAmountComponentParams() } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index c222bfce07..18e832c369 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 @@ -18,6 +18,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.common.PredefinedValues 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.SendRoute @@ -98,7 +99,10 @@ internal class SendAmountModel @Inject constructor( ), ) } - params.predefinedAmountValue?.let(::onAmountValueChange) + val predefinedValues = params.predefinedValues as? PredefinedValues.Content + if (predefinedValues?.amount != null) { + onAmountValueChange(predefinedValues.amount) + } } } 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 ff98000eed..5f500027fa 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 @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel import com.tangem.features.send.v2.subcomponents.destination.ui.DestinationBlock import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM @@ -38,7 +39,7 @@ internal class SendDestinationBlockComponent( DestinationBlock( destinationUM = state, isClickDisabled = !isClickEnabled, - isEditingDisabled = params.isPredefinedValues, + isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink, onClick = onClick, ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt index f4a69acb24..15b8c7bb6c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.v2.subcomponents.destination import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent.ModelCallback import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM @@ -32,8 +33,6 @@ internal sealed class SendDestinationComponentParams { override val userWalletId: UserWalletId, override val cryptoCurrency: CryptoCurrency, val blockClickEnableFlow: StateFlow, - val predefinedAddressValue: String?, - val predefinedMemoValue: String?, - val isPredefinedValues: Boolean, + val predefinedValues: PredefinedValues, ) : SendDestinationComponentParams() } \ No newline at end of file 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 c83153b4c4..c31c86c96d 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 @@ -22,6 +22,7 @@ import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase +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 import com.tangem.features.send.v2.send.SendRoute @@ -29,6 +30,7 @@ import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents import com.tangem.features.send.v2.subcomponents.destination.model.transformers.* @@ -96,19 +98,20 @@ internal class SendDestinationModel @Inject constructor( } private fun initialState() { - if ((uiState.value as? DestinationUM.Content)?.isInitialized == false) { + if ((uiState.value as? DestinationUM.Content)?.isInitialized == false || uiState.value is DestinationUM.Empty) { _uiState.update( SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, isInitialized = true, ), ) - val params = params as? SendDestinationComponentParams.DestinationBlockParams - if (params?.predefinedAddressValue != null && params.predefinedMemoValue != null) { + val params = params as? DestinationBlockParams + val predefinedValues = params?.predefinedValues as? PredefinedValues.Content.Deeplink + if (predefinedValues?.address != null) { _uiState.update( SendDestinationPredefinedStateTransformer( - address = params.predefinedAddressValue, - memo = params.predefinedMemoValue, + address = predefinedValues.address, + memo = predefinedValues.memo, ), ) } 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 e82500c413..3a8d73d809 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 @@ -11,7 +11,7 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM +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 import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt index 14760575ab..4f163c1a61 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt @@ -2,19 +2,17 @@ package com.tangem.features.send.v2.subcomponents.notifications import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.Modifier -import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.send.v2.subcomponents.notifications +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationsModel import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.StateFlow -import java.math.BigDecimal internal class NotificationsComponent( appComponentContext: AppComponentContext, @@ -45,12 +43,6 @@ internal class NotificationsComponent( val cryptoCurrencyStatus: CryptoCurrencyStatus, val feeCryptoCurrencyStatus: CryptoCurrencyStatus, val appCurrency: AppCurrency, - val destinationAddress: String, - val memo: String?, - val amountValue: BigDecimal, - val reduceAmountBy: BigDecimal, - val isIgnoreReduce: Boolean, - val fee: Fee?, - val feeError: GetFeeError?, + val notificationData: NotificationData, ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index b27fefd5e3..34013568f6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -78,13 +78,7 @@ internal class NotificationsModel @Inject constructor( private val currency = cryptoCurrencyStatus.currency private val appCurrency = params.appCurrency - private var destinationAddress = params.destinationAddress - private var memo = params.memo - private var amountValue = params.amountValue - private var reduceAmountBy = params.reduceAmountBy - private var isIgnoreReduce = params.isIgnoreReduce - private var fee = params.fee - private var feeError = params.feeError + private var notificationData = params.notificationData private val _uiState = MutableStateFlow>(persistentListOf()) val uiState = _uiState.asStateFlow() @@ -110,14 +104,7 @@ internal class NotificationsModel @Inject constructor( } private suspend fun updateState(data: NotificationData) { - destinationAddress = data.destinationAddress - memo = data.memo - amountValue = data.amountValue - reduceAmountBy = data.reduceAmountBy - isIgnoreReduce = data.isIgnoreReduce - fee = data.fee - feeError = data.feeError - + notificationData = data buildNotifications() } @@ -126,7 +113,7 @@ internal class NotificationsModel @Inject constructor( addFeeUnreachableNotification( tokenStatus = cryptoCurrencyStatus, coinStatus = feeCryptoCurrencyStatus, - feeError = feeError, + feeError = notificationData.feeError, onReload = { modelScope.launch { sendFeeReloadTrigger.triggerUpdate() @@ -134,13 +121,7 @@ internal class NotificationsModel @Inject constructor( }, onClick = ::showTokenDetails, ) - addDomainNotifications( - destinationAddress = destinationAddress, - memo = memo, - amountValue = amountValue, - reduceAmountBy = reduceAmountBy, - fee = fee, - ) + addDomainNotifications() } notificationsUpdateTrigger.callbackHasError(notifications.any { it is NotificationUM.Error }) @@ -161,13 +142,7 @@ internal class NotificationsModel @Inject constructor( } } - private suspend fun MutableList.addDomainNotifications( - destinationAddress: String, - memo: String?, - amountValue: BigDecimal, - reduceAmountBy: BigDecimal, - fee: Fee?, - ) { + private suspend fun MutableList.addDomainNotifications() = with(notificationData) { val balance = cryptoCurrencyStatus.value.amount ?: return val feeValue = fee?.amount?.value ?: return val isFeeCoverage = checkFeeCoverage( @@ -305,7 +280,7 @@ internal class NotificationsModel @Inject constructor( ) { val validationError = validateTransactionUseCase( userWalletId = userWalletId, - amount = amountValue.convertToSdkAmount(cryptoCurrencyStatus.currency), + amount = enteredAmount.convertToSdkAmount(currency), fee = fee, memo = memo, destination = destinationAddress, @@ -353,7 +328,7 @@ internal class NotificationsModel @Inject constructor( addHighFeeWarningNotification( enteredAmountValue = enteredAmount, cryptoCurrencyStatus = cryptoCurrencyStatus, - ignoreAmountReduce = isIgnoreReduce, + ignoreAmountReduce = notificationData.isIgnoreReduce, onReduceClick = { reduceBy, reduceByDiff, _ -> modelScope.launch { sendAmountReduceTrigger.triggerReduceBy( From 655370bc693b6ef7964f0ba4b3161bc2ff0f47fb Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 20:15:51 +0000 Subject: [PATCH 26/72] 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 32be17375b..e6912e07bc 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-997" +tangemBlockchainSdk = "develop-1009" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-451" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 0727b381ca24d177a799884c50d3281ffa087dc3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 00:17:59 -0700 Subject: [PATCH 27/72] Updated on 2026-08-14 --- .../models/event/MainScreenAnalyticsEvent.kt | 35 +++++++- .../entry/impl/model/OnboardingEntryModel.kt | 24 +++++- .../model/OnboardingVisaAccessCodeModel.kt | 16 ++++ .../model/OnboardingVisaApproveModel.kt | 14 +++ .../model/OnboardingVisaChooseWalletModel.kt | 14 +++ .../model/OnboardingVisaInProgressModel.kt | 4 + .../model/OnboardingVisaOtherWalletModel.kt | 6 ++ .../model/OnboardingVisaPinCodeModel.kt | 9 ++ .../model/OnboardingVisaWelcomeModel.kt | 13 +++ .../analytics/OnboardingVisaAnalyticsEvent.kt | 85 +++++++++++++++++++ .../model/analytics/VisaAnalyticsEvent.kt | 23 +++++ .../wallet/model/intents/VisaWalletIntents.kt | 7 +- .../WalletCurrencyActionsClickIntents.kt | 13 +-- .../transformers/SetVisaInfoTransformer.kt | 16 +++- .../BalancesAndLimitsBottomSheetConverter.kt | 13 ++- .../state/utils/WalletLoadingStateFactory.kt | 3 +- 16 files changed, 277 insertions(+), 18 deletions(-) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/VisaAnalyticsEvent.kt diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt index 5d8c3a6887..879b87a6f9 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt @@ -22,9 +22,35 @@ sealed class MainScreenAnalyticsEvent( ) // region Action Buttons feature - data class ButtonBuy(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent( + data class ButtonBuy( + val status: AnalyticsParam.Status, + val screenType: String? = null, + ) : MainScreenAnalyticsEvent( event = "Button - Buy", - params = mapOf(AnalyticsParam.STATUS to status.value), + params = buildMap { + put(AnalyticsParam.STATUS, status.value) + screenType?.let { put(AnalyticsParam.TYPE, it) } + }, + ) + + data object ButtonReceive : MainScreenAnalyticsEvent( + event = "Button - Receive", + ) + + data object LimitsClicked : MainScreenAnalyticsEvent( + event = "Limits Clicked", + ) + + data object NoticeBalancesInfo : MainScreenAnalyticsEvent( + event = "Notice - Balances Info", + ) + + data object NoticeLimitsInfo : MainScreenAnalyticsEvent( + event = "Notice - Limits Info", + ) + + data object ButtonExplore : MainScreenAnalyticsEvent( + event = "Button - Explore", ) data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent( @@ -83,4 +109,9 @@ sealed class MainScreenAnalyticsEvent( params = mapOf(ERROR_CODE to errorCode), ) // endregion + + companion object { + const val VISA_TYPE = "Visa" + const val WALLET_TYPE = "Wallet" + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index e466f10765..b6a675a820 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -4,6 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.replaceAll import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.models.scan.ProductType import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -12,7 +13,6 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -29,6 +29,7 @@ import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEve import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable @@ -150,10 +151,16 @@ internal class OnboardingEntryModel @Inject constructor( if (askBiometryFeatureToggles.isAskForBiometryEnabled) { modelScope.launch { if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) { + doIfVisa { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.BiometricScreenOpened) + } stackNavigation.replaceAll( OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks(doneMode)), ) } else { + doIfVisa { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) + } stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, @@ -163,6 +170,9 @@ internal class OnboardingEntryModel @Inject constructor( } } } else { + doIfVisa { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) + } stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, @@ -177,6 +187,9 @@ internal class OnboardingEntryModel @Inject constructor( ) : AskBiometryComponent.ModelCallbacks { override fun onAllowed() { analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.On)) + doIfVisa { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) + } stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, @@ -187,6 +200,9 @@ internal class OnboardingEntryModel @Inject constructor( override fun onDenied() { analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.Off)) + doIfVisa { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) + } stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, @@ -232,6 +248,12 @@ internal class OnboardingEntryModel @Inject constructor( } } + private inline fun doIfVisa(function: () -> Unit) { + if (params.scanResponse.productType == ProductType.Visa) { + function() + } + } + inner class OnboardingTwinModelCallbacks : OnboardingTwinComponent.ModelCallbacks { override fun onDone() { navigateToFinalScreenFlow() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index 71c5adec28..7feeafe67c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.CompletionResult import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -19,6 +20,9 @@ import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent import com.tangem.features.onboarding.v2.visa.impl.common.ActivationReadyEvent import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationTaskMode @@ -30,6 +34,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @Stable @ModelScoped internal class OnboardingVisaAccessCodeModel @Inject constructor( @@ -40,6 +45,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( private val tangemSdkManager: TangemSdkManager, private val visaAuthRepository: VisaAuthRepository, private val uiMessageSender: UiMessageSender, + private val analyticsEventsHandler: AnalyticsEventHandler, ) : Model() { private val params: OnboardingVisaAccessCodeComponent.Config = paramsContainer.require() @@ -59,6 +65,10 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( val onBack = MutableSharedFlow() val onDone = MutableSharedFlow() + init { + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.SettingAccessCodeStarted) + } + fun onBack() { if (uiState.value.buttonLoading) return @@ -99,11 +109,14 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( private fun onContinue() { when (uiState.value.step) { OnboardingVisaAccessCodeUM.Step.Enter -> { + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeEntered) if (checkAccessCodeMinChars().not()) return _uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) } + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeReenterScreen) } OnboardingVisaAccessCodeUM.Step.ReEnter -> { if (checkAccessCodesMatch().not()) return + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.OnboardingVisa) startActivationProcess(accessCode = uiState.value.accessCodeFirst.text) } } @@ -160,6 +173,9 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( is CompletionResult.Failure -> { loading(false) uiMessageSender.showErrorDialog(result.error.universalError) + analyticsEventsHandler.send( + VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE), + ) return@launch } is CompletionResult.Success -> result.data diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt index a4803e73d0..9f14644a5c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.approve.model import androidx.compose.runtime.Stable import com.tangem.common.CompletionResult import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -15,6 +16,9 @@ import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.features.onboarding.v2.visa.impl.child.approve.OnboardingVisaApproveComponent import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state.OnboardingVisaApproveUM +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow @@ -32,6 +36,7 @@ internal class OnboardingVisaApproveModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val tangemSdkManager: TangemSdkManager, private val uiMessageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -47,6 +52,10 @@ internal class OnboardingVisaApproveModel @Inject constructor( val uiState = _uiState.asStateFlow() val onDone = MutableSharedFlow() + init { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.WalletPrepare) + } + private fun getInitialState(): OnboardingVisaApproveUM { return OnboardingVisaApproveUM( onApproveClick = ::onApproveClick, @@ -56,6 +65,8 @@ internal class OnboardingVisaApproveModel @Inject constructor( private fun onApproveClick() { loading(true) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonApprove) + modelScope.launch { val dataToSign = runCatching { visaActivationRepository.getCustomerWalletAcceptanceData(params.preparationDataForApprove.request) @@ -77,6 +88,9 @@ internal class OnboardingVisaApproveModel @Inject constructor( is CompletionResult.Failure -> { loading(false) uiMessageSender.showErrorDialog(result.error.universalError) + analyticsEventHandler.send( + VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE), + ) return@launch } is CompletionResult.Success -> result.data diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt index 3d48e57718..b9d427dc44 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model import androidx.compose.runtime.Stable +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.ui.extensions.resourceReference @@ -8,6 +9,7 @@ import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent.Params.Event import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.OnboardingVisaChooseWalletUM import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.SelectableChainRowUM +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableSharedFlow @@ -21,6 +23,7 @@ import javax.inject.Inject @ModelScoped internal class OnboardingVisaChooseWalletModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val _uiState = MutableStateFlow(getInitialState()) @@ -53,7 +56,18 @@ internal class OnboardingVisaChooseWalletModel @Inject constructor( private fun onContinueClicked() { modelScope.launch { val selectedOption = _uiState.value.selectedOption ?: return@launch + val value = getAnalyticsValue(selectedOption.event) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ChooseWalletScreen(value)) onEvent.emit(selectedOption.event) } } + + private fun getAnalyticsValue(event: Event) = when (event) { + Event.TangemWallet -> { + "Tangem" + } + Event.OtherWallet -> { + "Other" + } + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index b8d608e1d3..b409af7757 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model import androidx.compose.runtime.Stable import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -20,6 +21,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay @@ -41,6 +43,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, private val uiMessageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -53,6 +56,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( val onDone = MutableSharedFlow() init { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ActivationInProgressScreen) modelScope.launch { while (true) { val result = runCatching { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt index 875e5a4b49..4d89e3baa0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model import androidx.compose.runtime.Stable import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -13,6 +14,7 @@ import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.OnboardingVisaOtherWalletComponent import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -29,6 +31,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val urlOpener: UrlOpener, private val shareManager: ShareManager, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val config = paramsContainer.require() @@ -44,6 +47,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( val onDone = MutableSharedFlow() init { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.GoToWebsiteOpened) modelScope.launch { while (true) { val result = runCatching { @@ -68,10 +72,12 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( } private fun onShareClicked() { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonShareLink) shareManager.shareText("https://tangem.com/") // TODO } private fun onOpenInBrowserClicked() { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonBrowser) urlOpener.openUrl("https://tangem.com/") // TODO } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt index bcb96ade2f..f3919153a9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.pincode.model import androidx.compose.runtime.Stable import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -17,6 +18,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -37,6 +39,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( private val authTokenStorage: VisaAuthTokenStorage, private val otpStorage: VisaAuthTokenStorage, private val setVisaPinCodeUseCase: SetVisaPinCodeUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -49,6 +52,10 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( val uiState = _uiState.asStateFlow() val onDone = MutableSharedFlow() + init { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinCodeScreenOpened) + } + private fun getInitialState(): OnboardingVisaPinCodeUM { return OnboardingVisaPinCodeUM( submitButtonEnabled = false, @@ -66,6 +73,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( pinCode = pin, submitButtonEnabled = PinCodeValidation.validate(pin), error = if (isError) { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ErrorPinValidation) resourceReference(R.string.visa_onboarding_pin_validation_error_message) } else { null @@ -76,6 +84,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( } private fun onSubmitClick() { + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinEntered) val pinCode = _uiState.value.pinCode if (PinCodeValidation.validate(pinCode).not()) return diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt index db3f28bd1a..35869da702 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model import com.tangem.common.CompletionResult import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -14,6 +15,9 @@ import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.features.onboarding.v2.visa.impl.child.welcome.OnboardingVisaWelcomeComponent.Config import com.tangem.features.onboarding.v2.visa.impl.child.welcome.OnboardingVisaWelcomeComponent.DoneEvent +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state.OnboardingVisaWelcomeUM import com.tangem.features.onboarding.v2.visa.impl.common.ActivationReadyEvent import com.tangem.sdk.api.TangemSdkManager @@ -33,6 +37,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( private val tangemSdkManager: TangemSdkManager, private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, private val uiMessageSender: UiMessageSender, + private val analyticsEventsHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -42,6 +47,10 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( val uiState = _uiState.asStateFlow() val onDone = MutableSharedFlow() + init { + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ActivationScreenOpened) + } + private fun getInitialState(): OnboardingVisaWelcomeUM { return OnboardingVisaWelcomeUM( mode = if (params is Config.WelcomeBack) { @@ -54,6 +63,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( } private fun onContinueClick() { + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ButtonActivate) if (params !is Config.WelcomeBack) { modelScope.launch { onDone.emit(DoneEvent.WelcomeDone) } return @@ -86,6 +96,9 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( is CompletionResult.Failure -> { loading(false) uiMessageSender.showErrorDialog(result.error.universalError) + analyticsEventsHandler.send( + VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE), + ) return@launch } is CompletionResult.Success -> result.data diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt new file mode 100644 index 0000000000..25759dce3a --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt @@ -0,0 +1,85 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +internal sealed class OnboardingVisaAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Onboarding / Visa", event, params) { + + data object ActivationScreenOpened : OnboardingVisaAnalyticsEvent( + event = "Activation Screen Opened", + ) + + data object ButtonActivate : OnboardingVisaAnalyticsEvent( + event = "Button - Activate", + ) + + data object SettingAccessCodeStarted : OnboardingVisaAnalyticsEvent( + event = "Setting Access Code Started", + ) + + data object AccessCodeEntered : OnboardingVisaAnalyticsEvent( + event = "Access Code Entered", + ) + + data object AccessCodeReenterScreen : OnboardingVisaAnalyticsEvent( + event = "Access Code Re-enter Screen", + ) + + data object OnboardingVisa : OnboardingVisaAnalyticsEvent( + event = "Onboarding / Visa", + ) + + data class ChooseWalletScreen( + val type: String, + ) : OnboardingVisaAnalyticsEvent( + event = "Choose Wallet Screen", + params = mapOf("Type" to type), + ) + + data object WalletPrepare : OnboardingVisaAnalyticsEvent( + event = "Wallet Prepare", + ) + + data object ButtonApprove : OnboardingVisaAnalyticsEvent( + event = "Button - Approve", + ) + + data object GoToWebsiteOpened : OnboardingVisaAnalyticsEvent( + event = "Go To Website Opened", + ) + + data object ButtonBrowser : OnboardingVisaAnalyticsEvent( + event = "Button - Browser", + ) + + data object ButtonShareLink : OnboardingVisaAnalyticsEvent( + event = "Button - Share Link", + ) + + data object ActivationInProgressScreen : OnboardingVisaAnalyticsEvent( + event = "Activation In Progress Screen", + ) + + data object PinCodeScreenOpened : OnboardingVisaAnalyticsEvent( + event = "PIN Code Screen Opened", + ) + + data object PinEntered : OnboardingVisaAnalyticsEvent( + event = "PIN Entered", + params = mapOf("Type" to "Visa"), + ) + + data object BiometricScreenOpened : OnboardingVisaAnalyticsEvent( + event = "Biometric Screen Opened", + ) + + data object SuccessScreenOpened : OnboardingVisaAnalyticsEvent( + event = "Success Screen Opened", + ) + + data object ErrorPinValidation : OnboardingVisaAnalyticsEvent( + event = "Error - Pin Validation", + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/VisaAnalyticsEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/VisaAnalyticsEvent.kt new file mode 100644 index 0000000000..f31e8c0a83 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/VisaAnalyticsEvent.kt @@ -0,0 +1,23 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +internal const val ONBOARDING_SOURCE = "Onboarding" +internal const val MAIN_SOURCE = "Main" + +internal sealed class VisaAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Visa", event, params) { + + data class Errors( + val errorCode: String, + val source: String, + ) : VisaAnalyticsEvent( + event = "Errors", + params = mapOf( + "Error Code" to errorCode, + "Source" to source, + ), + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt index 5f66b3f918..9aed66d0f3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.child.wallet.model.intents import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -40,13 +42,15 @@ internal class VisaWalletIntentsImplementor @Inject constructor( private val getUserWalletsUseCase: GetWalletsUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, ) : BaseWalletClickIntents(), VisaWalletIntents { private val balancesAndLimitsBottomSheetConverter by lazy(mode = LazyThreadSafetyMode.NONE) { - BalancesAndLimitsBottomSheetConverter(eventSender) + BalancesAndLimitsBottomSheetConverter(eventSender, analyticsEventHandler) } override fun onBalancesAndLimitsClick() { + analyticsEventHandler.send(MainScreenAnalyticsEvent.LimitsClicked) modelScope.launch(dispatchers.main) { val userWalletId = stateController.getSelectedWalletId() val balancesAndLimits = getVisaCurrencyUseCase(userWalletId) @@ -87,6 +91,7 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } override fun onExploreClick(exploreUrl: String) { + analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonExplore) router.openUrl(exploreUrl) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index 9068f9a7f1..be96aa27a4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -15,6 +15,7 @@ import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetCo import com.tangem.common.ui.bottomsheet.receive.AddressModel import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList @@ -78,7 +79,7 @@ interface WalletCurrencyActionsClickIntents { unavailabilityReason: ScenarioUnavailabilityReason, ) - fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent? = null) fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) @@ -94,7 +95,7 @@ interface WalletCurrencyActionsClickIntents { fun onAnalyticsClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onMultiWalletBuyClick(userWalletId: UserWalletId) + fun onMultiWalletBuyClick(userWalletId: UserWalletId, screenType: String) fun onMultiWalletSellClick(userWalletId: UserWalletId) @@ -151,7 +152,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( appRouter.push(route) } - override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent?) { val userWalletId = stateHolder.getSelectedWalletId() analyticsEventHandler.send( @@ -166,6 +167,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrencyStatus.currency.symbol), ) + event?.let { analyticsEventHandler.send(it) } + stateHolder.showBottomSheet( createReceiveBottomSheetContent( currency = cryptoCurrencyStatus.currency, @@ -470,7 +473,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } - override fun onMultiWalletBuyClick(userWalletId: UserWalletId) { + override fun onMultiWalletBuyClick(userWalletId: UserWalletId, screenType: String) { onMultiWalletActionClick( statusFlow = if (onrampFeatureToggles.isFeatureEnabled) { rampStateManager.getExpressInitializationStatus(userWalletId) @@ -478,7 +481,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( rampStateManager.getBuyInitializationStatus() }, route = AppRoute.BuyCrypto(userWalletId = userWalletId), - eventCreator = MainScreenAnalyticsEvent::ButtonBuy, + eventCreator = { MainScreenAnalyticsEvent.ButtonBuy(status = it, screenType = screenType) }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt index 1d65f129ae..1dc3f95565 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt @@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import arrow.core.Either import arrow.core.getOrElse +import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent +import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent.Companion.VISA_TYPE import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat @@ -144,17 +146,23 @@ internal class SetVisaInfoTransformer( ), ) - return return persistentListOf( + return persistentListOf( WalletManageButton.Receive( enabled = true, dimContent = false, - onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus = cryptoCurrencyStatus) }, - onLongClick = { clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus = cryptoCurrencyStatus) }, + onClick = { + clickIntents.onReceiveClick(cryptoCurrencyStatus, MainScreenAnalyticsEvent.ButtonReceive) + }, + onLongClick = { + clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus) + }, ), WalletManageButton.Buy( enabled = true, dimContent = false, - onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId) }, + onClick = { + clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId, screenType = VISA_TYPE) + }, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt index ffb4722637..eb2e240d23 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt @@ -1,5 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.DateTimeFormatters @@ -13,6 +15,7 @@ import java.math.BigDecimal internal class BalancesAndLimitsBottomSheetConverter( private val eventSender: WalletEventSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Converter { override fun convert(value: VisaCurrency): BalancesAndLimitsBottomSheetConfig { @@ -28,23 +31,25 @@ internal class BalancesAndLimitsBottomSheetConverter( blockedBalance = value.balances.blocked.let(::formatAmount), debit = value.balances.debt.let(::formatAmount), amlVerified = value.balances.verified.let(::formatAmount), - onInfoClick = this::showBalanceInfo, + onInfoClick = this::balanceInfoOnClick, ), limit = BalancesAndLimitsBottomSheetConfig.Limit( availableBy = DateTimeFormatters.formatDate(date = value.limits.expirationDate), total = otpLimit, other = noOtpLimit, singleTransaction = value.limits.singleTransaction.let(::formatAmount), - onInfoClick = { showLimitInfo(otpLimit, noOtpLimit) }, + onInfoClick = { limitInfoOnClick(otpLimit, noOtpLimit) }, ), ) } - private fun showBalanceInfo() { + private fun balanceInfoOnClick() { + analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeBalancesInfo) eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo)) } - private fun showLimitInfo(totalLimit: String, otherLimit: String) { + private fun limitInfoOnClick(totalLimit: String, otherLimit: String) { + analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeLimitsInfo) eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo(totalLimit, otherLimit))) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 439993d834..4cb5daf3c1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.utils +import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent.Companion.WALLET_TYPE import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -103,7 +104,7 @@ internal class WalletLoadingStateFactory( WalletManageButton.Buy( enabled = true, dimContent = false, - onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId) }, + onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId, WALLET_TYPE) }, ), WalletManageButton.Swap( enabled = true, From e125eff4719d120f12db3ce2d7ca0bc8eb93f77a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 11:50:10 +0400 Subject: [PATCH 28/72] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 14 ++++- .../com/tangem/tap/domain/extensions/Card.kt | 3 +- .../model/UserWalletInformation.kt | 2 - .../tap/domain/userWalletList/utils/Mapper.kt | 2 - .../redux/OnboardingWalletMiddleware.kt | 4 +- .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../card/DefaultDerivationsRepositoryTest.kt | 1 - .../BiometricUserWalletsListManagerTest.kt | 1 - .../domain/wallet/MockUserWalletFactory.kt | 1 - .../common/ui/userwallet/UserWalletItem.kt | 62 ++++++++++--------- .../converter/UserWalletItemUMConverter.kt | 7 ++- .../ui/userwallet/state/UserWalletItemUM.kt | 13 +++- .../configs/feature_toggles_config.json | 4 ++ .../core/ui/components/artwork/ArtworkUM.kt | 17 +++++ .../com/tangem/domain/models/ArtworkModel.kt | 6 ++ .../domain/wallets/models/UserWallet.kt | 3 - .../wallets/builder/UserWalletBuilder.kt | 5 +- .../wallets/usecase/GetCardImageUseCase.kt | 34 ++++++++-- .../preview/PreviewUserWalletListComponent.kt | 3 - .../details/utils/UserWalletsFetcher.kt | 30 ++++++++- .../PreviewAddToPortfolioBSContentProvider.kt | 1 - .../onboarding/v2/common/ui/WalletCard.kt | 5 +- .../impl/model/OnboardingMultiWalletModel.kt | 34 ++++++---- .../impl/ui/OnboardingMultiWallet.kt | 10 +-- .../v2/multiwallet/impl/ui/WalletArtwork.kt | 31 +++++----- .../impl/ui/state/OnboardingMultiWalletUM.kt | 8 ++- .../model/OnboardingNoteCreateWalletModel.kt | 7 ++- .../ui/OnboardingNoteCreateWalletScreen.kt | 8 +-- .../ui/state/OnboardingNoteCreateWalletUM.kt | 4 +- .../topup/model/OnboardingNoteTopUpModel.kt | 2 +- .../topup/ui/OnboardingNoteTopUpHeader.kt | 8 ++- .../topup/ui/OnboardingNoteTopUpScreen.kt | 2 +- .../topup/ui/state/OnboardingNoteTopUpUM.kt | 3 +- .../impl/model/OnboardingNoteCommonState.kt | 3 +- .../v2/note/impl/model/OnboardingNoteModel.kt | 11 ++-- .../v2/twin/impl/ui/TwinWalletArtwork.kt | 5 +- gradle/tangem_dependencies.toml | 2 +- .../com/tangem/sdk/api/di/CardSdkModule.kt | 7 +++ .../featuretoggles/CardSdkFeatureToggles.kt | 2 + .../DefaultCardSdkFeatureToggles.kt | 3 + 40 files changed, 251 insertions(+), 119 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/artwork/ArtworkUM.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/ArtworkModel.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 9be71f685e..4096521b75 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -14,6 +14,8 @@ import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSy import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.features.nft.NFTFeatureToggles +import com.tangem.operations.attestation.CardArtworksProvider +import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -172,8 +174,16 @@ internal object WalletsDomainModule { @Provides @Singleton - fun provideGetCardImageUseCase(onlineCardVerifier: OnlineCardVerifier): GetCardImageUseCase { - return GetCardImageUseCase(verifier = onlineCardVerifier) + fun provideGetCardImageUseCase( + onlineCardVerifier: OnlineCardVerifier, + cardArtworksProvider: CardArtworksProvider, + cardSdkFeatureToggles: CardSdkFeatureToggles, + ): GetCardImageUseCase { + return GetCardImageUseCase( + verifier = onlineCardVerifier, + cardArtworksProvider = cardArtworksProvider, + cardSdkFeatureToggles = cardSdkFeatureToggles, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index 7117819925..547dfbd6ce 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -6,6 +6,7 @@ import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.Artwork +import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo @@ -37,7 +38,7 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl( if (artworkId.isNullOrEmpty()) { ifAnyError() } else { - OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId) + CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId) } } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt index e7a9f1873f..95e75741b7 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt @@ -21,8 +21,6 @@ internal data class UserWalletPublicInformation( val name: String, @Json(name = "walletId") val walletId: UserWalletId, - @Json(name = "artworkUrl") - val artworkUrl: String, @Json(name = "cardsInWallet") val cardsInWallet: Set, @Json(name = "scanResponse") diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt index 4c73101416..8dca07bec0 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt @@ -15,7 +15,6 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation get() = UserWalletPublicInformation( name = name, walletId = walletId, - artworkUrl = artworkUrl, cardsInWallet = cardsInWallet, isMultiCurrency = isMultiCurrency, scanResponse = scanResponse.copy( @@ -31,7 +30,6 @@ internal fun UserWalletPublicInformation.toUserWallet(): UserWallet { return UserWallet( name = name, walletId = walletId, - artworkUrl = artworkUrl, cardsInWallet = cardsInWallet, scanResponse = scanResponse, isMultiCurrency = isMultiCurrency, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 3fefa3a904..fe64265670 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -31,7 +31,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator -import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.extensions.localizedDescriptionRes @@ -250,7 +250,7 @@ private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray, if (artworkId.isNullOrEmpty()) { defaultArtwork ?: Uri.EMPTY } else { - Uri.parse(OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)) + Uri.parse(CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)) } } is Result.Failure -> defaultArtwork ?: Uri.EMPTY diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 2525bdd3cd..9d8ccb0fbb 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -32,6 +32,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles +import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository @@ -76,5 +77,6 @@ data class DaggerGraphState( val settingsManager: SettingsManager? = null, val uiMessageSender: UiMessageSender? = null, val onlineCardVerifier: OnlineCardVerifier? = null, + val cardArworksProvider: CardArtworksProvider? = null, val userWalletBuilderFactory: UserWalletBuilder.Factory? = null, ) : StateType \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt index c66581f06d..7b9bd90813 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt @@ -39,7 +39,6 @@ internal class DefaultDerivationsRepositoryTest { private val defaultUserWallet = UserWallet( name = "", walletId = defaultUserWalletId, - artworkUrl = "", cardsInWallet = setOf(), isMultiCurrency = false, scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()), diff --git a/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt index 8180ca85ab..209dcbb443 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt @@ -194,7 +194,6 @@ internal class BiometricUserWalletsListManagerTest(private val model: Model) { return UserWallet( name = "Wallet $id", walletId = UserWalletId(stringValue = id), - artworkUrl = "", cardsInWallet = emptySet(), isMultiCurrency = true, hasBackupError = false, diff --git a/common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt index 365267e1ba..4c16726aa0 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt @@ -23,7 +23,6 @@ object MockUserWalletFactory { return UserWallet( walletId = userWalletId, name = "Wallet 1", - artworkUrl = "", cardsInWallet = emptySet(), scanResponse = scanResponse, isMultiCurrency = scanResponse.cardTypesResolver.isMultiwalletAllowed(), 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 c4a34ce6ee..b0d7438548 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 @@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material3.CardColors import androidx.compose.material3.Icon import androidx.compose.material3.Text -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 @@ -61,7 +61,7 @@ fun UserWalletItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - CardImage(imageUrl = state.imageUrl) + CardImage(state.imageState) NameAndInfo( modifier = Modifier.weight(1f), name = state.name, @@ -147,39 +147,49 @@ private fun NameAndInfo( } @Composable -private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) { +private fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modifier) { val imageModifier = modifier .width(TangemTheme.dimens.size24) .height(TangemTheme.dimens.size36) .clip(TangemTheme.shapes.roundedCornersSmall) - SubcomposeAsyncImage( - modifier = imageModifier, - model = ImageRequest.Builder(LocalContext.current) - .transformations(RotationTransformation(angle = 90f)) - .size( - width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }, - height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() }, - ) - .data(imageUrl) - .crossfade(enable = true) - .allowHardware(enable = false) - .build(), - loading = { + when (imageState) { + is UserWalletItemUM.ImageState.Loading -> { RectangleShimmer( modifier = imageModifier, radius = TangemTheme.dimens.size2, ) - }, - error = { - Image( + } + is UserWalletItemUM.ImageState.Image -> { + SubcomposeAsyncImage( modifier = imageModifier, - imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36), + model = ImageRequest.Builder(LocalContext.current) + .transformations(RotationTransformation(angle = 90f)) + .size( + width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }, + height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() }, + ) + .data(imageState.artwork.verifiedArtwork?.toByteArray() ?: imageState.artwork.defaultUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { + RectangleShimmer( + modifier = imageModifier, + radius = TangemTheme.dimens.size2, + ) + }, + error = { + Image( + modifier = imageModifier, + imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36), + contentDescription = null, + ) + }, contentDescription = null, ) - }, - contentDescription = null, - ) + } + } } @Composable @@ -215,7 +225,6 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider { override fun convert(value: UserWallet): UserWalletItemUM { @@ -40,10 +43,12 @@ class UserWalletItemUMConverter( name = stringReference(name), information = getInfo(userWallet = this), balance = getBalanceInfo(userWallet = this), - imageUrl = artworkUrl, isEnabled = !isLocked, endIcon = endIcon, onClick = { onClick(value.walletId) }, + imageState = artwork?.let { + UserWalletItemUM.ImageState.Image(ArtworkUM(it.verifiedArtwork, it.defaultUrl)) + } ?: UserWalletItemUM.ImageState.Loading, ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt index 04b6329ce9..a986ba5b62 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -1,5 +1,6 @@ package com.tangem.common.ui.userwallet.state +import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId import javax.annotation.concurrent.Immutable @@ -10,7 +11,7 @@ data class UserWalletItemUM( val name: TextReference, val information: TextReference, val balance: Balance, - val imageUrl: String, + val imageState: ImageState = ImageState.Loading, val isEnabled: Boolean, val endIcon: EndIcon = EndIcon.None, val onClick: () -> Unit, @@ -36,4 +37,14 @@ data class UserWalletItemUM( val isFlickering: Boolean, ) : Balance() } + + @Immutable + sealed class ImageState { + + data object Loading : ImageState() + + data class Image( + val artwork: ArtworkUM, + ) : ImageState() + } } \ No newline at end of file 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 c0d2c19ebd..526a32d276 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 @@ -43,6 +43,10 @@ "name": "NOTE_REFACTORING_ENABLED", "version": "5.23.0" }, + { + "name": "NEW_ARTWORK_LOADING", + "version": "5.24.0" + }, { "name": "NEW_ATTESTATION_ENABLED", "version": "5.23.0" diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/artwork/ArtworkUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/artwork/ArtworkUM.kt new file mode 100644 index 0000000000..5c1f568b02 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/artwork/ArtworkUM.kt @@ -0,0 +1,17 @@ +package com.tangem.core.ui.components.artwork + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Immutable +data class ArtworkUM( + val verifiedArtwork: ImmutableList? = null, + val defaultUrl: String, +) { + + constructor(bytes: ByteArray?, defaultUrl: String) : this( + verifiedArtwork = bytes?.toList()?.toImmutableList(), + defaultUrl = defaultUrl, + ) +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/ArtworkModel.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/ArtworkModel.kt new file mode 100644 index 0000000000..777d3355d8 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/ArtworkModel.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.models + +data class ArtworkModel( + val verifiedArtwork: ByteArray? = null, + val defaultUrl: String, +) \ No newline at end of file diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt index 95b44fff95..df15cfd819 100644 --- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt @@ -10,7 +10,6 @@ import com.tangem.domain.models.scan.ScanResponse * * @property name User wallet name * @property walletId User wallet [UserWalletId] - * @property artworkUrl User wallet card artwork URL * @property cardsInWallet List of cards IDs assigned with this user's wallet. The list will be empty if the wallet * has been backed up on another device. * @property isMultiCurrency Indicates whether this user wallet can work with more than one currency @@ -22,8 +21,6 @@ data class UserWallet( val name: String, @Json(name = "walletId") val walletId: UserWalletId, - @Json(name = "artworkUrl") - val artworkUrl: String, @Json(name = "cardsInWallet") val cardsInWallet: Set, @Json(name = "isMultiCurrency") diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt index dfaec8126c..7053d98d7d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt @@ -5,7 +5,6 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase -import com.tangem.domain.wallets.usecase.GetCardImageUseCase import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -13,7 +12,6 @@ import dagger.assisted.AssistedInject class UserWalletBuilder @AssistedInject constructor( @Assisted private val scanResponse: ScanResponse, private val generateWalletNameUseCase: GenerateWalletNameUseCase, - private val getCardImageUseCase: GetCardImageUseCase, ) { private var backupCardsIds: Set = emptySet() private var hasBackupError: Boolean = false @@ -38,7 +36,7 @@ class UserWalletBuilder @AssistedInject constructor( this.hasBackupError = hasBackupError } - suspend fun build(): UserWallet? { + fun build(): UserWallet? { return with(scanResponse) { UserWalletIdBuilder.scanResponse(scanResponse) .build() @@ -50,7 +48,6 @@ class UserWalletBuilder @AssistedInject constructor( isBackupNotAllowed = card.isBackupNotAllowed, isStartToCoin = cardTypesResolver.isStart2Coin(), ), - artworkUrl = getCardImageUseCase.invoke(card.cardId, card.cardPublicKey), cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(), diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt index b44e440301..048729f9e9 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt @@ -4,8 +4,12 @@ import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinsHelper +import com.tangem.domain.models.ArtworkModel import com.tangem.domain.wallets.models.Artwork +import com.tangem.operations.attestation.ArtworkSize +import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles /** * Use case for getting card image url @@ -14,7 +18,11 @@ import com.tangem.operations.attestation.OnlineCardVerifier * [REDACTED_AUTHOR] */ -class GetCardImageUseCase(private val verifier: OnlineCardVerifier) { +class GetCardImageUseCase( + private val verifier: OnlineCardVerifier, + private val cardArtworksProvider: CardArtworksProvider, + private val cardSdkFeatureToggles: CardSdkFeatureToggles, +) { /** * Get card image url @@ -22,14 +30,30 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier) { * @param cardId card id * @param cardPublicKey card public key */ - suspend operator fun invoke(cardId: String, cardPublicKey: ByteArray): String { + suspend operator fun invoke(cardId: String, cardPublicKey: ByteArray, size: ArtworkSize): ArtworkModel { + return if (cardSdkFeatureToggles.isNewArtworkLoadingEnabled) { + val result = cardArtworksProvider.getArtwork( + cardId = cardId, + cardPublicKey = cardPublicKey, + size = size, + ) + when (result) { + is Result.Failure -> ArtworkModel(null, getFallbackArtworkUrl(cardId)) + is Result.Success -> ArtworkModel(result.data, getFallbackArtworkUrl(cardId)) + } + } else { + ArtworkModel(null, getLegacyArtwork(cardId, cardPublicKey)) + } + } + + private suspend fun getLegacyArtwork(cardId: String, cardPublicKey: ByteArray): String { return when (val result = verifier.getCardInfo(cardId, cardPublicKey)) { is Result.Success -> { val artworkId = result.data.artwork?.id if (artworkId.isNullOrEmpty()) { getFallbackArtworkUrl(cardId) } else { - OnlineCardVerifier.getUrlForArtwork( + CardArtworksProvider.getUrlForArtwork( cardId = cardId, cardPublicKey = cardPublicKey.toHexString(), artworkId = artworkId, @@ -41,8 +65,6 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier) { } } - fun getDefaultFallbackUrl(): String = Artwork.DEFAULT_IMG_URL - private fun getFallbackArtworkUrl(cardId: String): String { return when { cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL @@ -50,7 +72,7 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier) { else -> when (TwinsHelper.getTwinCardNumber(cardId)) { TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL - else -> getDefaultFallbackUrl() + else -> Artwork.DEFAULT_IMG_URL } } } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index d9bdebec63..7d8f07f26d 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -29,7 +29,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { name = stringReference("My Wallet"), information = getInformation(cardCount = 1), balance = UserWalletItemUM.Balance.Loading, - imageUrl = "", isEnabled = true, onClick = {}, ), @@ -38,7 +37,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { name = stringReference("Old wallet"), information = getInformation(cardCount = 2), balance = UserWalletItemUM.Balance.Loading, - imageUrl = "", isEnabled = true, onClick = {}, ), @@ -47,7 +45,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { name = stringReference("Multi Card"), information = getInformation(cardCount = 3), balance = UserWalletItemUM.Balance.Loading, - imageUrl = "", isEnabled = false, onClick = {}, ), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index eef9deef0d..23cde30f76 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -18,19 +18,23 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.core.utils.toLce +import com.tangem.domain.models.ArtworkModel import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.details.impl.R +import com.tangem.operations.attestation.ArtworkSize import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class UserWalletsFetcher @Inject constructor( getWalletsUseCase: GetWalletsUseCase, @@ -39,8 +43,11 @@ internal class UserWalletsFetcher @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val router: Router, private val messageSender: UiMessageSender, + private val getCardImageUseCase: GetCardImageUseCase, ) { + private var loadedArtworks: HashMap = hashMapOf() + @OptIn(ExperimentalCoroutinesApi::class) val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> val uiModels = UserWalletItemUMConverter(onClick = ::navigateToWalletSettings).convertList(wallets) @@ -52,12 +59,14 @@ internal class UserWalletsFetcher @Inject constructor( flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(), flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(), flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(), - ) { maybeAppCurrency, balanceHidingSettings, maybeBalances -> + flow4 = loadArtworks(wallets), + ) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks -> val models = createUiModels( wallets = wallets, maybeAppCurrency = maybeAppCurrency, maybeBalances = maybeBalances, balanceHidingSettings = balanceHidingSettings, + artworks = artworks, ).getOrElse( ifLoading = { return@combine }, ifError = { @@ -72,11 +81,27 @@ internal class UserWalletsFetcher @Inject constructor( }.collect() } + private fun loadArtworks(wallets: List): Flow> { + return flow { + emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish + wallets.forEach { wallet -> + val artwork = getCardImageUseCase( + cardId = wallet.cardId, + cardPublicKey = wallet.scanResponse.card.cardPublicKey, + size = ArtworkSize.SMALL, + ) + loadedArtworks[wallet.walletId] = artwork + emit(loadedArtworks) + } + } + } + private fun createUiModels( wallets: List, maybeAppCurrency: Either, maybeBalances: Lce>, balanceHidingSettings: BalanceHidingSettings, + artworks: HashMap, ): Lce> = lce { val balances = withError( transform = { Error.UnableToGetBalances }, @@ -99,6 +124,7 @@ internal class UserWalletsFetcher @Inject constructor( appCurrency = appCurrency, balance = balance, isBalanceHidden = balanceHidingSettings.isBalanceHidden, + artwork = artworks[userWallet.walletId], ) .convert(userWallet) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt index 16c72a0b96..4dd21ecb8a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -27,7 +27,6 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider name = stringReference("Wallet 1"), information = stringReference("3 cards"), balance = UserWalletItemUM.Balance.Loading, - imageUrl = "", isEnabled = true, endIcon = UserWalletItemUM.EndIcon.Arrow, onClick = {}, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt index 79f9406a97..05e2c4fd39 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt @@ -7,14 +7,15 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import coil.compose.AsyncImage import coil.request.ImageRequest +import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.features.onboarding.v2.impl.R @Composable -internal fun WalletCard(url: String?, modifier: Modifier = Modifier) { +internal fun WalletCard(artwork: ArtworkUM?, modifier: Modifier = Modifier) { AsyncImage( modifier = modifier, model = ImageRequest.Builder(LocalContext.current) - .data(url) + .data(artwork?.verifiedArtwork ?: artwork?.defaultUrl) .crossfade(true) .build(), placeholder = painterResource(R.drawable.card_placeholder_black), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index 2a35bf160f..c685d24d3c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -6,6 +6,7 @@ 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.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.onboarding.repository.OnboardingRepository @@ -16,6 +17,7 @@ import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM +import com.tangem.operations.attestation.ArtworkSize import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.BackupServiceHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -78,18 +80,26 @@ internal class OnboardingMultiWalletModel @Inject constructor( modelScope.launch { backups.collectLatest { backup -> when { - _uiState.value.artwork2Url == null && backup.card2 != null -> { + _uiState.value.artwork2 == null && backup.card2 != null -> { val artwork = - getCardImageUseCase.invoke(backup.card2.cardId, backup.card2.cardPublicKey) + getCardImageUseCase.invoke( + cardId = backup.card2.cardId, + cardPublicKey = backup.card2.cardPublicKey, + size = ArtworkSize.LARGE, + ) _uiState.update { - it.copy(artwork2Url = artwork) + it.copy(artwork2 = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl)) } } - _uiState.value.artwork3Url == null && backup.card3 != null -> { + _uiState.value.artwork3 == null && backup.card3 != null -> { val artwork = - getCardImageUseCase.invoke(backup.card3.cardId, backup.card3.cardPublicKey) + getCardImageUseCase.invoke( + cardId = backup.card3.cardId, + cardPublicKey = backup.card3.cardPublicKey, + size = ArtworkSize.LARGE, + ) _uiState.update { - it.copy(artwork3Url = artwork) + it.copy(artwork3 = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl)) } } } @@ -156,12 +166,14 @@ internal class OnboardingMultiWalletModel @Inject constructor( private fun loadCardArtwork() { modelScope.launch { val artwork = - getCardImageUseCase.invoke(params.scanResponse.card.cardId, params.scanResponse.card.cardPublicKey) + getCardImageUseCase.invoke( + cardId = params.scanResponse.card.cardId, + cardPublicKey = params.scanResponse.card.cardPublicKey, + size = ArtworkSize.LARGE, + ) - if (artwork != getCardImageUseCase.getDefaultFallbackUrl()) { - _uiState.update { - it.copy(artwork1Url = artwork) - } + _uiState.update { + it.copy(artwork1 = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl)) } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt index 4af97756ab..00b794fd58 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt @@ -25,9 +25,9 @@ internal fun OnboardingMultiWallet( ) { if (isSeedPhraseState.not()) { WalletArtworks( - url1 = state.artwork1Url, - url2 = state.artwork2Url, - url3 = state.artwork3Url, + artwork1 = state.artwork1, + artwork2 = state.artwork2, + artwork3 = state.artwork3, modifier = Modifier .padding(horizontal = 34.dp) .padding(top = 20.dp) @@ -53,7 +53,7 @@ internal fun OnboardingMultiWallet( private fun Preview() { TangemThemePreview { OnboardingMultiWallet( - state = OnboardingMultiWalletUM(artwork1Url = null), + state = OnboardingMultiWalletUM(artwork1 = null), artworksState = WalletArtworksState.Folded, isSeedPhraseState = false, childContent = { md -> @@ -73,7 +73,7 @@ private fun Preview() { private fun Preview2() { TangemThemePreview { OnboardingMultiWallet( - state = OnboardingMultiWalletUM(artwork1Url = null), + state = OnboardingMultiWalletUM(artwork1 = null), artworksState = WalletArtworksState.Folded, isSeedPhraseState = true, childContent = { md -> diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/WalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/WalletArtwork.kt index cb4b29a905..d199e807dc 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/WalletArtwork.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/WalletArtwork.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.zIndex +import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.common.ui.WalletCard @@ -57,9 +58,9 @@ sealed class WalletArtworksState { @Composable fun WalletArtworks( - url1: String?, - url2: String?, - url3: String?, + artwork1: ArtworkUM?, + artwork2: ArtworkUM?, + artwork3: ArtworkUM?, state: WalletArtworksState, modifier: Modifier = Modifier, ) { @@ -120,9 +121,9 @@ fun WalletArtworks( } AnimatedWalletCards( - url1 = url1, - url2 = url2, - url3 = url3, + artwork1 = artwork1, + artwork2 = artwork2, + artwork3 = artwork3, modifier = Modifier .widthIn(max = 450.dp) .matchParentSize(), @@ -152,9 +153,9 @@ private data class WalletCardTransitionState( @Suppress("LongParameterList") @Composable private fun AnimatedWalletCards( - url1: String?, - url2: String?, - url3: String?, + artwork1: ArtworkUM?, + artwork2: ArtworkUM?, + artwork3: ArtworkUM?, transition1: Transition, transition2: Transition, transition3: Transition, @@ -199,7 +200,7 @@ private fun AnimatedWalletCards( scaleY = scaleY3 rotationZ = rotation3 }, - url = url3, + artwork = artwork3, ) WalletCard( @@ -217,7 +218,7 @@ private fun AnimatedWalletCards( scaleX = scaleX2 scaleY = scaleY2 }, - url = url2, + artwork = artwork2, ) WalletCard( @@ -234,7 +235,7 @@ private fun AnimatedWalletCards( translationY = translationY1 rotationZ = rotation1 }, - url = url1, + artwork = artwork1, ) } } @@ -447,9 +448,9 @@ private fun Preview() { var state: WalletArtworksState by remember { mutableStateOf(WalletArtworksState.Folded) } WalletArtworks( - url1 = null, - url2 = null, - url3 = null, + artwork1 = null, + artwork2 = null, + artwork3 = null, state = state, modifier = Modifier.fillMaxWidth(), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt index 829828761d..3220607f0f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt @@ -1,7 +1,9 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state +import com.tangem.core.ui.components.artwork.ArtworkUM + internal data class OnboardingMultiWalletUM( - val artwork1Url: String? = null, - val artwork2Url: String? = null, - val artwork3Url: String? = null, + val artwork1: ArtworkUM? = null, + val artwork2: ArtworkUM? = null, + val artwork3: ArtworkUM? = null, ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt index 59f7b2e7ab..ce79faeb51 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt @@ -5,6 +5,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder @@ -93,9 +94,11 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( private fun observeArtwork() { modelScope.launch { - params.childParams.commonState.collect { + params.childParams.commonState.collect { state -> _uiState.value = _uiState.value.copy( - artworkUrl = it.cardArtworkUrl, + artwork = state.cardArtwork?.let { + ArtworkUM(it.verifiedArtwork, it.defaultUrl) + }, ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt index f12d83941a..91644d77bb 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt @@ -32,9 +32,9 @@ internal fun OnboardingNoteCreateWallet(state: OnboardingNoteCreateWalletUM, mod verticalArrangement = Arrangement.Bottom, ) { WalletArtworks( - url1 = state.artworkUrl, - url2 = null, - url3 = null, + artwork1 = state.artwork, + artwork2 = null, + artwork3 = null, modifier = Modifier .padding(top = 64.dp) .padding(horizontal = 24.dp) @@ -86,7 +86,7 @@ private fun OnboardingNoteCreatePreview() { TangemThemePreview { OnboardingNoteCreateWallet( state = OnboardingNoteCreateWalletUM( - artworkUrl = null, + artwork = null, createWalletInProgress = false, onCreateClick = {}, ), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/state/OnboardingNoteCreateWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/state/OnboardingNoteCreateWalletUM.kt index 950445d00d..d11347b973 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/state/OnboardingNoteCreateWalletUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/state/OnboardingNoteCreateWalletUM.kt @@ -1,7 +1,9 @@ package com.tangem.features.onboarding.v2.note.impl.child.create.ui.state +import com.tangem.core.ui.components.artwork.ArtworkUM + data class OnboardingNoteCreateWalletUM( - val artworkUrl: String? = null, + val artwork: ArtworkUM? = null, val createWalletInProgress: Boolean = false, val onCreateClick: () -> Unit = {}, ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index 021509cd04..bdac9d90ca 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -129,7 +129,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor( modelScope.launch { params.childParams.commonState.collect { _uiState.value = _uiState.value.copy( - cardArtworkUrl = it.cardArtworkUrl, + cardArtwork = it.cardArtwork, ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt index 64125adc75..9830905a93 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalTangemShimmer import com.tangem.core.ui.res.TangemTheme @@ -21,11 +22,12 @@ import com.tangem.features.onboarding.v2.common.ui.RefreshButton import com.tangem.features.onboarding.v2.common.ui.WalletCard import com.tangem.features.onboarding.v2.impl.R import com.valentinilk.shimmer.shimmer +import kotlinx.collections.immutable.ImmutableList @Composable fun OnboardingNoteTopUpHeader( balance: String, - cardArtworkUrl: String?, + cardArtwork: ArtworkUM?, isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, modifier: Modifier = Modifier, @@ -77,7 +79,7 @@ fun OnboardingNoteTopUpHeader( } WalletCard( modifier = Modifier.width(120.dp).align(Alignment.TopCenter), - url = cardArtworkUrl, + artwork = cardArtwork, ) RefreshButton( modifier = Modifier.align(Alignment.BottomCenter), @@ -93,7 +95,7 @@ private fun OnboardinNoteTopUpHeaderPreview() { TangemThemePreview { OnboardingNoteTopUpHeader( balance = "0.00000001 BTC", - cardArtworkUrl = "", + cardArtwork = ArtworkUM(null as ImmutableList?, ""), onRefreshBalanceClick = {}, isRefreshing = false, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt index b7bee01049..476d06b378 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt @@ -34,7 +34,7 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif ) { OnboardingNoteTopUpHeader( balance = state.balance, - cardArtworkUrl = state.cardArtworkUrl, + cardArtwork = state.cardArtwork, onRefreshBalanceClick = state.onRefreshBalanceClick, isRefreshing = state.isRefreshing, modifier = Modifier diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt index 327de0d586..c9d9ca3541 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt @@ -1,9 +1,10 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state +import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig data class OnboardingNoteTopUpUM( - val cardArtworkUrl: String? = null, + val cardArtwork: ArtworkUM? = null, val availableForBuy: Boolean = false, val availableForBuyLoading: Boolean = true, val balance: String = "", diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt index 8ebe5bcd8b..740f471257 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.note.impl.model +import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet @@ -7,6 +8,6 @@ import com.tangem.domain.wallets.models.UserWallet internal data class OnboardingNoteCommonState( val scanResponse: ScanResponse, val userWallet: UserWallet? = null, - val cardArtworkUrl: String? = null, + val cardArtwork: ArtworkUM? = null, val cryptoCurrencyStatus: CryptoCurrencyStatus? = null, ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt index 30733d3ff8..43303a39b2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt @@ -6,6 +6,7 @@ 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.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.features.onboarding.v2.common.ui.exitOnboardingDialog @@ -13,6 +14,7 @@ import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.OnboardingNoteInnerNavigationState import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute import com.tangem.features.onboarding.v2.note.impl.route.stepNum +import com.tangem.operations.attestation.ArtworkSize import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -50,7 +52,7 @@ internal class OnboardingNoteModel @Inject constructor( val commonUiState: MutableStateFlow = _uiState init { - loadArtworkUrl() + loadArtwork() } fun updateStepForNewRoute(route: OnboardingNoteRoute) { @@ -85,15 +87,16 @@ internal class OnboardingNoteModel @Inject constructor( } } - private fun loadArtworkUrl() { + private fun loadArtwork() { modelScope.launch { val cardInfo = params.scanResponse.card - val url = getCardImageUseCase( + val artwork = getCardImageUseCase.invoke( cardId = cardInfo.cardId, cardPublicKey = cardInfo.cardPublicKey, + size = ArtworkSize.LARGE, ) _uiState.update { - it.copy(cardArtworkUrl = url) + it.copy(cardArtwork = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl)) } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt index 5fba3214f8..97758be4e1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.util.fastForEach import androidx.compose.ui.zIndex import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.components.artwork.ArtworkUM import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -212,7 +213,7 @@ private fun AnimatedTwinCards( scaleX = scaleX2 scaleY = scaleY2 }, - url = Artwork.TWIN_CARD_2_URL, + artwork = ArtworkUM(defaultUrl = Artwork.TWIN_CARD_2_URL), ) WalletCard( @@ -229,7 +230,7 @@ private fun AnimatedTwinCards( translationY = translationY1 rotationZ = rotation1 }, - url = Artwork.TWIN_CARD_1_URL, + artwork = ArtworkUM(defaultUrl = Artwork.TWIN_CARD_1_URL), ) } } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index e6912e07bc..9021f713aa 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] tangemBlockchainSdk = "develop-1009" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-451" +tangemCardSdk = "develop-453" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt index 6a6cfde251..e0a1deedfc 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt @@ -1,5 +1,7 @@ package com.tangem.sdk.api.di +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.operations.attestation.OnlineCardVerifier import dagger.Module import dagger.Provides @@ -14,4 +16,9 @@ internal object CardSdkModule { @Provides @Singleton fun provideOnlineCardVerifier(): OnlineCardVerifier = OnlineCardVerifier() + + @Provides + @Singleton + fun provideCardArtworksProvider(sdkRepository: CardSdkConfigRepository): CardArtworksProvider = + CardArtworksProvider(sdkRepository.sdk.config.isTangemAttestationProdEnv) } \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt index b90f1577cc..7eb52af432 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt @@ -8,4 +8,6 @@ package com.tangem.sdk.api.featuretoggles interface CardSdkFeatureToggles { val isNewAttestationEnabled: Boolean + + val isNewArtworkLoadingEnabled: Boolean } \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt index 4146304590..0a12b3ce37 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt @@ -9,4 +9,7 @@ internal class DefaultCardSdkFeatureToggles @Inject constructor( override val isNewAttestationEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "NEW_ATTESTATION_ENABLED") + + override val isNewArtworkLoadingEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "NEW_ARTWORK_LOADING") } \ No newline at end of file From 19fa651a775396ecb22750ed31062a03e9e0781c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 10:23:30 +0500 Subject: [PATCH 29/72] Updated on 2026-08-14 --- .../features/send/v2/send/confirm/model/SendConfirmModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cb79ab86b1..cfe0d1fc26 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -32,7 +32,7 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.ui.state.ConfirmUM -import com.tangem.features.send.v2.common.ui.state.NavigationUM +import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents From 08f86f4d5248df3ee293ef80d42bea0beb1f535a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 11:25:34 +0400 Subject: [PATCH 30/72] Updated on 2026-08-14 --- .../tap/domain/tokens/DefaultTokensFeatureToggles.kt | 6 ++++++ .../src/main/assets/configs/feature_toggles_config.json | 8 ++++++++ .../datasource/appcurrency/AppCurrencyResponseStore.kt | 5 +++++ .../appcurrency/DefaultAppCurrencyResponseStore.kt | 6 ++++++ .../data/quotes/single/DefaultSingleQuoteProducer.kt | 4 ++++ .../data/quotes/single/DefaultSingleQuoteProducerTest.kt | 2 ++ .../com/tangem/domain/tokens/TokensFeatureToggles.kt | 4 ++++ .../tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt | 2 ++ 8 files changed, 37 insertions(+) diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index 2927924f8c..adc8e80741 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -9,4 +9,10 @@ internal class DefaultTokensFeatureToggles( override val isNetworksLoadingRefactoringEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED") + + override val isQuotesLoadingRefactoringEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "QUOTES_LOADING_REFACTORING_ENABLED") + + override val isStakingLoadingRefactoringEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_LOADING_REFACTORING_ENABLED") } \ No newline at end of file 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 526a32d276..66d5de0e26 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 @@ -66,5 +66,13 @@ { "name": "NETWORKS_LOADING_REFACTORING_ENABLED", "version": "5.23.0" + }, + { + "name": "QUOTES_LOADING_REFACTORING_ENABLED", + "version": "5.24.0" + }, + { + "name": "STAKING_LOADING_REFACTORING_ENABLED", + "version": "undefined" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt index 15d4280a28..5f9e2c12a9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.appcurrency import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import kotlinx.coroutines.flow.Flow /** * Store of app currency data model [CurrenciesResponse.Currency] @@ -9,5 +10,9 @@ import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse */ interface AppCurrencyResponseStore { + /** Get flow of [CurrenciesResponse.Currency] */ + fun get(): Flow + + /** Get [CurrenciesResponse.Currency] synchronously or null */ suspend fun getSyncOrNull(): CurrenciesResponse.Currency? } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt index 6d1b36f2fd..1c268c06c7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt @@ -3,7 +3,9 @@ package com.tangem.datasource.appcurrency import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import kotlinx.coroutines.flow.Flow /** * Default implementation of [AppCurrencyResponseStore] @@ -14,6 +16,10 @@ internal class DefaultAppCurrencyResponseStore( private val appPreferencesStore: AppPreferencesStore, ) : AppCurrencyResponseStore { + override fun get(): Flow { + return appPreferencesStore.getObject(PreferencesKeys.SELECTED_APP_CURRENCY_KEY) + } + override suspend fun getSyncOrNull(): CurrenciesResponse.Currency? { return appPreferencesStore.getObjectSyncOrNull( PreferencesKeys.SELECTED_APP_CURRENCY_KEY, 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 346bce56ca..d8450243b6 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 @@ -3,11 +3,13 @@ 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.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapNotNull /** @@ -19,6 +21,7 @@ import kotlinx.coroutines.flow.mapNotNull internal class DefaultSingleQuoteProducer @AssistedInject constructor( @Assisted val params: SingleQuoteProducer.Params, private val quotesStore: QuotesStoreV2, + private val dispatchers: CoroutineDispatcherProvider, ) : SingleQuoteProducer { override val fallback: Quote = Quote.Empty(rawCurrencyId = params.rawCurrencyId) @@ -27,6 +30,7 @@ internal class DefaultSingleQuoteProducer @AssistedInject constructor( return quotesStore.get() .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } } .distinctUntilChanged() + .flowOn(dispatchers.default) } @AssistedFactory 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/DefaultSingleQuoteProducerTest.kt index 655c97fad6..1b56374bae 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/DefaultSingleQuoteProducerTest.kt @@ -7,6 +7,7 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.quotes.single.SingleQuoteProducer import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -29,6 +30,7 @@ internal class DefaultSingleQuoteProducerTest { private val producer = DefaultSingleQuoteProducer( params = params, quotesStore = quotesStore, + dispatchers = TestingCoroutineDispatcherProvider(), ) @Test diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index b48aaa3c4d..be40961b10 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -8,4 +8,8 @@ package com.tangem.domain.tokens interface TokensFeatureToggles { val isNetworksLoadingRefactoringEnabled: Boolean + + val isQuotesLoadingRefactoringEnabled: Boolean + + val isStakingLoadingRefactoringEnabled: Boolean } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 3e5e99c917..b515f71dd9 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -180,6 +180,8 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { tokensFeatureToggles = object : TokensFeatureToggles { override val isNetworksLoadingRefactoringEnabled: Boolean = false + override val isQuotesLoadingRefactoringEnabled: Boolean = false + override val isStakingLoadingRefactoringEnabled: Boolean = false }, singleNetworkStatusSupplier = mockk(), multiNetworkStatusFetcher = mockk(), From de501988d06e837e21c0c6923dc6b54df4bffa82 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 13:34:13 +0300 Subject: [PATCH 31/72] Updated on 2026-08-14 --- .../CardActivationRemoteStateResponse.kt | 3 ++ core/res/src/main/res/values-ja/strings.xml | 5 +++ core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 3 +- .../VisaActivationStatusConverter.kt | 45 +++++++++++++++++++ .../visa/model/VisaActivationOrderInfo.kt | 4 +- .../visa/model/VisaActivationRemoteState.kt | 38 ++++++++++++++-- .../impl/DefaultOnboardingVisaComponent.kt | 5 ++- .../model/OnboardingVisaAccessCodeModel.kt | 1 + .../model/OnboardingVisaInProgressModel.kt | 40 ++++++++++++++--- .../model/OnboardingVisaOtherWalletModel.kt | 2 +- .../pincode/OnboardingVisaPinCodeComponent.kt | 1 + .../model/OnboardingVisaPinCodeModel.kt | 5 +++ .../v2/visa/impl/model/OnboardingVisaModel.kt | 7 ++- .../v2/visa/impl/route/OnboardingVisaRoute.kt | 5 ++- 15 files changed, 148 insertions(+), 17 deletions(-) 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/visa/models/response/CardActivationRemoteStateResponse.kt index e0ee6cc4f0..aeb498dac8 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/visa/models/response/CardActivationRemoteStateResponse.kt @@ -7,10 +7,13 @@ import com.squareup.moshi.JsonClass data class CardActivationRemoteStateResponse( @Json(name = "activation_status") val status: String, @Json(name = "activation_order") val activationOrder: ActivationOrder?, + @Json(name = "stepChangeCode") val stepChangeCode: Int?, + @Json(name = "updatedAt") val updatedAt: String?, ) { @JsonClass(generateAdapter = true) data class ActivationOrder( @Json(name = "id") val id: String, + @Json(name = "customer_id") val customerId: String, @Json(name = "customer_wallet_address") val customerWalletAddress: String, ) } \ No newline at end of file diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 874225b0fe..b59492d1ed 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -109,6 +109,8 @@ 報酬を受け取る 閉じる 確認 + Tangemサポートへ問い合わせる + Visaサポートへ問い合わせる 続ける コピー アドレスをコピー @@ -515,6 +517,7 @@ トークン標準 特徴 結果がありません。別のリクエストをお試しください。 + ネットワークを選択 私のウォレットへ NFTを受け取る %1$dコレクションの%2$dNFT @@ -1031,12 +1034,14 @@ 長くはかかりません。アカウントを設定しています。 長くはかかりません。アクティベーションを完了しています。 準備完了です! + PINの認証に失敗しました。もう一度お試しいただくか、別のコードを使用してください。 無効な暗証番号:連続や繰り返しを避けてください ウェブサイトに移動 アカウントの設定を続けましょう。 お帰りなさい! 手順に従ってアカウントを設定してください。 ようこそ! + この取引に異議を唱える ロック解除 カードをスキャンしてアクセスロックを解除する ロック解除が必要 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index de090f81f6..3ea8b2da0c 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1020,6 +1020,7 @@ Переименование кошелька Разблокировать все Разблокировать все с %s + ПИН не принят. Попробуйте ещё раз или введите другой код. Блокчейн недоступен. Попробуйте позже. Отсканируйте карту или кольцо Этот кошелек уже был активирован ранее.\nЕсли это сделали не вы, свяжитесь со службой поддержки.\nTangem никогда не продает кошелек вместе с предустановленным кодом доступа. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b2b7ab6c75..440dac2a29 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -528,11 +528,11 @@ Choose network To My wallet Receive NFT + Traits %1$d NFTs in %2$d collection Tap here to receive first NFT NFT collections Unable to load the data - Traits Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. @@ -1070,6 +1070,7 @@ Set up a 4-digit code. 
It will be used for payments. PIN code Create PIN Code + PIN was not accepted. Try again or use a different code. Invalid PIN: avoid sequences or repeats You\'re good to go! Prepare the Tangem card and tap to approve diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt index d82a09116a..b67a8484e1 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt @@ -1,15 +1,60 @@ package com.tangem.data.visa.converter import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse +import com.tangem.domain.visa.model.VisaActivationOrderInfo import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject +import javax.inject.Singleton +@Singleton class VisaActivationStatusConverter @Inject constructor() : Converter { + private val lastUpdatedAt = MutableStateFlow(null) + override fun convert(value: CardActivationRemoteStateResponse): VisaActivationRemoteState { + // handle pin code error + // either we entered pin code before with an error (WasError) or after receiving an error (InProgress) + if ( + value.status == Status.AwaitingPin.stringValue && + value.stepChangeCode != null && + value.stepChangeCode == PIN_CODE_VALIDATION_ERROR + ) { + return if (lastUpdatedAt.value != value.updatedAt) { + lastUpdatedAt.value == value.updatedAt + + VisaActivationRemoteState.AwaitingPinCode( + activationOrderInfo = value.activationOrder!!.convert(), + status = VisaActivationRemoteState.AwaitingPinCode.Status.WasError, + ) + } else { + VisaActivationRemoteState.AwaitingPinCode( + activationOrderInfo = value.activationOrder!!.convert(), + status = VisaActivationRemoteState.AwaitingPinCode.Status.InProgress, + ) + } + } + // TODO Will be implemented in the future return VisaActivationRemoteState.Activated } + + private fun CardActivationRemoteStateResponse.ActivationOrder.convert(): VisaActivationOrderInfo { + return VisaActivationOrderInfo( + orderId = id, + customerId = customerId, + customerWalletAddress = customerWalletAddress, + ) + } + + private enum class Status(val stringValue: String) { + AwaitingPin("AWAITING_PIN"), + // TODO complete statuses list when backend is ready + } + + private companion object { + const val PIN_CODE_VALIDATION_ERROR = 1000 + } } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationOrderInfo.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationOrderInfo.kt index 78bbd9692b..76389945fd 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationOrderInfo.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationOrderInfo.kt @@ -8,6 +8,6 @@ import kotlinx.serialization.Serializable @JsonClass(generateAdapter = true) data class VisaActivationOrderInfo( @Json(name = "orderId") val orderId: String, - @Json(name = "customer_id") val customerId: String, - @Json(name = "customer_wallet_address") val customerWalletAddress: String, + @Json(name = "customerId") val customerId: String, + @Json(name = "customerWalletAddress") val customerWalletAddress: String, ) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt index 88e4389287..889037bee3 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationRemoteState.kt @@ -20,9 +20,14 @@ sealed class VisaActivationRemoteState { data object PaymentAccountDeploying : VisaActivationRemoteState() @Serializable - data class WaitingPinCode( + data class AwaitingPinCode( val activationOrderInfo: VisaActivationOrderInfo, - ) : VisaActivationRemoteState() + val status: Status, + ) : VisaActivationRemoteState() { + enum class Status { + WaitingForPinCode, InProgress, WasError + } + } @Serializable data object WaitingForActivationFinishing : VisaActivationRemoteState() @@ -43,6 +48,7 @@ class VisaActivationRemoteState_Json( @Json(name = "type") val type: VisaActivationRemoteState_Type, @Json(name = "requestCardWallet") val requestCardWallet: VisaCardWalletDataToSignRequest? = null, @Json(name = "activationOrderInfo") val activationOrderInfo: VisaActivationOrderInfo? = null, + @Json(name = "awaiting_pin_code_status") val awaitingPinCodeStatus: AwaitingPinCodeStatus_Type? = null, ) @Suppress("ClassNaming") @@ -58,6 +64,12 @@ enum class VisaActivationRemoteState_Type { BlockedForActivation, } +@Suppress("ClassNaming") +@JsonClass(generateAdapter = false) +enum class AwaitingPinCodeStatus_Type { + WaitingForPinCode, InProgress, WasError +} + @Suppress("ClassNaming") class VisaActivationRemoteState_JsonAdapter { @@ -70,7 +82,17 @@ class VisaActivationRemoteState_JsonAdapter { VisaActivationRemoteState.CustomerWalletSignatureRequired(value.activationOrderInfo!!) VisaActivationRemoteState_Type.PaymentAccountDeploying -> VisaActivationRemoteState.PaymentAccountDeploying VisaActivationRemoteState_Type.WaitingPinCode -> - VisaActivationRemoteState.WaitingPinCode(value.activationOrderInfo!!) + VisaActivationRemoteState.AwaitingPinCode( + activationOrderInfo = value.activationOrderInfo!!, + status = when (value.awaitingPinCodeStatus!!) { + AwaitingPinCodeStatus_Type.WaitingForPinCode -> + VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode + AwaitingPinCodeStatus_Type.InProgress -> + VisaActivationRemoteState.AwaitingPinCode.Status.InProgress + AwaitingPinCodeStatus_Type.WasError -> + VisaActivationRemoteState.AwaitingPinCode.Status.WasError + }, + ) VisaActivationRemoteState_Type.WaitingForActivationFinishing -> VisaActivationRemoteState.WaitingForActivationFinishing VisaActivationRemoteState_Type.Activated -> VisaActivationRemoteState.Activated @@ -93,10 +115,18 @@ class VisaActivationRemoteState_JsonAdapter { ) is VisaActivationRemoteState.PaymentAccountDeploying -> VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.PaymentAccountDeploying) - is VisaActivationRemoteState.WaitingPinCode -> + is VisaActivationRemoteState.AwaitingPinCode -> VisaActivationRemoteState_Json( VisaActivationRemoteState_Type.WaitingPinCode, activationOrderInfo = value.activationOrderInfo, + awaitingPinCodeStatus = when (value.status) { + VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode -> + AwaitingPinCodeStatus_Type.WaitingForPinCode + VisaActivationRemoteState.AwaitingPinCode.Status.InProgress -> + AwaitingPinCodeStatus_Type.InProgress + VisaActivationRemoteState.AwaitingPinCode.Status.WasError -> + AwaitingPinCodeStatus_Type.WasError + }, ) is VisaActivationRemoteState.WaitingForActivationFinishing -> VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.WaitingForActivationFinishing) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt index b3cc93357a..c18f5585e7 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt @@ -167,7 +167,9 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( params = OnboardingVisaOtherWalletComponent.Params( childParams = childParams, onDone = { - model.stackNavigation.pushNew(OnboardingVisaRoute.PinCode(activationOrderInfo = it)) + model.stackNavigation.pushNew( + OnboardingVisaRoute.PinCode(activationOrderInfo = it, pinCodeValidationError = false), + ) }, ), ) @@ -176,6 +178,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( config = OnboardingVisaPinCodeComponent.Config( scanResponse = model.currentScanResponse.value, activationOrderInfo = route.activationOrderInfo, + wasValidationError = route.pinCodeValidationError, ), params = OnboardingVisaPinCodeComponent.Params( childParams = childParams, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index 7feeafe67c..a07b1acd3a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -92,6 +92,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( _uiState.update { it.copy( accessCodeFirst = textFieldValue, + accessCodeSecond = TextFieldValue(), atLeastMinCharsError = false, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index b409af7757..a138341670 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -75,9 +75,10 @@ internal class OnboardingVisaInProgressModel @Inject constructor( VisaActivationRemoteState.WaitingForActivationFinishing, -> { } - is VisaActivationRemoteState.WaitingPinCode -> { - navigateToPinCode(result.activationOrderInfo) - return@launch + is VisaActivationRemoteState.AwaitingPinCode -> { + navigateToPinCodeIfNeeded(result) { + return@launch + } } VisaActivationRemoteState.Activated -> { finishActivation() @@ -90,8 +91,37 @@ internal class OnboardingVisaInProgressModel @Inject constructor( } } - private suspend fun navigateToPinCode(activationOrderInfo: VisaActivationOrderInfo) { - onDone.emit(Params.DoneEvent.NavigateTo(OnboardingVisaRoute.PinCode(activationOrderInfo))) + private suspend inline fun navigateToPinCodeIfNeeded( + remoteState: VisaActivationRemoteState.AwaitingPinCode, + returnBlock: () -> Unit, + ) { + when (remoteState.status) { + VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode -> { + onDone.emit( + Params.DoneEvent.NavigateTo( + OnboardingVisaRoute.PinCode( + activationOrderInfo = remoteState.activationOrderInfo, + pinCodeValidationError = false, + ), + ), + ) + returnBlock() + } + VisaActivationRemoteState.AwaitingPinCode.Status.WasError -> { + onDone.emit( + Params.DoneEvent.NavigateTo( + OnboardingVisaRoute.PinCode( + activationOrderInfo = remoteState.activationOrderInfo, + pinCodeValidationError = true, + ), + ), + ) + returnBlock() + } + VisaActivationRemoteState.AwaitingPinCode.Status.InProgress -> { + /** waiting for new state */ + } + } } private suspend fun finishActivation() { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt index 4d89e3baa0..fa1adb5d3c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt @@ -54,7 +54,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( visaActivationRepository.getActivationRemoteStateLongPoll() }.getOrNull() - if (result is VisaActivationRemoteState.WaitingPinCode) { + if (result is VisaActivationRemoteState.AwaitingPinCode) { onDone.emit(result.activationOrderInfo) break } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt index b70d2995a1..baf52ede33 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt @@ -48,6 +48,7 @@ internal class OnboardingVisaPinCodeComponent( data class Config( val scanResponse: ScanResponse, val activationOrderInfo: VisaActivationOrderInfo, + val wasValidationError: Boolean, ) data class Params( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt index f3919153a9..432a024889 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -61,6 +61,11 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( submitButtonEnabled = false, onPinCodeChange = ::onPinCodeChange, onSubmitClick = ::onSubmitClick, + error = if (params.wasValidationError) { + resourceReference(R.string.visa_onboarding_pin_not_accepted) + } else { + null + }, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt index f93540c8ee..8bc8b14032 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt @@ -159,8 +159,11 @@ internal class OnboardingVisaModel @Inject constructor( VisaActivationRemoteState.WaitingForActivationFinishing -> { OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode) } - is VisaActivationRemoteState.WaitingPinCode -> { - OnboardingVisaRoute.PinCode(activationOrderInfo = remoteState.activationOrderInfo) + is VisaActivationRemoteState.AwaitingPinCode -> { + OnboardingVisaRoute.PinCode( + activationOrderInfo = remoteState.activationOrderInfo, + pinCodeValidationError = false, + ) } VisaActivationRemoteState.Activated, VisaActivationRemoteState.BlockedForActivation, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt index 9dca6484e9..762fc96544 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt @@ -48,5 +48,8 @@ internal sealed class OnboardingVisaRoute { } @Serializable - data class PinCode(val activationOrderInfo: VisaActivationOrderInfo) : OnboardingVisaRoute() + data class PinCode( + val activationOrderInfo: VisaActivationOrderInfo, + val pinCodeValidationError: Boolean, + ) : OnboardingVisaRoute() } \ No newline at end of file From a1751c70dd63f43b4c400d816d1adfe4c9af8aba Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 20:10:54 +0500 Subject: [PATCH 32/72] Updated on 2026-08-14 --- .../CommonSendRoute.kt} | 19 ++++--- .../features/send/v2/common/ui/SendContent.kt | 7 ++- .../send/v2/send/DefaultSendComponent.kt | 54 ++++++++++++------- .../v2/send/confirm/SendConfirmComponent.kt | 4 +- .../v2/send/confirm/model/SendConfirmModel.kt | 10 ++-- ...odelModule.kt => CommonSendModelModule.kt} | 2 +- .../features/send/v2/send/model/SendModel.kt | 4 +- .../amount/SendAmountComponentParams.kt | 7 +-- .../amount/model/SendAmountModel.kt | 17 +++--- .../SendDestinationComponentParams.kt | 7 +-- .../destination/model/SendDestinationModel.kt | 26 +++------ .../fee/SendFeeComponentParams.kt | 5 +- .../subcomponents/fee/model/SendFeeModel.kt | 18 +++---- 13 files changed, 91 insertions(+), 89 deletions(-) rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/{send/SendRoute.kt => common/CommonSendRoute.kt} (58%) rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/{SendModelModule.kt => CommonSendModelModule.kt} (94%) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt similarity index 58% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt index 078366292e..7493c4d073 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt @@ -1,35 +1,34 @@ -package com.tangem.features.send.v2.send +package com.tangem.features.send.v2.common import com.tangem.core.decompose.navigation.Route import kotlinx.serialization.Serializable -@Serializable -internal sealed class SendRoute : Route { +internal sealed class CommonSendRoute : Route { abstract val isEditMode: Boolean @Serializable - data object Empty : SendRoute() { + data object Empty : CommonSendRoute() { override val isEditMode = false } @Serializable - data object Confirm : SendRoute() { + data object Confirm : CommonSendRoute() { override val isEditMode: Boolean = false } @Serializable data class Destination( override val isEditMode: Boolean, - ) : SendRoute() + ) : CommonSendRoute() @Serializable data class Amount( override val isEditMode: Boolean, - ) : SendRoute() + ) : CommonSendRoute() @Serializable - data class Fee( - override val isEditMode: Boolean = true, - ) : SendRoute() + data object Fee : CommonSendRoute() { + override val isEditMode: Boolean = true + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index 265067a7df..83ce304653 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -13,11 +13,14 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.ui.state.NavigationUM @Composable -internal fun SendContent(navigationUM: NavigationUM, stackState: ChildStack) { +internal fun SendContent( + navigationUM: NavigationUM, + stackState: ChildStack, +) { Column( modifier = Modifier.Companion .background(color = TangemTheme.colors.background.tertiary) 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 07a7427df4..1f12ff1fdd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -19,6 +19,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.ui.SendContent import com.tangem.features.send.v2.common.ui.state.ConfirmUM @@ -45,17 +46,17 @@ internal class DefaultSendComponent @AssistedInject constructor( private val analyticsEventHandler: AnalyticsEventHandler, ) : SendComponent, AppComponentContext by appComponentContext { - private val stackNavigation = StackNavigation() + private val stackNavigation = StackNavigation() - private val innerRouter = InnerRouter( + private val innerRouter = InnerRouter( stackNavigation = stackNavigation, popCallback = { onChildBack() }, ) private val initialRoute = if (params.amount == null) { - SendRoute.Destination(isEditMode = false) + CommonSendRoute.Destination(isEditMode = false) } else { - SendRoute.Empty + CommonSendRoute.Empty } private val currentRoute = MutableStateFlow(initialRoute) @@ -118,42 +119,56 @@ internal class DefaultSendComponent @AssistedInject constructor( ) } - private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) { - SendRoute.Empty -> getStubComponent() - is SendRoute.Destination -> getDestinationComponent(factoryContext, route) - is SendRoute.Amount -> getAmountComponent(factoryContext, route) - is SendRoute.Fee -> getFeeComponent(factoryContext) - SendRoute.Confirm -> getConfirmComponent(factoryContext) + private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) { + CommonSendRoute.Empty -> getStubComponent() + is CommonSendRoute.Destination -> getDestinationComponent(factoryContext, route) + is CommonSendRoute.Amount -> getAmountComponent(factoryContext, route) + is CommonSendRoute.Fee -> getFeeComponent(factoryContext) + CommonSendRoute.Confirm -> getConfirmComponent(factoryContext) } - private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) = + private fun getDestinationComponent(factoryContext: AppComponentContext, route: CommonSendRoute) = SendDestinationComponent( appComponentContext = factoryContext, params = SendDestinationComponentParams.DestinationParams( state = model.uiState.value.destinationUM, - currentRoute = currentRoute.filterIsInstance(), + currentRoute = currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, userWalletId = params.userWalletId, cryptoCurrency = params.currency, callback = model, - isEditMode = route.isEditMode, + onBackClick = ::onChildBack, + onNextClick = { + if (route.isEditMode) { + onChildBack() + } else { + innerRouter.push(CommonSendRoute.Amount(isEditMode = false)) + } + }, ), ) - private fun getAmountComponent(factoryContext: AppComponentContext, route: SendRoute) = SendAmountComponent( + private fun getAmountComponent(factoryContext: AppComponentContext, route: CommonSendRoute) = SendAmountComponent( appComponentContext = factoryContext, params = SendAmountComponentParams.AmountParams( state = model.uiState.value.amountUM, - currentRoute = currentRoute.filterIsInstance(), + currentRoute = currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, userWallet = model.userWallet, appCurrency = model.appCurrency, cryptoCurrencyStatus = model.cryptoCurrencyStatus, callback = model, - isEditMode = route.isEditMode, predefinedValues = model.predefinedValues, + onBackClick = ::onChildBack, + onNextClick = { + if (route.isEditMode) { + onChildBack() + } else { + innerRouter.push(CommonSendRoute.Confirm) + } + }, ), ) @@ -166,7 +181,7 @@ internal class DefaultSendComponent @AssistedInject constructor( appComponentContext = factoryContext, params = SendFeeComponentParams.FeeParams( state = model.uiState.value.feeUM, - currentRoute = currentRoute.filterIsInstance(), + currentRoute = currentRoute.filterIsInstance(), analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, userWallet = model.userWallet, cryptoCurrencyStatus = model.cryptoCurrencyStatus, @@ -175,6 +190,7 @@ internal class DefaultSendComponent @AssistedInject constructor( sendAmount = sendAmount, destinationAddress = destinationAddress, callback = model, + onNextClick = ::onChildBack, ), ) } else { @@ -202,7 +218,7 @@ internal class DefaultSendComponent @AssistedInject constructor( params = SendConfirmComponent.Params( state = model.uiState.value, userWallet = model.userWallet, - currentRoute = currentRoute.filterIsInstance(), + currentRoute = currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, cryptoCurrencyStatus = model.cryptoCurrencyStatus, @@ -217,7 +233,7 @@ internal class DefaultSendComponent @AssistedInject constructor( private fun getStubComponent() = ComposableContentComponent { } private fun onChildBack() { - val isEmptyRoute = childStack.value.active.configuration == SendRoute.Empty + val isEmptyRoute = childStack.value.active.configuration == CommonSendRoute.Empty val isEmptyStack = childStack.value.backStack.isEmpty() val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success 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 3acce0b9c5..3725079e87 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 @@ -11,8 +11,8 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.PredefinedValues -import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel import com.tangem.features.send.v2.send.confirm.ui.SendConfirmContent import com.tangem.features.send.v2.common.ui.state.ConfirmUM @@ -141,7 +141,7 @@ internal class SendConfirmComponent( val feeCryptoCurrencyStatus: CryptoCurrencyStatus, val appCurrency: AppCurrency, val callback: ModelCallback, - val currentRoute: Flow, + val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, val predefinedValues: PredefinedValues, ) 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 cfe0d1fc26..1f06d93807 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 @@ -30,11 +30,11 @@ import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.ui.state.ConfirmUM -import com.tangem.features.send.v2.common.ui.state.ui.state.NavigationUM +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.SendRoute import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.send.analytics.SendAnalyticHelper @@ -164,7 +164,7 @@ internal class SendConfirmModel @Inject constructor( it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) } analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) - router.push(SendRoute.Destination(isEditMode = true)) + router.push(CommonSendRoute.Destination(isEditMode = true)) } } @@ -176,7 +176,7 @@ internal class SendConfirmModel @Inject constructor( it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) } analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount)) - router.push(SendRoute.Amount(isEditMode = true)) + router.push(CommonSendRoute.Amount(isEditMode = true)) } } @@ -188,7 +188,7 @@ internal class SendConfirmModel @Inject constructor( it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) } analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) - router.push(SendRoute.Fee()) + router.push(CommonSendRoute.Fee) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/SendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/CommonSendModelModule.kt similarity index 94% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/SendModelModule.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/CommonSendModelModule.kt index 7405859941..d2201019ff 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/SendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/CommonSendModelModule.kt @@ -12,7 +12,7 @@ import dagger.multibindings.IntoMap @Module @InstallIn(ModelComponent::class) -internal interface SendModelModule { +internal interface CommonSendModelModule { @Binds @IntoMap 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 ad59d24108..68e21e4fd1 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 @@ -29,10 +29,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.state.NavigationUM -import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.send.confirm.SendConfirmComponent import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory import com.tangem.features.send.v2.send.ui.state.SendUM @@ -215,7 +215,7 @@ internal class SendModel @Inject constructor( feeCryptoCurrencyStatus = feeCurrencyStatus if (params.amount != null) { - router.replaceAll(SendRoute.Confirm) + router.replaceAll(CommonSendRoute.Confirm) } } 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 05f4e2a828..7a59f9cfde 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 @@ -4,8 +4,8 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.PredefinedValues -import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -26,10 +26,11 @@ internal sealed class SendAmountComponentParams { override val appCurrency: AppCurrency, override val cryptoCurrencyStatus: CryptoCurrencyStatus, override val predefinedValues: PredefinedValues, - val isEditMode: Boolean, val callback: ModelCallback, - val currentRoute: Flow, + val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, + val onBackClick: () -> Unit, + val onNextClick: () -> Unit, ) : SendAmountComponentParams() data class AmountBlockParams( 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 18e832c369..b1e8483862 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 @@ -12,7 +12,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler 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.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -21,7 +20,6 @@ import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase 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 -import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener @@ -41,7 +39,6 @@ import javax.inject.Inject internal class SendAmountModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val router: Router, private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val sendAmountReduceListener: SendAmountReduceListener, private val analyticsEventHandler: AnalyticsEventHandler, @@ -161,11 +158,6 @@ internal class SendAmountModel @Inject constructor( ) } saveResult() - if ((params as? SendAmountComponentParams.AmountParams)?.isEditMode == true) { - router.pop() - } else { - router.push(SendRoute.Confirm) - } } private fun subscribeOnAmountReduceToTriggerUpdates() { @@ -223,7 +215,7 @@ internal class SendAmountModel @Inject constructor( if (!route.isEditMode) { saveResult() } - router.pop() + params.onBackClick() }, primaryButton = ButtonsUM.PrimaryButtonUM( text = if (route.isEditMode) { @@ -232,7 +224,10 @@ internal class SendAmountModel @Inject constructor( resourceReference(R.string.common_next) }, isEnabled = state.isPrimaryButtonEnabled, - onClick = ::onAmountNext, + onClick = { + onAmountNext() + params.onNextClick() + }, ), prevButton = ButtonsUM.PrimaryButtonUM( text = TextReference.EMPTY, @@ -240,7 +235,7 @@ internal class SendAmountModel @Inject constructor( isEnabled = true, onClick = { saveResult() - router.pop() + params.onBackClick() }, ).takeIf { route.isEditMode.not() }, secondaryPairButtonsUM = null, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt index 15b8c7bb6c..ecd05eba6d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt @@ -2,8 +2,8 @@ package com.tangem.features.send.v2.subcomponents.destination import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.PredefinedValues -import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent.ModelCallback import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import kotlinx.coroutines.flow.Flow @@ -22,9 +22,10 @@ internal sealed class SendDestinationComponentParams { override val cryptoCurrency: CryptoCurrency, override val userWalletId: UserWalletId, val isBalanceHidingFlow: StateFlow, - val currentRoute: Flow, + val currentRoute: Flow, val callback: ModelCallback, - val isEditMode: Boolean, + val onBackClick: () -> Unit, + val onNextClick: () -> Unit, ) : SendDestinationComponentParams() data class DestinationBlockParams( 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 c31c86c96d..f61949ffd7 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 @@ -3,7 +3,6 @@ package com.tangem.features.send.v2.subcomponents.destination.model import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -25,7 +24,6 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase 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 -import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.send.ui.state.ButtonsUM @@ -55,7 +53,6 @@ internal class SendDestinationModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val appRouter: AppRouter, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getWalletsUseCase: GetWalletsUseCase, @@ -272,7 +269,8 @@ internal class SendDestinationModel @Inject constructor( private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) { val isRecent = type == EnterAddressSource.RecentAddress - if (isRecent && isValidAddress && isValidMemo) onNextClick() + if (isRecent && isValidAddress && isValidMemo) saveResult() + (params as? SendDestinationComponentParams.DestinationParams)?.onNextClick?.invoke() } private fun saveResult() { @@ -280,15 +278,6 @@ internal class SendDestinationModel @Inject constructor( params.callback.onDestinationResult(uiState.value) } - private fun onNextClick() { - saveResult() - if ((params as? SendDestinationComponentParams.DestinationParams)?.isEditMode == true) { - router.pop() - } else { - router.push(SendRoute.Amount(isEditMode = false)) - } - } - private fun configDestinationNavigation() { val params = params as? SendDestinationComponentParams.DestinationParams ?: return combine( @@ -306,9 +295,7 @@ internal class SendDestinationModel @Inject constructor( R.drawable.ic_close_24 }, backIconClick = { - if (route.isEditMode) { - router.pop() - } else { + if (!route.isEditMode) { analyticsEventHandler.send( SendAnalyticEvents.CloseButtonClicked( source = SendScreenSource.Address, @@ -316,8 +303,8 @@ internal class SendDestinationModel @Inject constructor( isValid = state.isPrimaryButtonEnabled, ), ) - appRouter.pop() } + params.onBackClick() }, additionalIconRes = R.drawable.ic_qrcode_scan_24, additionalIconClick = ::onQrCodeScanClick, @@ -328,7 +315,10 @@ internal class SendDestinationModel @Inject constructor( resourceReference(R.string.common_next) }, isEnabled = state.isPrimaryButtonEnabled, - onClick = ::onNextClick, + onClick = { + saveResult() + params.onNextClick() + }, ), prevButton = null, secondaryPairButtonsUM = null, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt index 82c9f88081..20e3c94ded 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt @@ -3,7 +3,7 @@ package com.tangem.features.send.v2.subcomponents.fee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -29,8 +29,9 @@ internal sealed class SendFeeComponentParams { override val appCurrency: AppCurrency, override val sendAmount: BigDecimal, override val destinationAddress: String, - val currentRoute: Flow, + val currentRoute: Flow, val callback: SendFeeComponent.ModelCallback, + val onNextClick: () -> Unit, ) : SendFeeComponentParams() data class FeeBlockParams( 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 3a8d73d809..372dc1553a 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 @@ -6,7 +6,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler 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.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.transaction.usecase.GetFeeUseCase @@ -38,7 +37,6 @@ import javax.inject.Inject internal class SendFeeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val router: Router, private val isFeeApproximateUseCase: IsFeeApproximateUseCase, private val getFeeUseCase: GetFeeUseCase, private val urlOpener: UrlOpener, @@ -132,10 +130,10 @@ internal class SendFeeModel @Inject constructor( ), ) - navigate() + saveResult() } } else { - navigate() + saveResult() } } @@ -267,11 +265,6 @@ internal class SendFeeModel @Inject constructor( ) } - private fun navigate() { - saveResult() - router.pop() - } - private fun configFeeNavigation() { val params = params as? SendFeeComponentParams.FeeParams ?: return combine( @@ -284,11 +277,14 @@ internal class SendFeeModel @Inject constructor( title = resourceReference(R.string.common_fee_selector_title), subtitle = null, backIconRes = R.drawable.ic_back_24, - backIconClick = router::pop, + backIconClick = params.onNextClick, primaryButton = ButtonsUM.PrimaryButtonUM( text = resourceReference(R.string.common_continue), isEnabled = state.isPrimaryButtonEnabled, - onClick = ::onNextClick, + onClick = { + onNextClick() + params.onNextClick() + }, ), prevButton = null, secondaryPairButtonsUM = null, From eff63d40ab7596c2fc1c6c092b41d50d791da4ee Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 14:10:19 +0300 Subject: [PATCH 33/72] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 12 +++++++++--- settings.gradle.kts | 12 ++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 9021f713aa..884ee07a79 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,9 +1,15 @@ [versions] -tangemBlockchainSdk = "develop-1009" +# Blockchain SDK, Card SDK and Vico artifacts are built in a private namespace and then published to a +# public Maven repository, so anyone can build the app from source +# https://github.com/tangem/blockchain-sdk-kotlin/packages/ +# https://github.com/tangem/tangem-sdk-android/ +# https://github.com/tangem/vico + +tangemBlockchainSdk = "develop-1014" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-453" +tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemVico = "2.0.0-alpha.25-tangem-developments8" +tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/settings.gradle.kts b/settings.gradle.kts index 68b2aa945c..3f7384a646 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,7 +47,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/tangem-sdk-android") + url = uri("https://maven.pkg.github.com/tangem/tangem-sdk-android") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") @@ -56,7 +56,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/blst-android") + url = uri("https://maven.pkg.github.com/tangem/blst-android") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") @@ -65,7 +65,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/blockchain-sdk-kotlin") + url = uri("https://maven.pkg.github.com/tangem/blockchain-sdk-kotlin") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") @@ -74,7 +74,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/wallet-core") + url = uri("https://maven.pkg.github.com/tangem/wallet-core") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") @@ -86,7 +86,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/vico") + url = uri("https://maven.pkg.github.com/tangem/vico") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") @@ -97,7 +97,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/ic4j-agent") + url = uri("https://maven.pkg.github.com/tangem/ic4j-agent") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") From b1b39cd121196414b1873e1ea4bcd897539de19a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 14:03:10 +0400 Subject: [PATCH 34/72] Updated on 2026-08-14 --- .../tangem/common/test/utils/CoroutineExt.kt | 9 ++++----- .../DefaultMultiNetworkStatusProducerTest.kt | 18 +++++++++--------- .../DefaultSingleNetworkStatusProducerTest.kt | 16 ++++++++-------- .../NetworksStatusesStoreGetMethodTest.kt | 8 ++++---- .../single/DefaultSingleQuoteProducerTest.kt | 16 ++++++++-------- .../quotes/store/QuotesStoreGetMethodTest.kt | 6 +++--- 6 files changed, 36 insertions(+), 37 deletions(-) diff --git a/common/test/src/main/java/com/tangem/common/test/utils/CoroutineExt.kt b/common/test/src/main/java/com/tangem/common/test/utils/CoroutineExt.kt index 537e01b8f3..81467b2b4b 100644 --- a/common/test/src/main/java/com/tangem/common/test/utils/CoroutineExt.kt +++ b/common/test/src/main/java/com/tangem/common/test/utils/CoroutineExt.kt @@ -1,19 +1,18 @@ package com.tangem.common.test.utils -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher @OptIn(ExperimentalCoroutinesApi::class) -fun CoroutineScope.getEmittedValues(testScheduler: TestCoroutineScheduler, actual: Flow): List { +fun TestScope.getEmittedValues(flow: Flow): List { val values = mutableListOf() - launch(UnconfinedTestDispatcher(testScheduler)) { - actual.toList(values) + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + flow.toList(values) } return values diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt index 274e82eae5..6dc441b61c 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt @@ -65,7 +65,7 @@ internal class DefaultMultiNetworkStatusProducerTest { // check after producer.produce() verify { networksStatusesStore.get(params.userWalletId) } - val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values = getEmittedValues(flow = actual) // Check after flow was observed by subscriber (getEmittedValues). // Otherwise, userWalletsStore.getSyncOrNull is not called. @@ -95,7 +95,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networksStatusesFlow.emit(statuses.map(NetworkStatus::toSimple).toSet()) - val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values1 = getEmittedValues(flow = actual) // Check after flow was observed by subscriber (getEmittedValues). // Otherwise, userWalletsStore.getSyncOrNull is not called. @@ -112,7 +112,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networksStatusesFlow.emit(updatedStatuses.map(NetworkStatus::toSimple).toSet()) - val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values2 = getEmittedValues(flow = actual) // Check after flow was observed by subscriber (getEmittedValues). // Otherwise, userWalletsStore.getSyncOrNull is not called. @@ -143,7 +143,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networksStatusesFlow.emit(statuses.map(NetworkStatus::toSimple).toSet()) - val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values1 = getEmittedValues(flow = actual) // Check after flow was observed by subscriber (getEmittedValues). // Otherwise, userWalletsStore.getSyncOrNull is not called. @@ -155,7 +155,7 @@ internal class DefaultMultiNetworkStatusProducerTest { // second emit networksStatusesFlow.emit(statuses.map(NetworkStatus::toSimple).toSet()) - val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values2 = getEmittedValues(flow = actual) // Check after flow was observed by subscriber (getEmittedValues). // Otherwise, userWalletsStore.getSyncOrNull is not called. @@ -191,7 +191,7 @@ internal class DefaultMultiNetworkStatusProducerTest { // check after producer.produce() verify { networksStatusesStore.get(params.userWalletId) } - val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values1 = getEmittedValues(flow = actual) // Check after flow was observed by subscriber (getEmittedValues). // Otherwise, userWalletsStore.getSyncOrNull is not called. @@ -202,7 +202,7 @@ internal class DefaultMultiNetworkStatusProducerTest { innerFlow.emit(value = true) - val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values2 = getEmittedValues(flow = actual) // Check after flow was observed by subscriber (getEmittedValues). // Otherwise, userWalletsStore.getSyncOrNull is not called. @@ -222,7 +222,7 @@ internal class DefaultMultiNetworkStatusProducerTest { // check after producer.produce() verify { networksStatusesStore.get(params.userWalletId) } - val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values = getEmittedValues(flow = actual) verify(inverse = true) { userWalletsStore.getSyncOrNull(params.userWalletId) } @@ -248,7 +248,7 @@ internal class DefaultMultiNetworkStatusProducerTest { // check after producer.produce() verify { networksStatusesStore.get(params.userWalletId) } - val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values = getEmittedValues(flow = actual) // Check after flow was observed by subscriber (getEmittedValues). // Otherwise, userWalletsStore.getSyncOrNull is not called. diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt index 98894ea109..09a2126524 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt @@ -53,7 +53,7 @@ internal class DefaultSingleNetworkStatusProducerTest { verify { multiNetworkStatusSupplier(multiParams) } - val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values.size).isEqualTo(1) Truth.assertThat(values).isEqualTo(listOf(status)) @@ -74,7 +74,7 @@ internal class DefaultSingleNetworkStatusProducerTest { val status = MockNetworkStatusFactory.createMissedDerivation(params.network) expected.emit(value = setOf(status)) - val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values1 = getEmittedValues(flow = actual) Truth.assertThat(values1.size).isEqualTo(1) Truth.assertThat(values1).isEqualTo(listOf(status)) @@ -83,7 +83,7 @@ internal class DefaultSingleNetworkStatusProducerTest { val updatedStatus = status.copy(value = NetworkStatus.Unreachable(null)) expected.emit(value = setOf(updatedStatus)) - val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values2 = getEmittedValues(flow = actual) Truth.assertThat(values2.size).isEqualTo(2) Truth.assertThat(values2).isEqualTo(listOf(status, updatedStatus)) @@ -104,7 +104,7 @@ internal class DefaultSingleNetworkStatusProducerTest { val status = MockNetworkStatusFactory.createMissedDerivation(params.network) expected.emit(value = setOf(status)) - val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values1 = getEmittedValues(flow = actual) Truth.assertThat(values1.size).isEqualTo(1) Truth.assertThat(values1).isEqualTo(listOf(status)) @@ -112,7 +112,7 @@ internal class DefaultSingleNetworkStatusProducerTest { // second emit expected.emit(value = setOf(status)) - val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values2 = getEmittedValues(flow = actual) Truth.assertThat(values2.size).isEqualTo(1) Truth.assertThat(values2).isEqualTo(listOf(status)) @@ -140,7 +140,7 @@ internal class DefaultSingleNetworkStatusProducerTest { verify { multiNetworkStatusSupplier(multiParams) } - val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values1 = getEmittedValues(flow = actual) Truth.assertThat(values1.size).isEqualTo(1) val fallbackStatus = MockNetworkStatusFactory.createUnreachable(params.network) @@ -148,7 +148,7 @@ internal class DefaultSingleNetworkStatusProducerTest { innerFlow.emit(value = true) - val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values2 = getEmittedValues(flow = actual) Truth.assertThat(values2.size).isEqualTo(1) Truth.assertThat(values2).isEqualTo(listOf(status)) } @@ -166,7 +166,7 @@ internal class DefaultSingleNetworkStatusProducerTest { verify { multiNetworkStatusSupplier(multiParams) } - val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values.size).isEqualTo(0) } diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt index df73b079bb..522fcbeacc 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt @@ -30,7 +30,7 @@ internal class NetworksStatusesStoreGetMethodTest { fun `test get if runtime store is empty`() = runTest { val actual = store.get(userWalletId = userWalletId) - val values = backgroundScope.getEmittedValues(testScheduler, actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values).isEqualTo(emptyList>()) } @@ -41,7 +41,7 @@ internal class NetworksStatusesStoreGetMethodTest { val actual = store.get(userWalletId = userWalletId) - val values = backgroundScope.getEmittedValues(testScheduler, actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values).isEqualTo(emptyList>()) } @@ -54,7 +54,7 @@ internal class NetworksStatusesStoreGetMethodTest { val actual = store.get(userWalletId = userWalletId) - val values = backgroundScope.getEmittedValues(testScheduler, actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values.size).isEqualTo(1) Truth.assertThat(values).isEqualTo(listOf(emptySet())) @@ -70,7 +70,7 @@ internal class NetworksStatusesStoreGetMethodTest { val actual = store.get(userWalletId = userWalletId) - val values = backgroundScope.getEmittedValues(testScheduler, actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values.size).isEqualTo(1) Truth.assertThat(values).isEqualTo(listOf(setOf(status.toSimple()))) 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/DefaultSingleQuoteProducerTest.kt index 1b56374bae..f228d65673 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/DefaultSingleQuoteProducerTest.kt @@ -49,7 +49,7 @@ internal class DefaultSingleQuoteProducerTest { verify { quotesStore.get() } - val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values.size).isEqualTo(1) Truth.assertThat(values).isEqualTo(listOf(status)) @@ -69,7 +69,7 @@ internal class DefaultSingleQuoteProducerTest { val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) storeQuote.emit(value = setOf(status)) - val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values1 = getEmittedValues(flow = actual) Truth.assertThat(values1.size).isEqualTo(1) Truth.assertThat(values1).isEqualTo(listOf(status)) @@ -83,7 +83,7 @@ internal class DefaultSingleQuoteProducerTest { ) storeQuote.emit(value = setOf(updatedStatus)) - val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values2 = getEmittedValues(flow = actual) Truth.assertThat(values2.size).isEqualTo(2) Truth.assertThat(values2).isEqualTo(listOf(status, updatedStatus)) @@ -103,7 +103,7 @@ internal class DefaultSingleQuoteProducerTest { val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) storeQuote.emit(value = setOf(status)) - val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values1 = getEmittedValues(flow = actual) Truth.assertThat(values1.size).isEqualTo(1) Truth.assertThat(values1).isEqualTo(listOf(status)) @@ -111,7 +111,7 @@ internal class DefaultSingleQuoteProducerTest { // second emit storeQuote.emit(value = setOf(status)) - val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values2 = getEmittedValues(flow = actual) Truth.assertThat(values2.size).isEqualTo(1) Truth.assertThat(values2).isEqualTo(listOf(status)) @@ -143,7 +143,7 @@ internal class DefaultSingleQuoteProducerTest { verify { quotesStore.get() } - val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values1 = getEmittedValues(flow = actual) Truth.assertThat(values1.size).isEqualTo(1) val fallbackStatus = Quote.Empty(rawCurrencyId = params.rawCurrencyId) @@ -151,7 +151,7 @@ internal class DefaultSingleQuoteProducerTest { innerFlow.emit(value = true) - val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values2 = getEmittedValues(flow = actual) Truth.assertThat(values2.size).isEqualTo(1) Truth.assertThat(values2).isEqualTo(listOf(status)) } @@ -170,7 +170,7 @@ internal class DefaultSingleQuoteProducerTest { verify { quotesStore.get() } - val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values.size).isEqualTo(0) } 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 ede1171964..bbfd1053a6 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 @@ -30,7 +30,7 @@ internal class QuotesStoreGetMethodTest { fun `test get if runtime store is empty`() = runTest { val actual = store.get() - val values = backgroundScope.getEmittedValues(testScheduler, actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values).isEqualTo(emptyList>()) } @@ -41,7 +41,7 @@ internal class QuotesStoreGetMethodTest { val actual = store.get() - val values = backgroundScope.getEmittedValues(testScheduler, actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values).isEqualTo(listOf(emptySet())) } @@ -55,7 +55,7 @@ internal class QuotesStoreGetMethodTest { val actual = store.get() - val values = backgroundScope.getEmittedValues(testScheduler, actual) + val values = getEmittedValues(flow = actual) Truth.assertThat(values.size).isEqualTo(1) Truth.assertThat(values).isEqualTo(listOf(setOf(btcQuote.toDomain(), ethQuote.toDomain()))) From 5a134f4d33e873387929b69539e124678b939220 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 20:10:54 +0500 Subject: [PATCH 35/72] Updated on 2026-08-14 --- .../send/v2/common/CommonSendRoute.kt | 25 ++++++++++++++++- .../features/send/v2/sendnft/NFTSendRoute.kt | 28 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt index 7493c4d073..55f8d409d6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt @@ -1,34 +1,57 @@ package com.tangem.features.send.v2.common -import com.tangem.core.decompose.navigation.Route +import com.tangem.features.send.v2.common.CommonSendRoute import kotlinx.serialization.Serializable +<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt internal sealed class CommonSendRoute : Route { abstract val isEditMode: Boolean @Serializable data object Empty : CommonSendRoute() { +======== +@Serializable +internal sealed class SendRoute : CommonSendRoute { + + @Serializable + data object Empty : SendRoute(), CommonSendRoute.Empty { +>>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt override val isEditMode = false } @Serializable +<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt data object Confirm : CommonSendRoute() { +======== + data object Confirm : SendRoute(), CommonSendRoute.Confirm { +>>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt override val isEditMode: Boolean = false } @Serializable data class Destination( override val isEditMode: Boolean, +<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt ) : CommonSendRoute() +======== + ) : SendRoute(), CommonSendRoute.Destination +>>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt @Serializable data class Amount( override val isEditMode: Boolean, +<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt ) : CommonSendRoute() @Serializable data object Fee : CommonSendRoute() { +======== + ) : SendRoute(), CommonSendRoute.Amount + + @Serializable + data object Fee : SendRoute(), CommonSendRoute.Fee { +>>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt override val isEditMode: Boolean = true } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt new file mode 100644 index 0000000000..ccec86fa65 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt @@ -0,0 +1,28 @@ +package com.tangem.features.send.v2.sendnft + +import com.tangem.features.send.v2.common.CommonSendRoute +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class NFTSendRoute : CommonSendRoute { + + @Serializable + data object Empty : NFTSendRoute(), CommonSendRoute.Empty { + override val isEditMode = false + } + + @Serializable + data object Confirm : NFTSendRoute(), CommonSendRoute.Confirm { + override val isEditMode: Boolean = false + } + + @Serializable + data class Destination( + override val isEditMode: Boolean, + ) : NFTSendRoute(), CommonSendRoute.Destination + + @Serializable + data object Fee : NFTSendRoute(), CommonSendRoute.Fee { + override val isEditMode: Boolean = true + } +} \ No newline at end of file From 5762ba0d166c29ce8b6aa91785343f6bc47a5c7f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 18:43:42 +0500 Subject: [PATCH 36/72] Updated on 2026-08-14 --- .../staking/InvalidatePendingTransactionsUseCase.kt | 8 ++++---- .../state/converters/BalanceItemConverter.kt | 11 ++++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 2d48d88f25..40a8c174a9 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -1,10 +1,7 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.model.stakekit.Token +import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.repositories.StakingErrorResolver @@ -51,6 +48,9 @@ class InvalidatePendingTransactionsUseCase( } StakingActionType.WITHDRAW -> { modifyBalancesByStatus(balances, action, BalanceType.UNSTAKED) + if (token.network == NetworkType.TON) { + modifyBalancesByStatus(balances, action, BalanceType.PREPARING) + } } StakingActionType.UNLOCK_LOCKED -> { modifyBalancesByStatus(balances, action, BalanceType.LOCKED) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index dc8db5482a..ac9f4c0942 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -14,6 +14,7 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.lib.crypto.BlockchainUtils.isTon import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList @@ -60,12 +61,20 @@ internal class BalanceItemConverter( ), rawCurrencyId = value.rawCurrencyId, pendingActions = value.pendingActions.toPersistentList(), - isClickable = value.type.isClickable() && !value.isPending, + isClickable = value.isClickable(), isPending = value.isPending, ) } } + private fun BalanceItem.isClickable(): Boolean { + val networkId = cryptoCurrencyStatus.currency.network.id.value + return when { + isTon(networkId) -> pendingActions.any { it.type == StakingActionType.WITHDRAW } + else -> this.type.isClickable() && !this.isPending + } + } + private fun BalanceType.getTitle(validatorName: String?) = when (this) { BalanceType.PREPARING, BalanceType.STAKED, From 17c7ae519c18b47eb12b04d8e23c023ae63cf652 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Apr 2025 01:42:48 +0700 Subject: [PATCH 37/72] Updated on 2026-08-14 --- .../walletconnect/WalletConnectSdkHelper.kt | 34 +----- data/wallet-connect/build.gradle.kts | 1 + .../di/WalletConnectDataModule.kt | 3 - .../DefaultWcPersonalEthSignUseCase.kt | 110 ++++++++++++++++++ .../network/ethereum/WcEthNetwork.kt | 7 +- .../walletconnect/pair/WcPairSdkDelegate.kt | 2 +- .../sessions/DefaultWcSessionsManager.kt | 9 +- .../walletconnect/sign/BaseWcSignUseCase.kt | 3 + domain/wallet-connect/build.gradle.kts | 1 + .../domain/walletconnect/model/WcEthMethod.kt | 9 ++ .../domain/walletconnect/model/WcMethod.kt | 2 +- .../domain/walletconnect/usecase/WcUseCase.kt | 2 + .../usecase/ethereum/WcEthMethod.kt | 10 -- .../ethereum/WcPersonalEthSignUseCase.kt | 15 +++ 14 files changed, 154 insertions(+), 54 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt delete mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcPersonalEthSignUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index aa615456cc..cc73660628 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -4,7 +4,6 @@ import com.tangem.Message import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumUtils -import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.* @@ -16,6 +15,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType +import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper import com.tangem.operations.sign.SignHashCommand import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate @@ -357,36 +357,9 @@ class WalletConnectSdkHelper { ) } - private fun createMessageData(message: WcSignMessage): ByteArray { - val messageData = try { - message.data.removePrefix(HEX_PREFIX).hexToBytes() - } catch (exception: Exception) { - message.data.asciiToHex()?.hexToBytes() ?: byteArrayOf() - } + private fun createMessageData(message: WcSignMessage): ByteArray = LegacySdkHelper.createMessageData(message.data) - val prefixData = (ETH_MESSAGE_PREFIX + messageData.size.toString()).toByteArray() - return (prefixData + messageData).toKeccak() - } - - private fun String.hexToAscii(): String? { - return try { - removePrefix(HEX_PREFIX).hexToBytes() - .map { - val char = it.toInt().toChar() - if (char.isAscii()) char else return null - } - .joinToString("") - } catch (exception: Exception) { - return null - } - } - - private fun String.asciiToHex(): String? { - return map { - if (!it.isAscii()) return null - Integer.toHexString(it.code) - }.joinToString("") - } + private fun String.hexToAscii(): String? = LegacySdkHelper.hexToAscii(hex = this) suspend fun signPersonalMessage( hashToSign: ByteArray, @@ -535,7 +508,6 @@ class WalletConnectSdkHelper { "{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}" private companion object { - const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n" const val HEX_PREFIX = "0x" const val DEFAULT_MAX_GASLIMIT = 350000 // TODO remove after [REDACTED_JIRA] diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index 59061799bf..d1da51e0b0 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(projects.domain.walletConnect.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.card) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.models) 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 8c735d8bad..7ae5173300 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 @@ -30,7 +30,6 @@ import com.tangem.domain.walletconnect.request.WcRequestService import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -96,7 +95,6 @@ internal object WalletConnectDataModule { store: WalletConnectStore, dispatchers: CoroutineDispatcherProvider, legacyStore: WalletConnectSessionsRepository, - getUserWallet: GetUserWalletUseCase, getWallets: GetWalletsUseCase, ): DefaultWcSessionsManager { val scope = CoroutineScope(SupervisorJob() + dispatchers.io) @@ -105,7 +103,6 @@ internal object WalletConnectDataModule { dispatchers = dispatchers, legacyStore = legacyStore, getWallets = getWallets, - getUserWallet = getUserWallet, scope = scope, ) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt new file mode 100644 index 0000000000..d6d7cd9c1f --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt @@ -0,0 +1,110 @@ +package com.tangem.data.walletconnect.network.ethereum + +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak +import com.tangem.blockchain.common.HEX_PREFIX +import com.tangem.blockchain.extensions.isAscii +import com.tangem.common.CompletionResult +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toDecompressedPublicKey +import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.data.walletconnect.sign.BaseWcSignUseCase +import com.tangem.data.walletconnect.sign.OnSign +import com.tangem.data.walletconnect.sign.SignStateConverter.toResult +import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.domain.card.models.TwinKey +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.common.TapWorkarounds.isTangemTwins +import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.usecase.ethereum.WcPersonalEthSignUseCase +import com.tangem.domain.walletconnect.usecase.sign.WcSignState +import com.tangem.domain.walletmanager.WalletManagersFacade +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow + +internal class DefaultWcPersonalEthSignUseCase( + override val respondService: WcRespondService, + override val context: WcMethodUseCaseContext, + private val method: WcEthMethod.PersonalEthSign, + private val walletManagersFacade: WalletManagersFacade, + private val cardRepository: CardSdkConfigRepository, +) : BaseWcSignUseCase(), + WcPersonalEthSignUseCase { + + override val onSign: OnSign = collector@{ state -> + val hashToSign = LegacySdkHelper.createMessageData(state.signModel.rawMsg) + val userWallet = session.wallet + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network) + ?: return@collector + + val signer = cardRepository.getCommonSigner( + cardId = card.cardId.takeIf { isCardNotBackedUp }, + twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), + ) + + val signResult = when (val signResult = signer.sign(hashToSign, walletManager.wallet.publicKey)) { + is CompletionResult.Failure -> signResult.error.left() + is CompletionResult.Success -> signResult.data.right() + } + + val signedHash = signResult + .onLeft { emit(state.toResult(it.left())) } + .getOrNull() ?: return@collector + + val respond = EthereumUtils.prepareSignedMessageData( + signedHash = signedHash, + hashToSign = hashToSign, + publicKey = walletManager.wallet.publicKey.blockchainKey.toDecompressedPublicKey(), + ) + + val wcRespondResult = respondService.respond(rawSdkRequest, respond) + emit(state.toResult(wcRespondResult)) + } + + override fun invoke(): Flow> = flow { + val model = WcPersonalEthSignUseCase.SignModel( + rawMsg = method.message, + account = method.account, + humanMsg = LegacySdkHelper.hexToAscii(method.message).orEmpty(), + ) + emitAll(delegate(model)) + } +} + +object LegacySdkHelper { + private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n" + + fun createMessageData(message: String): ByteArray { + val messageData = try { + message.removePrefix(HEX_PREFIX).hexToBytes() + } catch (exception: Exception) { + message.asciiToHex()?.hexToBytes() ?: byteArrayOf() + } + + val prefixData = (ETH_MESSAGE_PREFIX + messageData.size.toString()).toByteArray() + return (prefixData + messageData).toKeccak() + } + + fun hexToAscii(hex: String): String? { + return try { + hex.removePrefix(HEX_PREFIX).hexToBytes().map { + val char = it.toInt().toChar() + if (char.isAscii()) char else return null + }.joinToString("") + } catch (exception: Exception) { + return null + } + } + + private fun String.asciiToHex(): String? { + return map { + if (!it.isAscii()) return null + Integer.toHexString(it.code) + }.joinToString("") + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index ffe057712c..a03f8d46ba 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -10,8 +10,7 @@ import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.WcRequest import com.tangem.domain.walletconnect.usecase.WcUseCase import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider -import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod -import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod.SignMessage +import com.tangem.domain.walletconnect.model.WcEthMethod import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow @@ -32,7 +31,7 @@ internal class WcEthNetwork( val name = Name.entries.find { it.raw == methodName } ?: return null return when (name) { Name.Sign -> TODO() - Name.PersonalSign -> WcMethodHandler.fromJson(params, moshi) + Name.PersonalSign -> TODO() Name.SignTypeData -> TODO() Name.SignTypeDataV4 -> TODO() Name.SignTransaction -> TODO() @@ -43,7 +42,7 @@ internal class WcEthNetwork( override fun handle(wcRequest: WcRequest) { wcRequest as WcRequest val useCase = when (wcRequest.method) { - is SignMessage -> TODO() + is WcEthMethod.PersonalEthSign -> TODO() } _useCases.trySend(useCase) } 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 423eeb9ea3..1c11137607 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 @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume -class WcPairSdkDelegate : WcSdkObserver { +internal class WcPairSdkDelegate : WcSdkObserver { private val onSessionProposal = Channel() private val onSessionSettleResponse = Channel() 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 6e31f2783d..341a80d075 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 @@ -13,7 +13,6 @@ import com.tangem.domain.walletconnect.model.WcSessionDTO import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope @@ -30,7 +29,6 @@ internal class DefaultWcSessionsManager constructor( private val store: WalletConnectStore, private val legacyStore: WalletConnectSessionsRepository, private val getWallets: GetWalletsUseCase, - private val getUserWallet: GetUserWalletUseCase, private val dispatchers: CoroutineDispatcherProvider, private val scope: CoroutineScope, ) : WcSessionsManager, WcSdkObserver { @@ -84,9 +82,12 @@ internal class DefaultWcSessionsManager constructor( } override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) { - val storedSessions = store.findSessionByTopic(topic) ?: return@withContext null + val storedSessions = sessions.firstOrNull() + ?.values?.flatten() + ?.firstOrNull { it.sdkModel.topic == topic } + ?: return@withContext null val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null - val wallet = getUserWallet.invoke(storedSessions.walletId).getOrNull() ?: return@withContext null + val wallet = storedSessions.wallet WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession)) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt index dd432f1df6..c1d53f51b9 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.sign import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletconnect.usecase.WcMethodUseCase import com.tangem.domain.walletconnect.usecase.sign.WcSignState import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase @@ -17,6 +18,7 @@ internal abstract class BaseWcSignUseCase : abstract val respondService: WcRespondService abstract val context: WcMethodUseCaseContext + override val network: Network get() = context.network override val session: WcSession get() = context.session override val rawSdkRequest: WcSdkSessionRequest get() = context.rawSdkRequest @@ -55,6 +57,7 @@ internal interface FinalActionCollector { internal class WcMethodUseCaseContext( val session: WcSession, val rawSdkRequest: WcSdkSessionRequest, + val network: Network, ) internal typealias OnSign = diff --git a/domain/wallet-connect/build.gradle.kts b/domain/wallet-connect/build.gradle.kts index 9b0cbaa2e3..e6bd5dea5d 100644 --- a/domain/wallet-connect/build.gradle.kts +++ b/domain/wallet-connect/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { /* Project - Domain */ implementation(projects.domain.core) + implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.walletConnect.models) diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt new file mode 100644 index 0000000000..ee9e7bfc98 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect.model + +sealed interface WcEthMethod : WcMethod { + + data class PersonalEthSign( + val message: String, + val account: String, + ) : WcEthMethod +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt index 05c3df4d98..fafbb31a81 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt @@ -1,5 +1,5 @@ package com.tangem.domain.walletconnect.model -interface WcMethod { +sealed interface WcMethod { data object Unsupported : WcMethod } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt index 6ff2c38c73..1aff7cea04 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.walletconnect.usecase +import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest @@ -8,4 +9,5 @@ interface WcUseCase interface WcMethodUseCase : WcUseCase { val session: WcSession val rawSdkRequest: WcSdkSessionRequest + val network: Network } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt deleted file mode 100644 index cf40981044..0000000000 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.domain.walletconnect.usecase.ethereum - -import com.tangem.domain.walletconnect.model.WcMethod - -sealed interface WcEthMethod : WcMethod { - - data class SignMessage( - val raw: List, - ) : WcEthMethod -} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcPersonalEthSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcPersonalEthSignUseCase.kt new file mode 100644 index 0000000000..b853bbb096 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcPersonalEthSignUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.walletconnect.usecase.ethereum + +import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase + +interface WcPersonalEthSignUseCase : + WcSignUseCase, + WcSignUseCase.FinalAction, + WcSignUseCase.SimpleRun { + + data class SignModel( + val humanMsg: String, + val account: String, + val rawMsg: String, + ) +} \ No newline at end of file From 19672ddf4053274b21c1a5f1820ff60ae144d893 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 20:54:55 +0500 Subject: [PATCH 38/72] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 6 + .../tokens/GetCryptoCurrenciesUseCase.kt | 25 ++ .../nft/details/model/NFTDetailsModel.kt | 15 +- features/send-v2/impl/build.gradle.kts | 2 + .../send/v2/common/CommonSendRoute.kt | 25 +- .../features/send/v2/common/ui/TapHelp.kt | 62 ++++ .../SendConfirmSentStateTransformer.kt | 4 +- .../v2/send/confirm/ui/SendConfirmContent.kt | 57 +-- .../v2/sendnft/DefaultNFTSendComponent.kt | 211 ++++++++--- .../features/send/v2/sendnft/NFTSendRoute.kt | 28 -- .../confirm/NFTSendConfirmComponent.kt | 147 ++++++++ .../v2/sendnft/confirm/model/ConfirmData.kt | 11 + .../model/NFTSendConfirmClickIntents.kt | 16 + .../confirm/model/NFTSendConfirmModel.kt | 335 ++++++++++++++++++ .../NFTSendConfirmSentStateTransformer.kt | 20 ++ .../NFTSendConfirmInitialStateTransformer.kt | 19 + .../NFTSendConfirmSendingStateTransformer.kt | 19 + ...endConfirmationNotificationsTransformer.kt | 55 +++ .../confirm/ui/NFTSendConfirmContent.kt | 104 ++++++ .../send/v2/sendnft/di/NFTSendModelModule.kt | 6 + .../send/v2/sendnft/model/NFTSendModel.kt | 25 +- .../v2/subcomponents/fee/SendFeeComponent.kt | 2 +- 22 files changed, 1021 insertions(+), 173 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt delete mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/ConfirmData.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmClickIntents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmSentStateTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmSendingStateTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt 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 237b83807e..99bb5efeef 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 @@ -427,4 +427,10 @@ internal object TokensDomainModule { tokensFeatureToggles = tokensFeatureToggles, ) } + + @Provides + @Singleton + fun provideGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { + return GetCryptoCurrenciesUseCase(currenciesRepository) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt new file mode 100644 index 0000000000..72b4d94cc4 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId + +class GetCryptoCurrenciesUseCase( + private val currenciesRepository: CurrenciesRepository, +) { + + /** + * Retrieves the list of cryptocurrencies within a multi-currency wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * + * @return An [Either] representing success (Right) or an error (Left) in fetching the status. + */ + suspend operator fun invoke(userWalletId: UserWalletId): Either> { + return Either.catch { + currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) + }.mapLeft(CurrencyStatusError::DataError) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index 446c289c09..4c45c7a714 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -61,14 +61,13 @@ internal class NFTDetailsModel @Inject constructor( } private fun onSendClick() { - // TODO implement - // router.push( - // AppRoute.NFTSend( - // userWalletId = params.userWalletId, - // nftAsset = params.nftAsset, - // nftCollectionName = params.nftCollectionName, - // ), - // ) + router.push( + AppRoute.NFTSend( + userWalletId = params.userWalletId, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + ), + ) } private fun navigateBack() { diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index 6858492736..9629c4825b 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /** Api */ implementation(projects.features.sendV2.api) implementation(projects.features.txhistory.api) + implementation(projects.features.nft.api) /** Libs */ implementation(projects.libs.crypto) @@ -54,6 +55,7 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.balanceHiding) + implementation(projects.domain.nft.models) /** Compose libraries */ implementation(deps.compose.foundation) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt index 55f8d409d6..7493c4d073 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt @@ -1,57 +1,34 @@ package com.tangem.features.send.v2.common -import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.core.decompose.navigation.Route import kotlinx.serialization.Serializable -<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt internal sealed class CommonSendRoute : Route { abstract val isEditMode: Boolean @Serializable data object Empty : CommonSendRoute() { -======== -@Serializable -internal sealed class SendRoute : CommonSendRoute { - - @Serializable - data object Empty : SendRoute(), CommonSendRoute.Empty { ->>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt override val isEditMode = false } @Serializable -<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt data object Confirm : CommonSendRoute() { -======== - data object Confirm : SendRoute(), CommonSendRoute.Confirm { ->>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt override val isEditMode: Boolean = false } @Serializable data class Destination( override val isEditMode: Boolean, -<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt ) : CommonSendRoute() -======== - ) : SendRoute(), CommonSendRoute.Destination ->>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt @Serializable data class Amount( override val isEditMode: Boolean, -<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt ) : CommonSendRoute() @Serializable data object Fee : CommonSendRoute() { -======== - ) : SendRoute(), CommonSendRoute.Amount - - @Serializable - data object Fee : SendRoute(), CommonSendRoute.Fee { ->>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt override val isEditMode: Boolean = true } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt new file mode 100644 index 0000000000..480650ee0e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt @@ -0,0 +1,62 @@ +package com.tangem.features.send.v2.common.ui + +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.lazy.LazyListScope +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.res.painterResource +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.impl.R +import kotlinx.coroutines.delay + +private const val TAP_HELP_KEY = "TAP_HELP_KEY" +private const val TAP_HELP_ANIMATION_DELAY = 500L + +internal fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) { + item(key = TAP_HELP_KEY) { + var wrappedIsDisplay by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = isDisplay) { + delay(TAP_HELP_ANIMATION_DELAY) + wrappedIsDisplay = isDisplay + } + + if (wrappedIsDisplay) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxWidth() + .animateItem() + .padding(top = TangemTheme.dimens.spacing20), + ) { + val background = TangemTheme.colors.button.secondary + Icon( + painter = painterResource(id = R.drawable.ic_send_hint_shape_12), + tint = TangemTheme.colors.button.secondary, + contentDescription = null, + modifier = Modifier, + ) + Text( + text = stringResourceSafe(id = R.string.send_summary_tap_hint), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(background) + .padding( + horizontal = TangemTheme.dimens.spacing14, + vertical = TangemTheme.dimens.spacing12, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt index 6eed2edabb..2d1b64e6b7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt @@ -6,8 +6,8 @@ import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.utils.transformer.Transformer internal class SendConfirmSentStateTransformer( - val txData: TransactionData.Uncompiled, - val txUrl: String, + private val txData: TransactionData.Uncompiled, + private val txUrl: String, ) : Transformer { override fun transform(prevState: SendUM): SendUM { return prevState.copy( 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 b734399624..bcf7d3283a 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 @@ -5,20 +5,15 @@ 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.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope -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.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM @@ -26,12 +21,16 @@ import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.transactions.TransactionDoneTitle -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat -import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.ui.tapHelp +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent @@ -39,11 +38,8 @@ import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.notifications import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.delay -private const val TAP_HELP_KEY = "TAP_HELP_KEY" private const val BLOCKS_KEY = "BLOCKS_KEY" -private const val TAP_HELP_ANIMATION_DELAY = 500L @Suppress("LongParameterList") @Composable @@ -148,45 +144,4 @@ private fun LazyListScope.blocks( feeBlockComponent.Content(modifier = Modifier) } } -} - -private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) { - item(key = TAP_HELP_KEY) { - var wrappedIsDisplay by remember { mutableStateOf(false) } - - LaunchedEffect(key1 = isDisplay) { - delay(TAP_HELP_ANIMATION_DELAY) - wrappedIsDisplay = isDisplay - } - - if (wrappedIsDisplay) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxWidth() - .animateItem() - .padding(top = TangemTheme.dimens.spacing20), - ) { - val background = TangemTheme.colors.button.secondary - Icon( - painter = painterResource(id = R.drawable.ic_send_hint_shape_12), - tint = TangemTheme.colors.button.secondary, - contentDescription = null, - modifier = Modifier, - ) - Text( - text = stringResourceSafe(id = R.string.send_summary_tap_hint), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - modifier = Modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(background) - .padding( - horizontal = TangemTheme.dimens.spacing14, - vertical = TangemTheme.dimens.spacing12, - ), - ) - } - } - } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index 8074966261..095cb06ce3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -2,84 +2,195 @@ package com.tangem.features.send.v2.sendnft 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.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe 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.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.common.ui.SendContent +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent +import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch +import java.math.BigDecimal internal class DefaultNFTSendComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: NFTSendComponent.Params, + private val nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, ) : NFTSendComponent, AppComponentContext by appComponentContext { - // private val stackNavigation = StackNavigation() - // - // private val innerRouter = InnerRouter( - // stackNavigation = stackNavigation, - // popCallback = { onChildBack() }, - // ) + private val stackNavigation = StackNavigation() - // private val initialRoute = NFTSendRoute.Empty - // private val currentRouteFlow = MutableStateFlow(initialRoute) + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) - private val model: NFTSendModel = getOrCreateModel(params = params/*, router = innerRouter*/) + private val initialRoute = CommonSendRoute.Empty + private val currentRouteFlow = MutableStateFlow(initialRoute) - // private val childStack = childStack( - // key = "NFTSendInnerStack", - // source = stackNavigation, - // serializer = null, - // initialConfiguration = initialRoute, - // handleBackButton = true, - // childFactory = { configuration, factoryContext -> - // // todo - // ComposableContentComponent { } - // }, - // ) + private val model: NFTSendModel = getOrCreateModel(params = params, router = innerRouter) + + private val childStack = childStack( + key = "NFTSendInnerStack", + source = stackNavigation, + serializer = null, + initialConfiguration = initialRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createChild( + configuration, + childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + ) + }, + ) init { - // childStack.subscribe( - // lifecycle = lifecycle, - // mode = ObserveLifecycleMode.CREATE_DESTROY, - // ) { stack -> - // componentScope.launch { - // when (val activeComponent = stack.active.instance) { - // is SendDestinationComponent -> { - // // analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened) - // activeComponent.updateState(model.uiState.value.destinationUM) - // } - // is SendFeeComponent -> { - // // analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened) - // } - // } - // currentRouteFlow.emit(stack.active.configuration) - // } - // } + childStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + when (val activeComponent = stack.active.instance) { + is NFTSendConfirmComponent -> if (currentRouteFlow.value.isEditMode) { + // analyticsEventHandler.send(SendAnalyticEvents.ConfirmationScreenOpened) + activeComponent.updateState(model.uiState.value) + } + is SendDestinationComponent -> { + // analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened) + activeComponent.updateState(model.uiState.value.destinationUM) + } + is SendFeeComponent -> { + // analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened) + } + } + currentRouteFlow.emit(stack.active.configuration) + } + } } @Composable override fun Content(modifier: Modifier) { - // val stackState by childStack.subscribeAsState() - // val state by model.uiState.collectAsStateWithLifecycle() + val stackState by childStack.subscribeAsState() + val state by model.uiState.collectAsStateWithLifecycle() BackHandler(onBack = ::onChildBack) - // TODO + SendContent( + navigationUM = state.navigationUM, + stackState = stackState, + ) } + private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) { + is CommonSendRoute.Destination -> getDestinationComponent(factoryContext, route) + is CommonSendRoute.Fee -> getFeeComponent(factoryContext) + CommonSendRoute.Confirm -> getConfirmComponent(factoryContext) + else -> getStubComponent() + } + + private fun getDestinationComponent(factoryContext: AppComponentContext, route: CommonSendRoute) = + SendDestinationComponent( + appComponentContext = factoryContext, + params = SendDestinationComponentParams.DestinationParams( + state = model.uiState.value.destinationUM, + currentRoute = currentRouteFlow.filterIsInstance(), + isBalanceHidingFlow = model.isBalanceHiddenFlow, + analyticsCategoryName = "", // todo + userWalletId = params.userWalletId, + cryptoCurrency = model.cryptoCurrency, + callback = model, + onBackClick = ::onChildBack, + onNextClick = { + if (route.isEditMode) { + onChildBack() + } else { + innerRouter.push(CommonSendRoute.Confirm) + } + }, + ), + ) + + private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent { + val state = model.uiState.value + val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value + return if (destinationAddress != null) { + SendFeeComponent( + appComponentContext = factoryContext, + params = SendFeeComponentParams.FeeParams( + state = model.uiState.value.feeUM, + currentRoute = currentRouteFlow.filterIsInstance(), + analyticsCategoryName = "", // todo + userWallet = model.userWallet, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, + appCurrency = model.appCurrency, + sendAmount = BigDecimal.ZERO, + destinationAddress = destinationAddress, + callback = model, + onNextClick = ::onChildBack, + ), + ) + } else { + getStubComponent() + } + } + + private fun getConfirmComponent(factoryContext: AppComponentContext) = NFTSendConfirmComponent( + appComponentContext = factoryContext, + nftDetailsBlockComponentFactory = nftDetailsBlockComponentFactory, + params = NFTSendConfirmComponent.Params( + state = model.uiState.value, + analyticsCategoryName = "", // todo + userWallet = model.userWallet, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, + appCurrency = model.appCurrency, + callback = model, + currentRoute = currentRouteFlow.filterIsInstance(), + isBalanceHidingFlow = model.isBalanceHiddenFlow, + ), + ) + + private fun getStubComponent() = ComposableContentComponent { } + private fun onChildBack() { - // TODO - // val isEmptyRoute = childStack.value.active.configuration == NFTSendRoute.Empty - // val isEmptyStack = childStack.value.backStack.isEmpty() - // val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success - // - // if (isEmptyRoute || isEmptyStack || isSuccess) { - // router.pop() - // } else { - // stackNavigation.pop() - // } + val isEmptyRoute = childStack.value.active.configuration == CommonSendRoute.Empty + val isEmptyStack = childStack.value.backStack.isEmpty() + val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success + + if (isEmptyRoute || isEmptyStack || isSuccess) { + router.pop() + } else { + stackNavigation.pop() + } } @AssistedFactory diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt deleted file mode 100644 index ccec86fa65..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.features.send.v2.sendnft - -import com.tangem.features.send.v2.common.CommonSendRoute -import kotlinx.serialization.Serializable - -@Serializable -internal sealed class NFTSendRoute : CommonSendRoute { - - @Serializable - data object Empty : NFTSendRoute(), CommonSendRoute.Empty { - override val isEditMode = false - } - - @Serializable - data object Confirm : NFTSendRoute(), CommonSendRoute.Confirm { - override val isEditMode: Boolean = false - } - - @Serializable - data class Destination( - override val isEditMode: Boolean, - ) : NFTSendRoute(), CommonSendRoute.Destination - - @Serializable - data object Fee : NFTSendRoute(), CommonSendRoute.Fee { - override val isEditMode: Boolean = true - } -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt new file mode 100644 index 0000000000..ead66f202b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt @@ -0,0 +1,147 @@ +package com.tangem.features.send.v2.sendnft.confirm + +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.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.common.PredefinedValues +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel +import com.tangem.features.send.v2.sendnft.confirm.ui.NFTSendConfirmContent +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams +import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent +import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData +import kotlinx.coroutines.flow.* +import java.math.BigDecimal + +internal class NFTSendConfirmComponent( + appComponentContext: AppComponentContext, + params: Params, + nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: NFTSendConfirmModel = getOrCreateModel(params = params) + + private val blockClickEnableFlow = MutableStateFlow(false) + + private val destinationBlockComponent = + SendDestinationBlockComponent( + appComponentContext = child("NFTSendConfirmDestinationBlock"), + params = DestinationBlockParams( + state = model.uiState.value.destinationUM, + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrency = params.cryptoCurrencyStatus.currency, + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + predefinedValues = PredefinedValues.Empty, + ), + onResult = model::onDestinationResult, + onClick = model::showEditDestination, + ) + + private val feeBlockComponent = SendFeeBlockComponent( + appComponentContext = child("NFTSendConfirmFeeBlock"), + params = SendFeeComponentParams.FeeBlockParams( + state = model.uiState.value.feeUM, + analyticsCategoryName = params.analyticsCategoryName, + userWallet = params.userWallet, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + appCurrency = params.appCurrency, + sendAmount = BigDecimal.ZERO, + destinationAddress = model.confirmData.enteredDestination.orEmpty(), + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + ), + onResult = model::onFeeResult, + onClick = model::showEditFee, + ) + + private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create( + context = child("NFTDetailsBlock"), + params = NFTDetailsBlockComponent.Params( + userWalletId = params.userWallet.walletId, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + ), + ) + + private val notificationsComponent = NotificationsComponent( + appComponentContext = child("NFTSendConfirmNotifications"), + params = NotificationsComponent.Params( + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + appCurrency = params.appCurrency, + notificationData = NotificationData( + destinationAddress = model.confirmData.enteredDestination.orEmpty(), + memo = model.confirmData.enteredMemo, + amountValue = BigDecimal.ZERO, + reduceAmountBy = BigDecimal.ZERO, + isIgnoreReduce = false, + fee = model.confirmData.fee, + feeError = model.confirmData.feeError, + ), + ), + ) + + init { + model.uiState.onEach { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + blockClickEnableFlow.value = confirmUM?.isSending == false + }.launchIn(componentScope) + } + + fun updateState(state: NFTSendUM) { + destinationBlockComponent.updateState(state.destinationUM) + feeBlockComponent.updateState(state.feeUM) + model.updateState(state) + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + val notificationState by notificationsComponent.state.collectAsStateWithLifecycle() + + NFTSendConfirmContent( + nftSendUM = state, + destinationBlockComponent = destinationBlockComponent, + feeBlockComponent = feeBlockComponent, + nftDetailsBlockComponent = nftDetailsBlockComponent, + notificationsComponent = notificationsComponent, + notificationsUM = notificationState, + ) + } + + data class Params( + val state: NFTSendUM, + val analyticsCategoryName: String, + val userWallet: UserWallet, + val appCurrency: AppCurrency, + val nftAsset: NFTAsset, + val nftCollectionName: String, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + val callback: ModelCallback, + val currentRoute: Flow, + val isBalanceHidingFlow: StateFlow, + ) + + interface ModelCallback { + fun onResult(nftSendUM: NFTSendUM) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/ConfirmData.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/ConfirmData.kt new file mode 100644 index 0000000000..704ce61b5c --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/ConfirmData.kt @@ -0,0 +1,11 @@ +package com.tangem.features.send.v2.sendnft.confirm.model + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.transaction.error.GetFeeError + +data class ConfirmData( + val enteredDestination: String?, + val enteredMemo: String?, + val fee: Fee?, + val feeError: GetFeeError?, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmClickIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmClickIntents.kt new file mode 100644 index 0000000000..292d62c6c0 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmClickIntents.kt @@ -0,0 +1,16 @@ +package com.tangem.features.send.v2.sendnft.confirm.model + +internal interface NFTSendConfirmClickIntents { + + fun showEditDestination() + + fun showEditFee() + + fun onSendClick() + + fun onExploreClick() + + fun onShareClick() + + fun onFailedTxEmailClick(errorMessage: String) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt new file mode 100644 index 0000000000..95cc3bb76a --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -0,0 +1,335 @@ +package com.tangem.features.send.v2.sendnft.confirm.model + +import android.os.SystemClock +import arrow.core.getOrElse +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +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.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase +import com.tangem.domain.settings.NeverShowTapHelpUseCase +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +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.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.send.ui.state.ButtonsUM +import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmInitialStateTransformer +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmSendingStateTransformer +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.transformer.update +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.math.BigDecimal +import javax.inject.Inject + +@Suppress("LongParameterList") +@ModelScoped +internal class NFTSendConfirmModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val appRouter: AppRouter, + private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, + private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, + private val notificationsUpdateTrigger: NotificationsUpdateTrigger, + private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger, + private val urlOpener: UrlOpener, + private val shareManager: ShareManager, + private val analyticsEventHandler: AnalyticsEventHandler, +) : Model(), NFTSendConfirmClickIntents { + + private val params: NFTSendConfirmComponent.Params = paramsContainer.require() + + private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + + private val _uiState = MutableStateFlow(params.state) + val uiState = _uiState.asStateFlow() + + private val destinationUM + get() = uiState.value.destinationUM as? DestinationUM.Content + private val feeUM + get() = uiState.value.feeUM as? FeeUM.Content + private val feeSelectorUM + get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content + + val confirmData: ConfirmData + get() = ConfirmData( + enteredDestination = destinationUM?.addressTextField?.value, + enteredMemo = destinationUM?.memoTextField?.value, + fee = feeSelectorUM?.selectedFee, + feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error, + ) + + private var sendIdleTimer: Long = 0L + + init { + configConfirmNavigation() + subscribeOnNotificationsUpdateTrigger() + subscribeOnCheckFeeResultUpdates() + initialState() + } + + fun updateState(nftSendUM: NFTSendUM) { + _uiState.value = nftSendUM + updateConfirmNotifications() + } + + fun onFeeResult(feeUM: FeeUM) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update { it.copy(feeUM = feeUM) } + updateConfirmNotifications() + } + + fun onDestinationResult(destinationUM: DestinationUM) { + _uiState.update { it.copy(destinationUM = destinationUM) } + updateConfirmNotifications() + } + + override fun showEditDestination() { + modelScope.launch { + neverShowTapHelpUseCase() + _uiState.update { + val confirmUM = it.confirmUM as? ConfirmUM.Content + it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) + } + // analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) + router.push(CommonSendRoute.Destination(isEditMode = true)) + } + } + + override fun showEditFee() { + modelScope.launch { + neverShowTapHelpUseCase() + _uiState.update { + val confirmUM = it.confirmUM as? ConfirmUM.Content + it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) + } + // analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) + router.push(CommonSendRoute.Fee) + } + } + + override fun onSendClick() { + _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = true)) + if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { + verifyAndSendTransaction() + } else { + modelScope.launch { + sendFeeCheckReloadTrigger.triggerCheckUpdate() + } + } + } + + override fun onExploreClick() { + val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return + // analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked) + urlOpener.openUrl(confirmUM.txUrl) + } + + override fun onShareClick() { + val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return + // analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked) + shareManager.shareText(confirmUM.txUrl) + } + + override fun onFailedTxEmailClick(errorMessage: String) { + // TODO() + } + + private fun initialState() { + val confirmUM = uiState.value.confirmUM + val feeUM = uiState.value.feeUM + + modelScope.launch { + val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false } + if (confirmUM is ConfirmUM.Empty || feeUM is FeeUM.Empty) { + _uiState.update { + it.copy( + confirmUM = NFTSendConfirmInitialStateTransformer( + isShowTapHelp = isShowTapHelp, + ).transform(uiState.value.confirmUM), + ) + } + updateConfirmNotifications() + } + } + } + + private fun subscribeOnNotificationsUpdateTrigger() { + notificationsUpdateTrigger.hasErrorFlow + .onEach { hasError -> + _uiState.update { + val feeUM = it.feeUM as? FeeUM.Content + val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content + it.copy( + confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + isPrimaryButtonEnabled = !hasError && feeSelectorUM != null, + ) ?: it.confirmUM, + ) + } + } + .launchIn(modelScope) + } + + private fun verifyAndSendTransaction() { + // TODO: + } + + // private suspend fun sendTransaction(txData: TransactionData.Uncompiled) { + // val result = sendTransactionUseCase( + // txData = txData, + // userWallet = userWallet, + // network = cryptoCurrency.network, + // ) + // + // _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false)) + // + // result.fold( + // ifLeft = { error -> + // // alertFactory.getSendTransactionErrorState( + // // error = error, + // // popBack = appRouter::pop, + // // onFailedTxEmailClick = ::onFailedTxEmailClick, + // // ) + // analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) + // }, + // ifRight = { + // updateTransactionStatus(txData) + // sendBalanceUpdater.scheduleUpdates() + // // sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value) + // }, + // ) + // } + + private fun subscribeOnCheckFeeResultUpdates() { + sendFeeCheckReloadTrigger.checkReloadResultFlow.onEach { isFeeResultSuccess -> + if (isFeeResultSuccess) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = true)) + verifyAndSendTransaction() + } else { + _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false)) + } + }.launchIn(modelScope) + } + + // private suspend fun updateTransactionStatus(txData: TransactionData.Uncompiled) { + // val txUrl = getExplorerTransactionUrlUseCase( + // userWalletId = userWallet.walletId, + // network = cryptoCurrency.network, + // ).getOrElse { "" } + // _uiState.update(NFTSendConfirmSentStateTransformer(txData, txUrl)) + // } + + private fun updateConfirmNotifications() { + modelScope.launch { + notificationsUpdateTrigger.triggerUpdate( + data = NotificationData( + destinationAddress = confirmData.enteredDestination.orEmpty(), + memo = confirmData.enteredMemo, + amountValue = BigDecimal.ZERO, + reduceAmountBy = BigDecimal.ZERO, + isIgnoreReduce = false, + fee = confirmData.fee, + feeError = confirmData.feeError, + ), + ) + } + _uiState.update { + it.copy( + confirmUM = NFTSendConfirmationNotificationsTransformer( + feeUM = uiState.value.feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrencyStatus.currency, + ).transform(uiState.value.confirmUM), + ) + } + } + + private fun configConfirmNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).onEach { (state, _) -> + val confirmUM = state.confirmUM + params.callback.onResult( + state.copy( + navigationUM = NavigationUM.Content( + title = resourceReference( + id = R.string.send_summary_title, + formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name), + ), + subtitle = null, + backIconRes = R.drawable.ic_close_24, + backIconClick = { + analyticsEventHandler.send( + SendAnalyticEvents.CloseButtonClicked( + source = SendScreenSource.Confirm, + isFromSummary = true, + isValid = confirmUM.isPrimaryButtonEnabled, + ), + ) + appRouter.pop() + }, + primaryButton = ButtonsUM.PrimaryButtonUM( + text = when (confirmUM) { + is ConfirmUM.Success -> resourceReference(R.string.common_close) + is ConfirmUM.Content -> if (confirmUM.isSending) { + resourceReference(R.string.send_sending) + } else { + resourceReference(R.string.common_send) + } + else -> resourceReference(R.string.common_send) + }, + iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content }, + isEnabled = confirmUM.isPrimaryButtonEnabled, + isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending, + onClick = { + when (confirmUM) { + is ConfirmUM.Success -> appRouter.pop() + is ConfirmUM.Content -> if (confirmUM.isSending) { + return@PrimaryButtonUM + } else { + onSendClick() + } + else -> return@PrimaryButtonUM + } + }, + ), + prevButton = null, + secondaryPairButtonsUM = ButtonsUM.SecondaryPairButtonsUM( + leftText = resourceReference(R.string.common_explore), + leftIconResId = R.drawable.ic_web_24, + onLeftClick = ::onExploreClick, + rightText = resourceReference(R.string.common_share), + rightIconResId = R.drawable.ic_share_24, + onRightClick = ::onShareClick, + ).takeIf { confirmUM is ConfirmUM.Success }, + ), + ), + ) + }.launchIn(modelScope) + } + + private companion object { + const val CHECK_FEE_UPDATE_DELAY = 10_000L + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmSentStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmSentStateTransformer.kt new file mode 100644 index 0000000000..5c613e80e6 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmSentStateTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.features.send.v2.sendnft.confirm.model + +import com.tangem.blockchain.common.TransactionData +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.utils.transformer.Transformer + +internal class NFTSendConfirmSentStateTransformer( + private val txData: TransactionData.Uncompiled, + private val txUrl: String, +) : Transformer { + override fun transform(prevState: NFTSendUM): NFTSendUM { + return prevState.copy( + confirmUM = ConfirmUM.Success( + transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(), + txUrl = txUrl, + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt new file mode 100644 index 0000000000..004117bd1d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.send.v2.sendnft.confirm.model.transformers + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +internal class NFTSendConfirmInitialStateTransformer( + private val isShowTapHelp: Boolean, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + return ConfirmUM.Content( + isSending = false, + showTapHelp = isShowTapHelp, + sendingFooter = TextReference.EMPTY, + notifications = persistentListOf(), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmSendingStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmSendingStateTransformer.kt new file mode 100644 index 0000000000..7b96c1a703 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmSendingStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.send.v2.sendnft.confirm.model.transformers + +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.utils.transformer.Transformer + +internal class NFTSendConfirmSendingStateTransformer( + val isSending: Boolean, +) : Transformer { + override fun transform(prevState: NFTSendUM): NFTSendUM { + val confirmUM = prevState.confirmUM as? ConfirmUM.Content ?: return prevState + return prevState.copy( + confirmUM = confirmUM.copy( + isPrimaryButtonEnabled = !isSending, + isSending = isSending, + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt new file mode 100644 index 0000000000..af93239501 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt @@ -0,0 +1,55 @@ +package com.tangem.features.send.v2.sendnft.confirm.model.transformers + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class NFTSendConfirmationNotificationsTransformer( + private val feeUM: FeeUM, + private val analyticsEventHandler: AnalyticsEventHandler, + private val cryptoCurrency: CryptoCurrency, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + val state = prevState as? ConfirmUM.Content ?: return prevState + val feeUM = feeUM as? FeeUM.Content ?: return prevState + return state.copy( + notifications = buildList { + addTooHighNotification(feeUM.feeSelectorUM) + addTooLowNotification(feeUM) + }.toPersistentList(), + ) + } + + private fun MutableList.addTooLowNotification(feeUM: FeeUM.Content) { + val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return + val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return + val minimumValue = multipleFees.minimum.amount.value ?: return + val customAmount = feeSelectorUM.customValues.firstOrNull() ?: return + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) { + add(NotificationUM.Warning.FeeTooLow) + analyticsEventHandler.send( + SendAnalyticEvents.NoticeTransactionDelays(cryptoCurrency.symbol), + ) + } + } + + private fun MutableList.addTooHighNotification(feeSelectorUM: FeeSelectorUM) { + if (feeSelectorUM !is FeeSelectorUM.Content) return + + val (isFeeTooHigh, diff) = checkIfFeeTooHigh(feeSelectorUM) + if (isFeeTooHigh) { + add(NotificationUM.Warning.TooHigh(diff)) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt new file mode 100644 index 0000000000..735f3ecaf0 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -0,0 +1,104 @@ +package com.tangem.features.send.v2.sendnft.confirm.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.ui.tapHelp +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent +import com.tangem.features.send.v2.subcomponents.notifications +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent +import kotlinx.collections.immutable.ImmutableList + +private const val BLOCKS_KEY = "BLOCKS_KEY" + +@Suppress("LongParameterList") +@Composable +internal fun NFTSendConfirmContent( + nftSendUM: NFTSendUM, + destinationBlockComponent: SendDestinationBlockComponent, + nftDetailsBlockComponent: NFTDetailsBlockComponent, + feeBlockComponent: SendFeeBlockComponent, + notificationsComponent: NotificationsComponent, + notificationsUM: ImmutableList, +) { + val confirmUM = nftSendUM.confirmUM as? ConfirmUM.Content + + Column { + LazyColumn( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + blocks( + nftSendUM = nftSendUM, + destinationBlockComponent = destinationBlockComponent, + nftDetailsBlockComponent = nftDetailsBlockComponent, + feeBlockComponent = feeBlockComponent, + ) + if (confirmUM != null) { + tapHelp(isDisplay = confirmUM.showTapHelp) + with(notificationsComponent) { + content( + state = notificationsUM, + isClickDisabled = confirmUM.isSending, + ) + } + notifications( + notifications = confirmUM.notifications, + isClickDisabled = confirmUM.isSending, + ) + } + } + } +} + +private fun LazyListScope.blocks( + nftSendUM: NFTSendUM, + destinationBlockComponent: SendDestinationBlockComponent, + nftDetailsBlockComponent: NFTDetailsBlockComponent, + feeBlockComponent: SendFeeBlockComponent, +) { + item(key = BLOCKS_KEY) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + AnimatedVisibility( + visible = nftSendUM.confirmUM is ConfirmUM.Success, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + ) { + val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success } + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + wrappedConfirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } + destinationBlockComponent.Content(modifier = Modifier) + + nftDetailsBlockComponent.Content(modifier = Modifier) + + feeBlockComponent.Content(modifier = Modifier) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt index cf70962383..3252c7bc71 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.v2.sendnft.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model +import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.model.NFTSendModel import dagger.Binds import dagger.Module @@ -17,4 +18,9 @@ internal interface NFTSendModelModule { @IntoMap @ClassKey(NFTSendModel::class) fun provideNFTSendModel(model: NFTSendModel): Model + + @Binds + @IntoMap + @ClassKey(NFTSendConfirmModel::class) + fun provideNFTSendConfirmModel(model: NFTSendConfirmModel): Model } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 3d5d6d178a..7b3be9e2b3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -9,6 +9,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency @@ -16,8 +17,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM @@ -31,7 +34,8 @@ import kotlin.properties.Delegates internal interface SendNFTComponentCallback : SendFeeComponent.ModelCallback, - SendDestinationComponent.ModelCallback + SendDestinationComponent.ModelCallback, + NFTSendConfirmComponent.ModelCallback @Suppress("LongParameterList") @Stable @@ -42,12 +46,14 @@ internal class NFTSendModel @Inject constructor( private val router: Router, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, + private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, ) : Model(), SendNFTComponentCallback { val params: NFTSendComponent.Params = paramsContainer.require() private val userWalletId = params.userWalletId + private val nftAsset = params.nftAsset private val _uiState = MutableStateFlow(initialState()) val uiState = _uiState.asStateFlow() @@ -70,15 +76,16 @@ internal class NFTSendModel @Inject constructor( _uiState.update { it.copy(navigationUM = navigationUM) } } + override fun onResult(nftSendUM: NFTSendUM) { + _uiState.value = nftSendUM + } + override fun onDestinationResult(destinationUM: DestinationUM) { _uiState.update { it.copy(destinationUM = destinationUM) } - - // todo } override fun onFeeResult(feeUM: FeeUM) { _uiState.update { it.copy(feeUM = feeUM) } - router.pop() } private fun initAppCurrency() { @@ -93,10 +100,10 @@ internal class NFTSendModel @Inject constructor( ifRight = { wallet -> userWallet = wallet - // cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId).getOrNull() - // ?.filterIsInstance() - // ?.firstOrNull { it.network == nftAsset.network } - // ?: return@launch + cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId).getOrNull() + ?.filterIsInstance() + ?.firstOrNull { it.network == nftAsset.network } + ?: return@launch getCurrenciesStatusUpdates( isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), @@ -124,7 +131,7 @@ internal class NFTSendModel @Inject constructor( cryptoCurrencyStatus = cryptoStatus, ).getOrNull() ?: cryptoStatus - // router.push(NFTSendRoute.Destination(isEditMode = false)) + router.replaceAll(CommonSendRoute.Destination(isEditMode = false)) }, ifLeft = { // sendConfirmAlertFactory.getGenericErrorState { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt index 5b7cdce04a..bfb3330873 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt @@ -13,7 +13,7 @@ import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM internal class SendFeeComponent( appComponentContext: AppComponentContext, - private val params: SendFeeComponentParams.FeeParams, + params: SendFeeComponentParams.FeeParams, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: SendFeeModel = getOrCreateModel(params = params) From eef5e02b3e9cf4878889a7abaf1e3a772662f0ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Apr 2025 16:55:48 +0200 Subject: [PATCH 39/72] Updated on 2026-08-14 From 0f297405cef07abede49f69e8fa8a8c16335a90a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 14 Apr 2025 04:51:33 +0000 Subject: [PATCH 40/72] 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 4a299587fa..884ee07a79 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.23-1015" +tangemBlockchainSdk = "develop-1014" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-456" +tangemCardSdk = "develop-455" #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 8d5c9e0e58daa8b6c2d8ad612d956a4258ae7175 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 11:51:40 +0400 Subject: [PATCH 41/72] Updated on 2026-08-14 --- .../tangem/datasource/di/QuotesStoreModule.kt | 29 +-- .../com/tangem/utils/coroutines/JobHolder.kt | 3 + .../quotes/multi/DefaultMultiQuoteFetcher.kt | 55 ++++-- .../quotes/multi/DefaultMultiQuoteUpdater.kt | 86 +++++++++ .../data/quotes/store/DefaultQuotesStoreV2.kt | 2 + .../tangem/data/quotes/store/QuotesStoreV2.kt | 3 + .../multi/DefaultMultiQuoteFetcherTest.kt | 77 +++++++- .../multi/DefaultMultiQuoteUpdaterTest.kt | 169 ++++++++++++++++++ .../quotes/store/QuotesStoreGetMethodTest.kt | 29 +++ .../domain/quotes/multi/MultiQuoteFetcher.kt | 11 +- .../domain/quotes/multi/MultiQuoteUpdater.kt | 15 ++ 11 files changed, 448 insertions(+), 31 deletions(-) create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteUpdater.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt index dbcf34e870..48e2d14670 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.di import android.content.Context +import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi @@ -26,21 +27,27 @@ internal object QuotesStoreModule { @Provides @Singleton - fun provideQuotesStore( + fun providePersistenceQuotesStore( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, dispatchers: CoroutineDispatcherProvider, - ): QuotesStore { - return DefaultQuotesStore( - persistenceStore = DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = mapWithStringKeyTypes(), - defaultValue = emptyMap(), - ), - produceFile = { context.dataStoreFile(fileName = "quotes") }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ): DataStore> { + return DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(), + defaultValue = emptyMap(), ), + produceFile = { context.dataStoreFile(fileName = "quotes") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + } + + @Provides + @Singleton + fun provideQuotesStore(persistenceStore: DataStore>): QuotesStore { + return DefaultQuotesStore( + persistenceStore = persistenceStore, runtimeStore = RuntimeSharedStore(), ) } diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index 6e5a872f02..4676289125 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -24,7 +24,10 @@ class JobHolder { /** Cancel current [job] */ fun cancel() { job?.cancel() + job = null } + + fun isEmpty() = job == null } fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this) 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 5fc337e08c..e9fa631d23 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 @@ -6,9 +6,13 @@ import com.tangem.data.quotes.store.QuotesStoreV2 import com.tangem.data.tokens.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.quotes.multi.MultiQuoteFetcher import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton /** * Default implementation of [MultiQuoteFetcher] @@ -16,18 +20,26 @@ import timber.log.Timber * @property tangemTechApi tangemTech api * @property appCurrencyResponseStore app currency response store * @property quotesStore quotes store + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ -internal class DefaultMultiQuoteFetcher( +@Singleton +internal class DefaultMultiQuoteFetcher @Inject constructor( private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, private val quotesStore: QuotesStoreV2, + private val dispatchers: CoroutineDispatcherProvider, ) : MultiQuoteFetcher { private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter() - override suspend fun invoke(params: MultiQuoteFetcher.Params): Either = Either.catch { + override suspend fun invoke(params: MultiQuoteFetcher.Params) = Either.catchOn(dispatchers.default) { + if (params.currenciesIds.isEmpty()) { + Timber.d("No currencies to fetch quotes for") + return@catchOn + } + quotesStore.refresh(currenciesIds = params.currenciesIds) val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( @@ -37,26 +49,37 @@ internal class DefaultMultiQuoteFetcher( ), ) - val appCurrency = appCurrencyResponseStore.getSyncOrNull() - ?: error(message = "Unable to get AppCurrency for updating quotes") + val appCurrencyId = getAppCurrencyId(params = params) + val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") - safeApiCallWithTimeout( - call = { - val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") - val response = tangemTechApi.getQuotes(currencyId = appCurrency.id, coinIds = coinIds).bind() - - val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( - response = response, - filteredIds = replacementIdsResult.idsFiltered, - ) - - quotesStore.storeActual(values = updatedResponse.quotes) - }, + val response = safeApiCallWithTimeout( + call = { tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds).bind() }, onError = { error -> throw error }, ) + + val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( + response = response, + filteredIds = replacementIdsResult.idsFiltered, + ) + + quotesStore.storeActual(values = updatedResponse.quotes) } .onLeft { Timber.e(it) quotesStore.storeError(currenciesIds = params.currenciesIds) } + + private suspend fun getAppCurrencyId(params: MultiQuoteFetcher.Params): String { + val appCurrencyId = params.appCurrencyId + ?: appCurrencyResponseStore.getSyncOrNull()?.id + + if (appCurrencyId.isNullOrBlank()) { + val exception = IllegalStateException("Unable to get AppCurrency for updating quotes") + Timber.e(exception) + + throw exception + } + + return appCurrencyId + } } \ 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 new file mode 100644 index 0000000000..06e492857e --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt @@ -0,0 +1,86 @@ +package com.tangem.data.quotes.multi + +import androidx.annotation.VisibleForTesting +import arrow.core.left +import com.tangem.data.quotes.store.QuotesStoreV2 +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.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +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 + * @param dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class DefaultMultiQuoteUpdater @Inject constructor( + private val appCurrencyResponseStore: AppCurrencyResponseStore, + private val quotesStore: QuotesStoreV2, + private val multiQuoteFetcher: MultiQuoteFetcher, + dispatchers: CoroutineDispatcherProvider, +) : MultiQuoteUpdater { + + private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default) + private val updaterHolder = JobHolder() + + override fun subscribe() { + Timber.d("Subscribe on quotes updates") + getMultiQuoteUpdates() + .launchIn(coroutineScope) + .saveIn(updaterHolder) + } + + override fun unsubscribe() { + Timber.e("Unsubscribe from quotes updates") + updaterHolder.cancel() + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun getMultiQuoteUpdates(): EitherFlow { + return appCurrencyResponseStore.get() + .drop(count = 1) // skip initial value + .distinctUntilChanged() + .filterNotNull() + .mapLatest { appCurrency -> + val currenciesIds = quotesStore.getAllSyncOrNull().orEmpty() + .mapTo(destination = hashSetOf(), transform = Quote::rawCurrencyId) + + multiQuoteFetcher( + params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrency.id), + ) + .onLeft(Timber::e) + } + .retryWhen { cause, _ -> + Timber.e("Retry updating quotes: $cause") + + emit(cause.left()) + + delay(timeMillis = 2000) + true + } + } + + @VisibleForTesting(VisibleForTesting.NONE) + fun getMultiQuoteUpdatesFlow(): EitherFlow = getMultiQuoteUpdates() + + @VisibleForTesting(VisibleForTesting.NONE) + fun getUpdaterJobHolder(): JobHolder = updaterHolder +} \ No newline at end of file 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 2ae06d88e6..9fbbe97e50 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 @@ -47,6 +47,8 @@ internal class DefaultQuotesStoreV2( override fun get(): Flow> = runtimeStore.get() + override suspend fun getAllSyncOrNull(): Set? = runtimeStore.getSyncOrNull() + override suspend fun refresh(currenciesIds: Set) { updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.CACHE) } 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 b530a6559c..ed31f3abf9 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 @@ -11,6 +11,9 @@ 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) 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/DefaultMultiQuoteFetcherTest.kt index b35a5c6f06..3e85896a19 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/DefaultMultiQuoteFetcherTest.kt @@ -11,6 +11,7 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify import io.mockk.coVerifyOrder @@ -32,11 +33,12 @@ internal class DefaultMultiQuoteFetcherTest { tangemTechApi = tangemTechApi, appCurrencyResponseStore = appCurrencyResponseStore, quotesStore = quotesStore, + dispatchers = TestingCoroutineDispatcherProvider(), ) @Test fun `fetch quotes successfully`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -64,9 +66,78 @@ internal class DefaultMultiQuoteFetcherTest { Truth.assertThat(actual.isRight()).isTrue() } + @Test + fun `fetch quotes successfully if currenciesIds from params is empty`() = runTest { + val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = null) + + val actual = fetcher(params) + + coVerify(inverse = true) { + quotesStore.refresh(currenciesIds = any()) + appCurrencyResponseStore.getSyncOrNull() + tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) + quotesStore.storeActual(values = any()) + quotesStore.storeError(currenciesIds = any()) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @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 coinIds = "BTC,ETH" + coEvery { + tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds) + } returns ApiResponse.Success(successResponse) + + val actual = fetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = params.currenciesIds) + + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + + quotesStore.storeActual(values = successResponse.quotes) + } + + coVerify(inverse = true) { + appCurrencyResponseStore.getSyncOrNull() + quotesStore.storeError(currenciesIds = any()) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch quotes failure because appCurrencyId from params is blank`() = runTest { + val appCurrencyId = "" + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) + + val actual = fetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.storeError(currenciesIds = params.currenciesIds) + } + + coVerify(inverse = true) { + appCurrencyResponseStore.getSyncOrNull() + tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) + quotesStore.storeActual(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 { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -97,7 +168,7 @@ internal class DefaultMultiQuoteFetcherTest { @Test fun `fetch quotes failure because app currency not found`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null 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/DefaultMultiQuoteUpdaterTest.kt new file mode 100644 index 0000000000..f14962f52b --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt @@ -0,0 +1,169 @@ +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.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiQuoteUpdaterTest { + + private val appCurrencyResponseStore: AppCurrencyResponseStore = mockk() + private val quotesStore: QuotesStoreV2 = mockk() + private val multiQuoteFetcher: MultiQuoteFetcher = mockk() + + private val multiQuoteUpdater = DefaultMultiQuoteUpdater( + appCurrencyResponseStore = appCurrencyResponseStore, + quotesStore = quotesStore, + multiQuoteFetcher = multiQuoteFetcher, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `test that initial app currency is skipped`() = runTest { + val appCurrencyFlow = flowOf(null, usdAppCurrency) + + 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 actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() + + coVerify(exactly = 1) { appCurrencyResponseStore.get() } + + val values = getEmittedValues(actual) + Truth.assertThat(values).isEqualTo(listOf(Unit.right())) + + coVerifyOrder { + quotesStore.getAllSyncOrNull() + multiQuoteFetcher(params) + } + } + + @Test + fun `test that flow is filtered the same status`() = runTest { + val appCurrencyFlow = flowOf(usdAppCurrency, usdAppCurrency) + + 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 actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() + + coVerify(exactly = 1) { appCurrencyResponseStore.get() } + + val values = getEmittedValues(actual) + Truth.assertThat(values).isEqualTo(listOf(Unit.right())) + + coVerifyOrder { + quotesStore.getAllSyncOrNull() + multiQuoteFetcher(params) + } + } + + @Test + fun `test that flow is filtered the null`() = runTest { + val appCurrencyFlow = flowOf(null, null) + + every { appCurrencyResponseStore.get() } returns appCurrencyFlow + + val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() + + coVerify(exactly = 1) { appCurrencyResponseStore.get() } + + val values = getEmittedValues(actual) + Truth.assertThat(values.size).isEqualTo(0) + + coVerify(inverse = true) { + quotesStore.getAllSyncOrNull() + multiQuoteFetcher(any()) + } + } + + @Test + fun `test if flow throws exception`() = runTest { + val exception = IllegalStateException() + + val innerFlow = MutableStateFlow(value = false) + val appCurrencyFlow = flow { + if (innerFlow.value) { + emitAll(flowOf(null, usdAppCurrency)) + } else { + throw exception + } + } + .buffer(capacity = 5) + + 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 actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() + + coVerify(exactly = 1) { appCurrencyResponseStore.get() } + + val values1 = getEmittedValues(actual) + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first().isLeft()).isTrue() + Truth.assertThat(values1.first().leftOrNull()).isInstanceOf(exception::class.java) + Truth.assertThat(values1.first().leftOrNull()).hasMessageThat().isEqualTo(exception.message) + + coVerify(inverse = true) { + quotesStore.getAllSyncOrNull() + multiQuoteFetcher(any()) + } + + innerFlow.emit(value = true) + + val values2 = getEmittedValues(flow = actual) + Truth.assertThat(values2).isEqualTo(listOf(Unit.right())) + + coVerifyOrder { + quotesStore.getAllSyncOrNull() + multiQuoteFetcher(params) + } + } + + @Test + fun `subscribe and unsubscribe successfully`() { + every { appCurrencyResponseStore.get() } returns emptyFlow() + + val updaterJobHolder = multiQuoteUpdater.getUpdaterJobHolder() + Truth.assertThat(updaterJobHolder.isEmpty()).isTrue() + + multiQuoteUpdater.subscribe() + + Truth.assertThat(updaterJobHolder.isEmpty()).isFalse() + + multiQuoteUpdater.unsubscribe() + + Truth.assertThat(updaterJobHolder.isEmpty()).isTrue() + } + + private companion object { + val usdAppCurrency = CurrenciesResponse.Currency( + id = "USD".lowercase(), + code = "USD", + name = "US Dollar", + unit = "$", + type = "fiat", + rateBTC = "", + ) + } +} \ 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 index bbfd1053a6..d5a517f5e2 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 @@ -60,4 +60,33 @@ internal class QuotesStoreGetMethodTest { 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/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt index 4520ad1fd6..4319ae8399 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/MultiQuoteFetcher.kt @@ -10,5 +10,14 @@ import com.tangem.domain.tokens.model.CryptoCurrency */ interface MultiQuoteFetcher : FlowFetcher { - data class Params(val currenciesIds: Set) + /** + * Params + * + * @property currenciesIds identifiers of currencies + * @property appCurrencyId app currency id, if null then selected app currency will be used + */ + data class Params( + val currenciesIds: Set, + val appCurrencyId: String?, + ) } \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteUpdater.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteUpdater.kt new file mode 100644 index 0000000000..ee490bac35 --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteUpdater.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.quotes.multi + +/** + * Updater of quotes + * +[REDACTED_AUTHOR] + */ +interface MultiQuoteUpdater { + + /** Subscribe */ + fun subscribe() + + /** Unsubscribe */ + fun unsubscribe() +} \ No newline at end of file From fac23bd05d304dee828ff39eca36063d27ccd288 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 14 Apr 2025 12:48:17 +0300 Subject: [PATCH 42/72] Updated on 2026-08-14 --- .../send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt | 4 +++- .../subcomponents/notifications/model/NotificationsModel.kt | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 95cc3bb76a..1adf375c6e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -27,6 +27,7 @@ import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendCon import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM @@ -50,6 +51,7 @@ internal class NFTSendConfirmModel @Inject constructor( private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, private val notificationsUpdateTrigger: NotificationsUpdateTrigger, private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger, + private val sendFeeCheckReloadListener: SendFeeCheckReloadListener, private val urlOpener: UrlOpener, private val shareManager: ShareManager, private val analyticsEventHandler: AnalyticsEventHandler, @@ -219,7 +221,7 @@ internal class NFTSendConfirmModel @Inject constructor( // } private fun subscribeOnCheckFeeResultUpdates() { - sendFeeCheckReloadTrigger.checkReloadResultFlow.onEach { isFeeResultSuccess -> + sendFeeCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess -> if (isFeeResultSuccess) { sendIdleTimer = SystemClock.elapsedRealtime() _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = true)) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index 3fc7b11551..127a253df0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -119,8 +119,8 @@ internal class NotificationsModel @Inject constructor( modelScope.launch { sendFeeReloadTrigger.triggerUpdate( feeData = SendFeeData( - amount = amountValue, - destinationAddress = destinationAddress, + amount = notificationData.amountValue, + destinationAddress = notificationData.destinationAddress, ), ) } From fc4c7ef9ab143037a6d6900c22b237dfbecc21d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 14 Apr 2025 18:14:47 +0700 Subject: [PATCH 43/72] Updated on 2026-08-14 --- .../di/WalletConnectDataModule.kt | 41 +++++------ .../network/ethereum/WcEthNetwork.kt | 69 +++++++++++-------- .../network/solana/WcSolanaNetwork.kt | 15 +++- .../pair/AssociateNetworksDelegate.kt | 44 ++++-------- .../pair/CaipNamespaceDelegate.kt | 9 +-- .../request/DefaultWcRequestService.kt | 36 +++------- .../walletconnect/request/WcMethodHandler.kt | 18 ----- .../request/WcRequestToUseCaseConverter.kt | 16 +++++ .../utils/WcNamespaceConverter.kt | 21 +++++- .../walletconnect/request/WcRequestService.kt | 8 --- 10 files changed, 132 insertions(+), 145 deletions(-) delete mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcMethodHandler.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcRequestToUseCaseConverter.kt delete mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt 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 7ae5173300..609ebb2959 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 @@ -5,7 +5,6 @@ import com.squareup.moshi.Moshi import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.walletconnect.DefaultWalletConnectRepository import com.tangem.data.walletconnect.initialize.DefaultWcInitializeUseCase -import com.tangem.data.walletconnect.model.NamespaceKey import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork import com.tangem.data.walletconnect.network.solana.WcSolanaNetwork import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate @@ -13,7 +12,7 @@ import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.request.DefaultWcRequestService -import com.tangem.data.walletconnect.request.WcMethodHandler +import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter import com.tangem.data.walletconnect.respond.DefaultWcRespondService import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager @@ -22,11 +21,9 @@ import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.repository.WalletConnectRepository import com.tangem.domain.walletconnect.repository.WcSessionsManager -import com.tangem.domain.walletconnect.request.WcRequestService import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -114,37 +111,37 @@ internal object WalletConnectDataModule { @Provides @Singleton fun defaultWcRequestService( - sessionsManager: WcSessionsManager, - respondService: WcRespondService, dispatchers: CoroutineDispatcherProvider, diHelperBox: DiHelperBox, ): DefaultWcRequestService { val scope = CoroutineScope(SupervisorJob() + dispatchers.io) return DefaultWcRequestService( - sessionsManager = sessionsManager, - respondService = respondService, - requestAdapters = diHelperBox.handlers, + requestConverters = diHelperBox.handlers, scope = scope, ) } - @Provides - @Singleton - fun wcRequestService(default: DefaultWcRequestService): WcRequestService = default - @Provides @Singleton fun wcRespondService(): WcRespondService = DefaultWcRespondService() @Provides @Singleton - fun wcEthNetwork(@SdkMoshi moshi: Moshi): WcEthNetwork = WcEthNetwork( + fun wcEthNetwork( + @SdkMoshi moshi: Moshi, + excludedBlockchains: ExcludedBlockchains, + sessionsManager: WcSessionsManager, + ): WcEthNetwork = WcEthNetwork( moshi = moshi, + excludedBlockchains = excludedBlockchains, + sessionsManager = sessionsManager, ) @Provides @Singleton - fun wcSolanaNetwork(): WcSolanaNetwork = WcSolanaNetwork() + fun wcSolanaNetwork(excludedBlockchains: ExcludedBlockchains): WcSolanaNetwork = WcSolanaNetwork( + excludedBlockchains = excludedBlockchains, + ) @Provides @Singleton @@ -162,12 +159,10 @@ internal object WalletConnectDataModule { diHelperBox: DiHelperBox, getWallets: GetWalletsUseCase, currenciesRepository: CurrenciesRepository, - excludedBlockchains: ExcludedBlockchains, ): AssociateNetworksDelegate = AssociateNetworksDelegate( namespaceConverters = diHelperBox.converters, getWallets = getWallets, currenciesRepository = currenciesRepository, - excludedBlockchains = excludedBlockchains, ) @Provides @@ -176,14 +171,14 @@ internal object WalletConnectDataModule { handlers = setOf( ethNetwork, ), - converters = buildMap { - ethNetwork.namespaceKey to ethNetwork - solanaNetwork.namespaceKey to solanaNetwork - }, + converters = setOf( + ethNetwork, + solanaNetwork, + ), ) internal class DiHelperBox( - val converters: Map, - val handlers: Set>, + val converters: Set, + val handlers: Set, ) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index a03f8d46ba..a682ccfd11 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -2,36 +2,50 @@ package com.tangem.data.walletconnect.network.ethereum import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.data.walletconnect.model.NamespaceKey -import com.tangem.data.walletconnect.request.WcMethodHandler +import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter +import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson import com.tangem.data.walletconnect.utils.WcNamespaceConverter -import com.tangem.domain.walletconnect.model.WcMethod -import com.tangem.domain.walletconnect.model.WcRequest -import com.tangem.domain.walletconnect.usecase.WcUseCase -import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider +import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletconnect.model.WcEthMethod -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.receiveAsFlow +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.walletconnect.usecase.WcMethodUseCase +import com.tangem.domain.wallets.models.UserWallet -internal class WcEthNetwork( +internal class WcEthNetwork constructor( private val moshi: Moshi, -) : WcMethodHandler, WcUseCasesFlowProvider, WcNamespaceConverter { - - private val _useCases: Channel = Channel(Channel.BUFFERED) - override val useCases = _useCases.receiveAsFlow() + private val excludedBlockchains: ExcludedBlockchains, + private val sessionsManager: WcSessionsManager, +) : WcRequestToUseCaseConverter, WcNamespaceConverter { override val namespaceKey: NamespaceKey = NamespaceKey("eip155") - override fun canHandle(methodName: String): Boolean { - return Name.entries.find { it.raw == methodName } != null + override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? { + val methodKey = request.request.method + val name = Name.entries.find { it.raw == methodKey } ?: return null + val method: WcEthMethod = name.toMethod(request) ?: return null + val session = sessionsManager.findSessionByTopic(request.topic) ?: return null + val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null + return when (method) { + is WcEthMethod.PersonalEthSign -> TODO() + } } - override fun deserialize(methodName: String, params: String): WcEthMethod? { - val name = Name.entries.find { it.raw == methodName } ?: return null - return when (name) { - Name.Sign -> TODO() - Name.PersonalSign -> TODO() + private fun Name.toMethod(request: WcSdkSessionRequest): WcEthMethod? { + val rawParams = request.request.params + return when (this) { + Name.EthSign, + Name.PersonalSign, + -> moshi.fromJson>(rawParams)?.let { list -> + val accountIndex = if (this == Name.EthSign) 0 else 1 + val messageIndex = if (this == Name.EthSign) 1 else 0 + val account = list.getOrNull(accountIndex) ?: return@let null + val message = list.getOrNull(messageIndex) ?: return@let null + WcEthMethod.PersonalEthSign(account = account, message = message) + } Name.SignTypeData -> TODO() Name.SignTypeDataV4 -> TODO() Name.SignTransaction -> TODO() @@ -39,16 +53,8 @@ internal class WcEthNetwork( } } - override fun handle(wcRequest: WcRequest) { - wcRequest as WcRequest - val useCase = when (wcRequest.method) { - is WcEthMethod.PersonalEthSign -> TODO() - } - _useCases.trySend(useCase) - } - enum class Name(val raw: String) { - Sign("eth_sign"), + EthSign("eth_sign"), PersonalSign("personal_sign"), SignTypeData("eth_signTypedData"), SignTypeDataV4("eth_signTypedData_v4"), @@ -56,13 +62,18 @@ internal class WcEthNetwork( SendTransaction("eth_sendTransaction"), } + override fun toNetwork(chainId: String, wallet: UserWallet): Network? { + return toNetwork(chainId, wallet, excludedBlockchains) + } + override fun toBlockchain(chainId: CAIP2): Blockchain? { if (chainId.namespace != namespaceKey.key) return null val ethChainId = chainId.reference.toIntOrNull() ?: return null return Blockchain.fromChainId(ethChainId) } - override fun toCAIP2(blockchain: Blockchain): CAIP2? { + override fun toCAIP2(network: Network): CAIP2? { + val blockchain = Blockchain.fromId(network.id.value) if (!blockchain.isEvm()) return null val chainId = blockchain.getChainId() ?: return null return CAIP2( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt index bda63dde43..d4c72c3ee2 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -1,11 +1,17 @@ package com.tangem.data.walletconnect.network.solana import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.data.walletconnect.model.NamespaceKey import com.tangem.data.walletconnect.utils.WcNamespaceConverter +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet + +internal class WcSolanaNetwork constructor( + private val excludedBlockchains: ExcludedBlockchains, +) : WcNamespaceConverter { -internal class WcSolanaNetwork : WcNamespaceConverter { override val namespaceKey: NamespaceKey = NamespaceKey("solana") override fun toBlockchain(chainId: CAIP2): Blockchain? { @@ -17,7 +23,12 @@ internal class WcSolanaNetwork : WcNamespaceConverter { } } - override fun toCAIP2(blockchain: Blockchain): CAIP2? { + override fun toNetwork(chainId: String, wallet: UserWallet): Network? { + return toNetwork(chainId, wallet, excludedBlockchains) + } + + override fun toCAIP2(network: Network): CAIP2? { + val blockchain = Blockchain.fromId(network.id.value) val chainId = when (blockchain) { Blockchain.Solana -> MAINNET_CHAIN_ID Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID 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 ce3ec70378..316902dfea 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 @@ -1,11 +1,6 @@ package com.tangem.data.walletconnect.pair import com.reown.walletkit.client.Wallet -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.data.common.currency.getNetwork -import com.tangem.data.walletconnect.model.CAIP2 -import com.tangem.data.walletconnect.model.NamespaceKey import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -16,17 +11,16 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase internal class AssociateNetworksDelegate constructor( - private val namespaceConverters: Map, + private val namespaceConverters: Set, private val getWallets: GetWalletsUseCase, private val currenciesRepository: CurrenciesRepository, - private val excludedBlockchains: ExcludedBlockchains, ) { @Throws(WcPairError.UnsupportedNetworks::class) suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } - val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() - val optionalNamespaces: Set = sessionProposal.optionalNamespaces.setOfChainId() + val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() + val optionalNamespaces: Set = sessionProposal.optionalNamespaces.setOfChainId() return userWallets .associateWith { wallet -> mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces) } @@ -34,8 +28,8 @@ internal class AssociateNetworksDelegate constructor( private suspend fun mapNetworksForWallet( wallet: UserWallet, - requiredNamespaces: Set, - optionalNamespaces: Set, + requiredNamespaces: Set, + optionalNamespaces: Set, ): ProposalNetwork { val walletNetworks = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId) .filterIsInstance() @@ -47,23 +41,10 @@ internal class AssociateNetworksDelegate constructor( val available = mutableSetOf() val notAdded = mutableSetOf() - fun CAIP2.toBlockchain() = namespaceConverters[NamespaceKey(this.namespace)]?.toBlockchain(this) - fun Blockchain.toNetwork() = getNetwork( - blockchain = this, - extraDerivationPath = null, - scanResponse = wallet.scanResponse, - excludedBlockchains = excludedBlockchains, - ) - requiredNamespaces.forEach { chainId -> - val blockchain = chainId.toBlockchain() - if (blockchain == null) { - unknownRequired.add(missingNetworkName(chainId)) - return@forEach - } - val wcNetwork = blockchain.toNetwork() + val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } if (wcNetwork == null) { - unknownRequired.add(missingNetworkName(blockchain)) + unknownRequired.add(missingNetworkName(chainId)) return@forEach } val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } @@ -74,7 +55,8 @@ internal class AssociateNetworksDelegate constructor( } } optionalNamespaces.forEach { chainId -> - val wcNetwork = chainId.toBlockchain()?.toNetwork() ?: return@forEach + val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } + ?: return@forEach val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } if (walletNetwork == null) { available.add(wcNetwork) @@ -92,10 +74,8 @@ internal class AssociateNetworksDelegate constructor( ) } - private fun Map.setOfChainId(): Set = - this.values.flatMap { proposal -> proposal.chains ?: listOf() } - .mapNotNull { rawChainId -> CAIP2.fromRaw(rawChainId) }.toSet() + private fun Map.setOfChainId(): Set = + this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet() - private fun missingNetworkName(chainId: CAIP2): String = chainId.namespace.replaceFirstChar(Char::titlecase) - private fun missingNetworkName(blockchain: Blockchain): String = blockchain.getCoinName() + private fun missingNetworkName(chainId: String): String = chainId.replaceFirstChar(Char::titlecase) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt index 2361baa676..597bdecefb 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt @@ -1,16 +1,14 @@ package com.tangem.data.walletconnect.pair import com.reown.walletkit.client.Wallet -import com.tangem.blockchain.common.Blockchain import com.tangem.data.walletconnect.model.CAIP10 -import com.tangem.data.walletconnect.model.NamespaceKey import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet internal class CaipNamespaceDelegate constructor( - private val namespaceConverters: Map, + private val namespaceConverters: Set, private val walletManagersFacade: WalletManagersFacade, ) { @@ -19,14 +17,11 @@ internal class CaipNamespaceDelegate constructor( userWallet: UserWallet, networks: List, ): Map { - val converters = namespaceConverters.values - val result = mutableMapOf() networks.map { network -> - val blockchain = Blockchain.fromId(network.id.value) val address = walletManagersFacade.getDefaultAddress(userWallet.walletId, network) - val chainId = converters.firstOrNull { it.toCAIP2(blockchain) != null }?.toCAIP2(blockchain) + val chainId = namespaceConverters.firstNotNullOfOrNull { it.toCAIP2(network) } requireNotNull(chainId) requireNotNull(address) CAIP10(chainId = chainId, accountAddress = address) 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 08839fa987..d4b9fdbf33 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,25 +1,22 @@ 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.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkSessionRequestConverter -import com.tangem.domain.walletconnect.model.WcMethod -import com.tangem.domain.walletconnect.model.WcRequest -import com.tangem.domain.walletconnect.repository.WcSessionsManager -import com.tangem.domain.walletconnect.request.WcRequestService +import com.tangem.domain.walletconnect.usecase.WcMethodUseCase +import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch internal class DefaultWcRequestService( - private val sessionsManager: WcSessionsManager, - private val respondService: WcRespondService, - private val requestAdapters: Set>, + private val requestConverters: Set, private val scope: CoroutineScope, -) : WcRequestService, WcSdkObserver { +) : WcSdkObserver, WcUseCasesFlowProvider { - override val requests: MutableSharedFlow> = MutableSharedFlow() + private val _useCases: Channel = Channel(Channel.BUFFERED) + override val useCases = _useCases.receiveAsFlow() override fun onSessionRequest( sessionRequest: Wallet.Model.SessionRequest, @@ -27,21 +24,10 @@ internal class DefaultWcRequestService( ) { // Triggered when a Dapp sends SessionRequest to sign a transaction or a message val sr = WcSdkSessionRequestConverter.convert(sessionRequest) - val method = sr.request.method - val params = sr.request.params scope.launch { - val session = sessionsManager.findSessionByTopic(sr.topic) - val handler: WcMethodHandler? = requestAdapters.firstOrNull { it.canHandle(method) } - - val deserialized: WcMethod? = handler?.deserialize(method, params) - if (handler == null || deserialized == null || session == null) { - respondService.rejectRequest(sr, "UnsupportedMethod") // todo(wc) use our domain error - return@launch - } - - val wcRequest = WcRequest(sr, session, deserialized) - handler.handle(wcRequest) - requests.emit(wcRequest) + val useCase = requestConverters.firstNotNullOfOrNull { it.toUseCase(sr) } + ?: return@launch // todo(wc) handle unsupported + _useCases.trySend(useCase) } } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcMethodHandler.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcMethodHandler.kt deleted file mode 100644 index b31b707d58..0000000000 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcMethodHandler.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.data.walletconnect.request - -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter -import com.tangem.domain.walletconnect.model.WcMethod -import com.tangem.domain.walletconnect.model.WcRequest - -interface WcMethodHandler { - fun canHandle(methodName: String): Boolean - fun deserialize(methodName: String, params: String): M? - fun handle(wcRequest: WcRequest) - - companion object { - @OptIn(ExperimentalStdlibApi::class) - inline fun fromJson(params: String, moshi: Moshi): T? = - runCatching { moshi.adapter().fromJsonValue(params) }.getOrNull() - } -} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcRequestToUseCaseConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcRequestToUseCaseConverter.kt new file mode 100644 index 0000000000..624440c0df --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcRequestToUseCaseConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.data.walletconnect.request + +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.usecase.WcMethodUseCase + +interface WcRequestToUseCaseConverter { + suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? + + companion object { + @OptIn(ExperimentalStdlibApi::class) + inline fun Moshi.fromJson(params: String): T? = + runCatching { this.adapter().fromJson(params) }.getOrNull() + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt index bb1e86298b..a95ea4604b 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt @@ -1,12 +1,31 @@ package com.tangem.data.walletconnect.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.getNetwork import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.data.walletconnect.model.NamespaceKey +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet internal interface WcNamespaceConverter { val namespaceKey: NamespaceKey + fun toBlockchain(chainId: CAIP2): Blockchain? - fun toCAIP2(blockchain: Blockchain): CAIP2? + fun toBlockchain(chainId: String): Blockchain? = toCAIP2(chainId)?.let { caip2 -> toBlockchain(caip2) } + + fun toCAIP2(network: Network): CAIP2? + fun toCAIP2(chainId: String): CAIP2? = CAIP2.fromRaw(chainId) + + fun toNetwork(chainId: String, wallet: UserWallet): Network? + fun toNetwork(chainId: String, wallet: UserWallet, excludedBlockchains: ExcludedBlockchains): Network? { + val blockchain = toBlockchain(chainId) ?: return null + return getNetwork( + blockchain = blockchain, + extraDerivationPath = null, + scanResponse = wallet.scanResponse, + excludedBlockchains = excludedBlockchains, + ) + } } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt deleted file mode 100644 index 5918accfd9..0000000000 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.walletconnect.request - -import com.tangem.domain.walletconnect.model.WcRequest -import kotlinx.coroutines.flow.Flow - -interface WcRequestService { - val requests: Flow> -} \ No newline at end of file From cb04ddc53335f1cdda74a2422789318afb661aac Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 14 Apr 2025 12:12:25 +0500 Subject: [PATCH 44/72] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 66 +------ .../com/tangem/common/routing/AppRoute.kt | 21 +-- .../nft/component/NFTAssetTraitsComponent.kt | 14 -- .../nft/component/NFTCollectionsComponent.kt | 14 -- ...NFTReceiveComponent.kt => NFTComponent.kt} | 4 +- .../nft/component/NFTDetailsComponent.kt | 17 -- ...omponent.kt => NFTCollectionsComponent.kt} | 24 +-- .../collections/model/NFTCollectionsModel.kt | 44 ++--- .../nft/common/DefaultNFTComponent.kt | 172 ++++++++++++++++++ .../tangem/features/nft/common/NFTRoute.kt | 31 ++++ .../features/nft/common/ui/NFTContent.kt | 37 ++++ ...ilsComponent.kt => NFTDetailsComponent.kt} | 25 +-- .../nft/details/model/NFTDetailsModel.kt | 18 +- .../features/nft/di/NFTFeatureModule.kt | 27 +-- ...iveComponent.kt => NFTReceiveComponent.kt} | 21 +-- .../nft/receive/model/NFTReceiveModel.kt | 14 +- ...omponent.kt => NFTAssetTraitsComponent.kt} | 23 +-- .../nft/traits/model/NFTAssetTraitsModel.kt | 14 +- .../intents/WalletContentClickIntents.kt | 2 +- .../router/DefaultWalletRouter.kt | 4 +- .../presentation/router/InnerWalletRouter.kt | 2 +- 21 files changed, 325 insertions(+), 269 deletions(-) delete mode 100644 features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTAssetTraitsComponent.kt delete mode 100644 features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTCollectionsComponent.kt rename features/nft/api/src/main/kotlin/com/tangem/features/nft/component/{NFTReceiveComponent.kt => NFTComponent.kt} (68%) delete mode 100644 features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt rename features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/{DefaultNFTCollectionsComponent.kt => NFTCollectionsComponent.kt} (60%) create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/NFTRoute.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTContent.kt rename features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/{DefaultNFTDetailsComponent.kt => NFTDetailsComponent.kt} (59%) rename features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/{DefaultNFTReceiveComponent.kt => NFTReceiveComponent.kt} (60%) rename features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/{DefaultNFTAssetTraitsComponent.kt => NFTAssetTraitsComponent.kt} (59%) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 005557d07b..362ae78fb2 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -12,10 +12,7 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.nft.component.NFTCollectionsComponent -import com.tangem.features.nft.component.NFTDetailsComponent -import com.tangem.features.nft.component.NFTReceiveComponent -import com.tangem.features.nft.component.NFTAssetTraitsComponent +import com.tangem.features.nft.component.* import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.PushNotificationsComponent @@ -85,10 +82,7 @@ internal class ChildFactory @Inject constructor( private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory, private val sendFeatureToggles: SendFeatureToggles, private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory, - private val nftCollectionsComponentFactory: NFTCollectionsComponent.Factory, - private val nftReceiveComponentFactory: NFTReceiveComponent.Factory, - private val nftDetailsComponentFactory: NFTDetailsComponent.Factory, - private val nftAssetTraitsComponentFactory: NFTAssetTraitsComponent.Factory, + private val nftComponentFactory: NFTComponent.Factory, private val nftSendComponentFactory: NFTSendComponent.Factory, private val testerRouter: TesterRouter, private val routingFeatureToggles: RoutingFeatureToggles, @@ -409,33 +403,11 @@ internal class ChildFactory @Inject constructor( componentFactory = walletComponentFactory, ) } - is AppRoute.NFTCollections -> + is AppRoute.NFT -> createComponentChild( context = context, - params = NFTCollectionsComponent.Params(userWalletId = route.userWalletId), - componentFactory = nftCollectionsComponentFactory, - ) - is AppRoute.NFTReceive -> - createComponentChild( - context = context, - params = NFTReceiveComponent.Params(userWalletId = route.userWalletId), - componentFactory = nftReceiveComponentFactory, - ) - is AppRoute.NFTDetails -> - createComponentChild( - context = context, - params = NFTDetailsComponent.Params( - userWalletId = route.userWalletId, - nftAsset = route.nftAsset, - nftCollectionName = route.collectionName, - ), - componentFactory = nftDetailsComponentFactory, - ) - is AppRoute.NFTAssetTraits -> - createComponentChild( - context = context, - params = NFTAssetTraitsComponent.Params(nftAsset = route.nftAsset), - componentFactory = nftAssetTraitsComponentFactory, + params = NFTComponent.Params(userWalletId = route.userWalletId), + componentFactory = nftComponentFactory, ) is AppRoute.NFTSend -> { createComponentChild( @@ -782,33 +754,11 @@ internal class ChildFactory @Inject constructor( componentFactory = storiesComponentFactory, ) } - is AppRoute.NFTCollections -> + is AppRoute.NFT -> route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = NFTCollectionsComponent.Params(userWalletId = route.userWalletId), - componentFactory = nftCollectionsComponentFactory, - ) - is AppRoute.NFTReceive -> - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = NFTReceiveComponent.Params(userWalletId = route.userWalletId), - componentFactory = nftReceiveComponentFactory, - ) - is AppRoute.NFTDetails -> - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = NFTDetailsComponent.Params( - userWalletId = route.userWalletId, - nftAsset = route.nftAsset, - nftCollectionName = route.collectionName, - ), - componentFactory = nftDetailsComponentFactory, - ) - is AppRoute.NFTAssetTraits -> - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = NFTAssetTraitsComponent.Params(nftAsset = route.nftAsset), - componentFactory = nftAssetTraitsComponentFactory, + params = NFTComponent.Params(userWalletId = route.userWalletId), + componentFactory = nftComponentFactory, ) is AppRoute.NFTSend -> { route.asComponentChild( diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index a21f44aa02..508661dac0 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -281,26 +281,9 @@ sealed class AppRoute(val path: String) : Route { ) : AppRoute(path = "/stories$storyId") @Serializable - data class NFTCollections( + data class NFT( val userWalletId: UserWalletId, - ) : AppRoute(path = "/nft_collections/${userWalletId.stringValue}") - - @Serializable - data class NFTReceive( - val userWalletId: UserWalletId, - ) : AppRoute(path = "/nft_receive/${userWalletId.stringValue}") - - @Serializable - data class NFTDetails( - val userWalletId: UserWalletId, - val nftAsset: NFTAsset, - val collectionName: String, - ) : AppRoute(path = "/nft_details/${userWalletId.stringValue}/${nftAsset.collectionId}/${nftAsset.id.stringValue}") - - @Serializable - data class NFTAssetTraits( - val nftAsset: NFTAsset, - ) : AppRoute(path = "/nft_traits/${nftAsset.collectionId}/${nftAsset.id.stringValue}") + ) : AppRoute(path = "/nft/${userWalletId.stringValue}") @Serializable data class NFTSend( diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTAssetTraitsComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTAssetTraitsComponent.kt deleted file mode 100644 index 2be81fbcef..0000000000 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTAssetTraitsComponent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.nft.component - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.nft.models.NFTAsset - -interface NFTAssetTraitsComponent : ComposableContentComponent { - - data class Params( - val nftAsset: NFTAsset, - ) - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTCollectionsComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTCollectionsComponent.kt deleted file mode 100644 index 3417b6a1f2..0000000000 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTCollectionsComponent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.nft.component - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.wallets.models.UserWalletId - -interface NFTCollectionsComponent : ComposableContentComponent { - - data class Params( - val userWalletId: UserWalletId, - ) - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTReceiveComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTComponent.kt similarity index 68% rename from features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTReceiveComponent.kt rename to features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTComponent.kt index 9184afa340..f2f8d3f9e5 100644 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTReceiveComponent.kt +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTComponent.kt @@ -4,11 +4,11 @@ import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.wallets.models.UserWalletId -interface NFTReceiveComponent : ComposableContentComponent { +interface NFTComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, ) - interface Factory : ComponentFactory + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt deleted file mode 100644 index 2848de0dd5..0000000000 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.nft.component - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.nft.models.NFTAsset -import com.tangem.domain.wallets.models.UserWalletId - -interface NFTDetailsComponent : ComposableContentComponent { - - data class Params( - val userWalletId: UserWalletId, - val nftAsset: NFTAsset, - val nftCollectionName: String, - ) - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/DefaultNFTCollectionsComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/NFTCollectionsComponent.kt similarity index 60% rename from features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/DefaultNFTCollectionsComponent.kt rename to features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/NFTCollectionsComponent.kt index bb82591e48..209883f972 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/DefaultNFTCollectionsComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/NFTCollectionsComponent.kt @@ -6,17 +6,18 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.nft.collections.model.NFTCollectionsModel import com.tangem.features.nft.collections.ui.NFTCollections -import com.tangem.features.nft.component.NFTCollectionsComponent import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -internal class DefaultNFTCollectionsComponent @AssistedInject constructor( +internal class NFTCollectionsComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted private val params: NFTCollectionsComponent.Params, -) : NFTCollectionsComponent, AppComponentContext by context { + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { private val model: NFTCollectionsModel = getOrCreateModel(params) @@ -27,11 +28,10 @@ internal class DefaultNFTCollectionsComponent @AssistedInject constructor( NFTCollections(state, modifier) } - @AssistedFactory - interface Factory : NFTCollectionsComponent.Factory { - override fun create( - context: AppComponentContext, - params: NFTCollectionsComponent.Params, - ): DefaultNFTCollectionsComponent - } + data class Params( + val userWalletId: UserWalletId, + val onBackClick: () -> Unit, + val onAssetClick: (asset: NFTAsset, collectionName: String) -> Unit, + val onReceiveClick: () -> Unit, + ) } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt index b8b89b6d9b..2e1ddc7a19 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -1,24 +1,21 @@ package com.tangem.features.nft.collections.model -import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection +import com.tangem.features.nft.collections.NFTCollectionsComponent import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM import com.tangem.features.nft.collections.entity.NFTCollectionsUM import com.tangem.features.nft.collections.entity.transformer.ChangeCollectionExpandedStateTransformer import com.tangem.features.nft.collections.entity.transformer.ToggleSearchBarTransformer import com.tangem.features.nft.collections.entity.transformer.UpdateDataStateTransformer import com.tangem.features.nft.collections.entity.transformer.UpdateSearchQueryTransformer -import com.tangem.features.nft.component.NFTCollectionsComponent import com.tangem.features.nft.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -29,23 +26,22 @@ import javax.inject.Inject @ModelScoped internal class NFTCollectionsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val router: Router, private val searchManager: InputManager, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase, paramsContainer: ParamsContainer, ) : Model() { - val state: StateFlow get() = _state + private val params: NFTCollectionsComponent.Params = paramsContainer.require() private val _state = MutableStateFlow( value = NFTCollectionsStateUM( - onBackClick = ::navigateBack, - content = NFTCollectionsUM.Loading(::onReceiveClick), + onBackClick = params.onBackClick, + content = NFTCollectionsUM.Loading(params.onBackClick), ), ) - private val params: NFTCollectionsComponent.Params = paramsContainer.require() + val state: StateFlow get() = _state init { subscribeToNFTCollections() @@ -60,11 +56,15 @@ internal class NFTCollectionsModel @Inject constructor( UpdateDataStateTransformer( nftCollections = nftCollections, searchQuery = query, - onReceiveClick = ::onReceiveClick, + onReceiveClick = { + params.onReceiveClick() + }, onRetryClick = ::onRetryClick, onExpandCollectionClick = ::onExpandCollectionClick, onRetryAssetsClick = ::onRetryAssetsClick, - onAssetClick = ::onAssetClick, + onAssetClick = { asset, collectionName -> + params.onAssetClick(asset, collectionName) + }, initialSearchBarFactory = ::getInitialSearchBar, ).transform(it) } @@ -115,28 +115,6 @@ internal class NFTCollectionsModel @Inject constructor( // TODO refresh all } - private fun onAssetClick(asset: NFTAsset, collectionName: String) { - router.push( - AppRoute.NFTDetails( - userWalletId = params.userWalletId, - nftAsset = asset, - collectionName = collectionName, - ), - ) - } - - private fun onReceiveClick() { - router.push( - AppRoute.NFTReceive( - userWalletId = params.userWalletId, - ), - ) - } - - private fun navigateBack() { - router.pop() - } - private fun loadCollectionAssets(collection: NFTCollection) { modelScope.launch { fetchNFTCollectionAssetsUseCase( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt new file mode 100644 index 0000000000..1c85ec5d39 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt @@ -0,0 +1,172 @@ +package com.tangem.features.nft.common + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.nft.collections.NFTCollectionsComponent +import com.tangem.features.nft.common.ui.NFTContent +import com.tangem.features.nft.component.NFTComponent +import com.tangem.features.nft.details.NFTDetailsComponent +import com.tangem.features.nft.receive.NFTReceiveComponent +import com.tangem.features.nft.traits.NFTAssetTraitsComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +internal class DefaultNFTComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: NFTComponent.Params, +) : NFTComponent, AppComponentContext by appComponentContext { + + private val stackNavigation = StackNavigation() + + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) + + private val initialRoute: NFTRoute = NFTRoute.Collections(params.userWalletId) + private val currentRoute = MutableStateFlow(initialRoute) + + private val childStack = childStack( + key = "sendInnerStack", + source = stackNavigation, + serializer = null, + initialConfiguration = initialRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createChild( + configuration, + childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + ) + }, + ) + + init { + childStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + currentRoute.emit(stack.active.configuration) + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val stackState by childStack.subscribeAsState() + + BackHandler(onBack = ::onChildBack) + NFTContent( + stackState = stackState, + ) + } + + private fun createChild(route: NFTRoute, factoryContext: AppComponentContext) = when (route) { + is NFTRoute.Collections -> getCollectionsComponent(factoryContext, route) + is NFTRoute.Receive -> getReceiveComponent(factoryContext, route) + is NFTRoute.Details -> getDetailsComponent(factoryContext, route) + is NFTRoute.AssetTraits -> getAssetTraitsComponent(factoryContext, route) + } + + private fun getCollectionsComponent( + factoryContext: AppComponentContext, + route: NFTRoute.Collections, + ): ComposableContentComponent = NFTCollectionsComponent( + context = factoryContext, + params = NFTCollectionsComponent.Params( + userWalletId = route.userWalletId, + onBackClick = ::onChildBack, + onReceiveClick = { + innerRouter.push( + NFTRoute.Receive( + userWalletId = route.userWalletId, + ), + ) + }, + onAssetClick = { asset, collectionName -> + innerRouter.push( + NFTRoute.Details( + userWalletId = route.userWalletId, + nftAsset = asset, + collectionName = collectionName, + ), + ) + }, + ), + ) + + private fun getReceiveComponent( + factoryContext: AppComponentContext, + route: NFTRoute.Receive, + ): ComposableContentComponent = NFTReceiveComponent( + context = factoryContext, + params = NFTReceiveComponent.Params( + userWalletId = route.userWalletId, + onBackClick = ::onChildBack, + ), + ) + + private fun getDetailsComponent( + factoryContext: AppComponentContext, + route: NFTRoute.Details, + ): ComposableContentComponent = NFTDetailsComponent( + context = factoryContext, + params = NFTDetailsComponent.Params( + userWalletId = route.userWalletId, + nftAsset = route.nftAsset, + nftCollectionName = route.collectionName, + onBackClick = ::onChildBack, + onAllTraitsClick = { + innerRouter.push( + NFTRoute.AssetTraits( + nftAsset = route.nftAsset, + ), + ) + }, + ), + ) + + private fun getAssetTraitsComponent( + factoryContext: AppComponentContext, + route: NFTRoute.AssetTraits, + ): ComposableContentComponent = NFTAssetTraitsComponent( + context = factoryContext, + params = NFTAssetTraitsComponent.Params( + nftAsset = route.nftAsset, + onBackClick = ::onChildBack, + ), + ) + + private fun onChildBack() { + val isEmptyStack = childStack.value.backStack.isEmpty() + + if (isEmptyStack) { + router.pop() + } else { + stackNavigation.pop() + } + } + + @AssistedFactory + interface Factory : NFTComponent.Factory { + override fun create(context: AppComponentContext, params: NFTComponent.Params): DefaultNFTComponent + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/NFTRoute.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/NFTRoute.kt new file mode 100644 index 0000000000..5c3482f2f6 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/NFTRoute.kt @@ -0,0 +1,31 @@ +package com.tangem.features.nft.common + +import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +internal sealed class NFTRoute : Route { + + @Serializable + data class Collections( + val userWalletId: UserWalletId, + ) : NFTRoute() + + @Serializable + data class Receive( + val userWalletId: UserWalletId, + ) : NFTRoute() + + @Serializable + data class Details( + val userWalletId: UserWalletId, + val nftAsset: NFTAsset, + val collectionName: String, + ) : NFTRoute() + + @Serializable + data class AssetTraits( + val nftAsset: NFTAsset, + ) : NFTRoute() +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTContent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTContent.kt new file mode 100644 index 0000000000..ef907f1e0f --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTContent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.nft.common.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.nft.common.NFTRoute + +@Composable +internal fun NFTContent(stackState: ChildStack) { + Column( + modifier = Modifier.Companion + .background(color = TangemTheme.colors.background.tertiary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.Companion.CenterHorizontally, + ) { + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.weight(1f), + ) { + it.instance.Content(Modifier.weight(1f)) + } + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/DefaultNFTDetailsComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt similarity index 59% rename from features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/DefaultNFTDetailsComponent.kt rename to features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt index bb220d9f9f..274d87d25e 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/DefaultNFTDetailsComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt @@ -6,17 +6,18 @@ 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.nft.component.NFTDetailsComponent +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.nft.details.model.NFTDetailsModel import com.tangem.features.nft.details.ui.NFTDetails import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -internal class DefaultNFTDetailsComponent @AssistedInject constructor( +internal class NFTDetailsComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted private val params: NFTDetailsComponent.Params, -) : NFTDetailsComponent, AppComponentContext by context { + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { private val model: NFTDetailsModel = getOrCreateModel(params) @@ -27,11 +28,11 @@ internal class DefaultNFTDetailsComponent @AssistedInject constructor( NFTDetails(state, modifier) } - @AssistedFactory - interface Factory : NFTDetailsComponent.Factory { - override fun create( - context: AppComponentContext, - params: NFTDetailsComponent.Params, - ): DefaultNFTDetailsComponent - } + data class Params( + val userWalletId: UserWalletId, + val nftAsset: NFTAsset, + val nftCollectionName: String, + val onBackClick: () -> Unit, + val onAllTraitsClick: () -> Unit, + ) } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index 4c45c7a714..c81f317e67 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -5,7 +5,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router -import com.tangem.features.nft.component.NFTDetailsComponent +import com.tangem.features.nft.details.NFTDetailsComponent import com.tangem.features.nft.details.entity.NFTAssetUM import com.tangem.features.nft.details.entity.NFTDetailsUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -35,9 +35,9 @@ internal class NFTDetailsModel @Inject constructor( traits = persistentListOf(), baseInfoItems = persistentListOf(), ), - onBackClick = ::navigateBack, + onBackClick = params.onBackClick, onReadMoreClick = ::onReadMoreClick, - onSeeAllTraitsClick = ::onSeeAllTraitsClick, + onSeeAllTraitsClick = params.onAllTraitsClick, onExploreClick = ::onExploreClick, onSendClick = ::onSendClick, bottomSheetConfig = null, @@ -48,14 +48,6 @@ internal class NFTDetailsModel @Inject constructor( // TODO implement } - private fun onSeeAllTraitsClick() { - router.push( - AppRoute.NFTAssetTraits( - nftAsset = params.nftAsset, - ), - ) - } - private fun onExploreClick() { // TODO implement } @@ -69,8 +61,4 @@ internal class NFTDetailsModel @Inject constructor( ), ) } - - private fun navigateBack() { - router.pop() - } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt index af79399a92..abc85dcd0a 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt @@ -4,19 +4,12 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.decompose.model.Model import com.tangem.features.nft.DefaultNFTFeatureToggles import com.tangem.features.nft.NFTFeatureToggles -import com.tangem.features.nft.collections.DefaultNFTCollectionsComponent import com.tangem.features.nft.collections.model.NFTCollectionsModel -import com.tangem.features.nft.component.NFTCollectionsComponent -import com.tangem.features.nft.component.NFTDetailsBlockComponent -import com.tangem.features.nft.component.NFTDetailsComponent -import com.tangem.features.nft.component.NFTReceiveComponent -import com.tangem.features.nft.component.NFTAssetTraitsComponent -import com.tangem.features.nft.details.DefaultNFTDetailsComponent +import com.tangem.features.nft.common.DefaultNFTComponent +import com.tangem.features.nft.component.* import com.tangem.features.nft.details.block.DefaultNFTDetailsBlockComponent import com.tangem.features.nft.details.model.NFTDetailsModel -import com.tangem.features.nft.receive.DefaultNFTReceiveComponent import com.tangem.features.nft.receive.model.NFTReceiveModel -import com.tangem.features.nft.traits.DefaultNFTAssetTraitsComponent import com.tangem.features.nft.traits.model.NFTAssetTraitsModel import dagger.Binds import dagger.Module @@ -43,28 +36,18 @@ internal object NFTFeatureModule { internal interface NFTFeatureModuleBinds { @Binds @Singleton - fun bindNFTCollectionsComponentFactory( - impl: DefaultNFTCollectionsComponent.Factory, - ): NFTCollectionsComponent.Factory + fun bindNFTComponentFactory(impl: DefaultNFTComponent.Factory): NFTComponent.Factory @Binds @IntoMap @ClassKey(NFTCollectionsModel::class) fun bindNFTCollectionsModel(model: NFTCollectionsModel): Model - @Binds - @Singleton - fun bindNFTReceiveComponentFactory(impl: DefaultNFTReceiveComponent.Factory): NFTReceiveComponent.Factory - @Binds @IntoMap @ClassKey(NFTReceiveModel::class) fun bindNFTReceiveModel(model: NFTReceiveModel): Model - @Binds - @Singleton - fun bindNFTDetailsComponentFactory(impl: DefaultNFTDetailsComponent.Factory): NFTDetailsComponent.Factory - @Binds @IntoMap @ClassKey(NFTDetailsModel::class) @@ -76,10 +59,6 @@ internal interface NFTFeatureModuleBinds { impl: DefaultNFTDetailsBlockComponent.Factory, ): NFTDetailsBlockComponent.Factory - @Binds - @Singleton - fun bindNFTTraitsComponentFactory(impl: DefaultNFTAssetTraitsComponent.Factory): NFTAssetTraitsComponent.Factory - @Binds @IntoMap @ClassKey(NFTAssetTraitsModel::class) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/DefaultNFTReceiveComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt similarity index 60% rename from features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/DefaultNFTReceiveComponent.kt rename to features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt index f9651f18e3..0be6d0b5f7 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/DefaultNFTReceiveComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt @@ -6,17 +6,17 @@ 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.nft.component.NFTReceiveComponent +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.nft.receive.model.NFTReceiveModel import com.tangem.features.nft.receive.ui.NFTReceive import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -internal class DefaultNFTReceiveComponent @AssistedInject constructor( +internal class NFTReceiveComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted private val params: NFTReceiveComponent.Params, -) : NFTReceiveComponent, AppComponentContext by context { + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { private val model: NFTReceiveModel = getOrCreateModel(params) @@ -27,11 +27,8 @@ internal class DefaultNFTReceiveComponent @AssistedInject constructor( NFTReceive(state, modifier) } - @AssistedFactory - interface Factory : NFTReceiveComponent.Factory { - override fun create( - context: AppComponentContext, - params: NFTReceiveComponent.Params, - ): DefaultNFTReceiveComponent - } + data class Params( + val userWalletId: UserWalletId, + val onBackClick: () -> Unit, + ) } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt index dc0eefd9b3..83bfeedb42 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -3,7 +3,6 @@ package com.tangem.features.nft.receive.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.core.decompose.navigation.Router import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.fields.InputManager @@ -14,8 +13,8 @@ import com.tangem.domain.nft.GetNFTAvailableNetworksUseCase import com.tangem.domain.nft.GetNFTNetworkStatusUseCase import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.features.nft.component.NFTReceiveComponent import com.tangem.features.nft.impl.R +import com.tangem.features.nft.receive.NFTReceiveComponent import com.tangem.features.nft.receive.entity.NFTReceiveUM import com.tangem.features.nft.receive.entity.transformer.ShowReceiveBottomSheetTransformer import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransformer @@ -31,7 +30,6 @@ import javax.inject.Inject @ModelScoped internal class NFTReceiveModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val router: Router, private val searchManager: InputManager, private val getNFTAvailableNetworksUseCase: GetNFTAvailableNetworksUseCase, private val filterNFTAvailableNetworksUseCase: FilterNFTAvailableNetworksUseCase, @@ -41,18 +39,18 @@ internal class NFTReceiveModel @Inject constructor( paramsContainer: ParamsContainer, ) : Model() { - val state: StateFlow get() = _state + private val params: NFTReceiveComponent.Params = paramsContainer.require() private val _state = MutableStateFlow( value = NFTReceiveUM( - onBackClick = ::navigateBack, + onBackClick = params.onBackClick, search = getInitialSearchBar(), networks = NFTReceiveUM.Networks.Content(persistentListOf()), bottomSheetConfig = null, ), ) - private val params: NFTReceiveComponent.Params = paramsContainer.require() + val state: StateFlow get() = _state init { subscribeToNFTAvailableNetworks() @@ -140,8 +138,4 @@ internal class NFTReceiveModel @Inject constructor( private fun onShareClick(text: String) { shareManager.shareText(text = text) } - - private fun navigateBack() { - router.pop() - } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/DefaultNFTAssetTraitsComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/NFTAssetTraitsComponent.kt similarity index 59% rename from features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/DefaultNFTAssetTraitsComponent.kt rename to features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/NFTAssetTraitsComponent.kt index ce9f6af262..ec54b5846d 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/DefaultNFTAssetTraitsComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/NFTAssetTraitsComponent.kt @@ -6,17 +6,17 @@ 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.nft.component.NFTAssetTraitsComponent -import com.tangem.features.nft.traits.ui.NFTAssetTraits +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.nft.models.NFTAsset import com.tangem.features.nft.traits.model.NFTAssetTraitsModel +import com.tangem.features.nft.traits.ui.NFTAssetTraits import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -internal class DefaultNFTAssetTraitsComponent @AssistedInject constructor( +internal class NFTAssetTraitsComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted private val params: NFTAssetTraitsComponent.Params, -) : NFTAssetTraitsComponent, AppComponentContext by context { + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { private val model: NFTAssetTraitsModel = getOrCreateModel(params) @@ -27,11 +27,8 @@ internal class DefaultNFTAssetTraitsComponent @AssistedInject constructor( NFTAssetTraits(state) } - @AssistedFactory - interface Factory : NFTAssetTraitsComponent.Factory { - override fun create( - context: AppComponentContext, - params: NFTAssetTraitsComponent.Params, - ): DefaultNFTAssetTraitsComponent - } + data class Params( + val nftAsset: NFTAsset, + val onBackClick: () -> Unit, + ) } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/model/NFTAssetTraitsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/model/NFTAssetTraitsModel.kt index 73c030f059..52ed09006f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/model/NFTAssetTraitsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/model/NFTAssetTraitsModel.kt @@ -3,9 +3,8 @@ package com.tangem.features.nft.traits.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.core.decompose.navigation.Router import com.tangem.domain.nft.models.NFTAsset -import com.tangem.features.nft.component.NFTAssetTraitsComponent +import com.tangem.features.nft.traits.NFTAssetTraitsComponent import com.tangem.features.nft.traits.entity.NFTAssetTraitUM import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -17,21 +16,20 @@ import javax.inject.Inject @ModelScoped internal class NFTAssetTraitsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val router: Router, paramsContainer: ParamsContainer, ) : Model() { private val params: NFTAssetTraitsComponent.Params = paramsContainer.require() - val state: StateFlow get() = _state - private val _state = MutableStateFlow( value = NFTAssetTraitsUM( traits = params.nftAsset.transform(), - onBackClick = ::navigateBack, + onBackClick = { params.onBackClick() }, ), ) + val state: StateFlow get() = _state + private fun NFTAsset.transform(): ImmutableList = this .traits .mapIndexed { index, trait -> @@ -41,8 +39,4 @@ internal class NFTAssetTraitsModel @Inject constructor( value = trait.value, ) }.toPersistentList() - - private fun navigateBack() { - router.pop() - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 79fd4f1a7f..c1e1f1fde0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -240,6 +240,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } override fun onNFTClick(userWalletId: UserWalletId) { - router.openNFTCollectionsScreen(userWalletId) + router.openNFT(userWalletId) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index f6305b826f..23ac155b8b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -116,7 +116,7 @@ internal class DefaultWalletRouter @Inject constructor( reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain)) } - override fun openNFTCollectionsScreen(userWalletId: UserWalletId) { - router.push(AppRoute.NFTCollections(userWalletId)) + override fun openNFT(userWalletId: UserWalletId) { + router.push(AppRoute.NFT(userWalletId)) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 4e49fb9f70..ad25778b8a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -58,5 +58,5 @@ internal interface InnerWalletRouter { fun openScanFailedDialog(onTryAgain: () -> Unit) /** Open NFT collections screen */ - fun openNFTCollectionsScreen(userWalletId: UserWalletId) + fun openNFT(userWalletId: UserWalletId) } \ No newline at end of file From 47a30176a69480d122f4fda1002b282c3125fd45 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Apr 2025 14:12:25 +0500 Subject: [PATCH 45/72] Updated on 2026-08-14 --- features/nft/impl/build.gradle.kts | 1 + .../transformer/UpdateDataStateTransformer.kt | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index a33b3e1ad5..7aba5590b7 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.core.datasource) /** Domain modules */ + implementation(projects.domain.models) implementation(projects.domain.nft) implementation(projects.domain.nft.models) implementation(projects.domain.tokens.models) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt index c5eec23071..77f0c846ee 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt @@ -61,7 +61,25 @@ internal class UpdateDataStateTransformer( state: NFTCollectionsStateUM, query: String, ): ImmutableList = mapNotNull { - if (query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true) { + val assetsFulfillQuery = if (query.isEmpty()) { + true + } else { + when (val assets = it.assets) { + is NFTCollection.Assets.Empty, + is NFTCollection.Assets.Failed, + is NFTCollection.Assets.Loading, + -> true + is NFTCollection.Assets.Value -> { + assets.items.any { asset -> + asset.name?.lowercase()?.contains(query.lowercase()) == true + } + } + } + } + + val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true + + if (collectionFulfillQuery || assetsFulfillQuery) { NFTCollectionUM( id = it.id.toString(), networkIconId = getActiveIconRes(it.network.id.value), From 275b53dae212262037b9b5ff0ac1e3a9a5ae015c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 21:24:16 +0400 Subject: [PATCH 46/72] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tap/di/domain/MarketsDomainModule.kt | 13 ++- .../tap/di/domain/TokensDomainModule.kt | 10 ++ .../data/quotes/di/QuoteFetcherModule.kt | 24 +++++ .../quotes/di/QuoteProducerFactoryModule.kt | 18 ++++ .../data/quotes/di/QuoteSupplierModule.kt | 42 ++++++++ domain/markets/build.gradle.kts | 1 + .../markets/GetCurrencyQuotesUseCase.kt | 23 ++++- domain/tokens/build.gradle.kts | 1 + .../CachedCurrenciesStatusesOperations.kt | 96 +++++++++++++++---- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 2 + features/wallet/impl/build.gradle.kts | 1 + 12 files changed, 206 insertions(+), 27 deletions(-) create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt create 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/QuoteSupplierModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b36d773b95..c2091e3c79 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -109,6 +109,7 @@ dependencies { implementation(projects.domain.promo) implementation(projects.domain.promo.models) implementation(projects.domain.networks) + implementation(projects.domain.quotes) implementation(projects.common) implementation(projects.common.routing) @@ -152,6 +153,7 @@ dependencies { implementation(projects.data.nft) implementation(projects.data.onramp) implementation(projects.data.networks) + implementation(projects.data.quotes) /** Features */ implementation(projects.features.onboarding) 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 3a568db5e6..0e18657362 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 @@ -5,6 +5,7 @@ import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -49,8 +50,16 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase { - return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository) + fun provideGetTokenQuotesUseCase( + quotesRepository: QuotesRepository, + singleQuoteSupplier: SingleQuoteSupplier, + tokensFeatureToggles: TokensFeatureToggles, + ): GetCurrencyQuotesUseCase { + return GetCurrencyQuotesUseCase( + quotesRepository = quotesRepository, + singleQuoteSupplier = singleQuoteSupplier, + tokensFeatureToggles = tokensFeatureToggles, + ) } @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 99bb5efeef..eedc9d7f2b 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 @@ -7,6 +7,8 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier 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.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.* import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations @@ -391,6 +393,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteFetcher: MultiQuoteFetcher, + singleQuoteSupplier: SingleQuoteSupplier, ): BaseCurrenciesStatusesOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -400,6 +404,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteFetcher = multiQuoteFetcher, + singleQuoteSupplier = singleQuoteSupplier, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -415,6 +421,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteFetcher: MultiQuoteFetcher, + singleQuoteSupplier: SingleQuoteSupplier, ): BaseCurrencyStatusOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -424,6 +432,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteFetcher = multiQuoteFetcher, + singleQuoteSupplier = singleQuoteSupplier, 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 new file mode 100644 index 0000000000..5a5eb2ecfd --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt @@ -0,0 +1,24 @@ +package com.tangem.data.quotes.di + +import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher +import com.tangem.data.quotes.multi.DefaultMultiQuoteUpdater +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteUpdater +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 +} \ 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 new file mode 100644 index 0000000000..0fc3d0faed --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt @@ -0,0 +1,18 @@ +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/QuoteSupplierModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt new file mode 100644 index 0000000000..af8188e5bc --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt @@ -0,0 +1,42 @@ +package com.tangem.data.quotes.di + +import androidx.datastore.core.DataStore +import com.tangem.data.quotes.store.CurrencyIdWithQuote +import com.tangem.data.quotes.store.DefaultQuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object QuoteSupplierModule { + + @Provides + @Singleton + fun provideQuotesStoreV2( + persistenceQuotesStore: DataStore, + dispatchers: CoroutineDispatcherProvider, + ): QuotesStoreV2 { + return DefaultQuotesStoreV2( + runtimeStore = RuntimeSharedStore(), + persistenceDataStore = persistenceQuotesStore, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideSingleQuoteSupplier(factory: SingleQuoteProducer.Factory): SingleQuoteSupplier { + return object : SingleQuoteSupplier( + factory = factory, + keyCreator = { "single_quote_${it.rawCurrencyId.value}" }, + ) {} + } +} \ No newline at end of file diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 6ba3a16199..7d820cb19a 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { api(projects.domain.models) api(projects.domain.networks) api(projects.domain.staking) + api(projects.domain.quotes) api(projects.domain.wallets) api(projects.domain.wallets.models) 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 dcd308db12..080a102047 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 @@ -3,6 +3,9 @@ package com.tangem.domain.markets import arrow.core.None import arrow.core.Option import arrow.core.toOption +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.QuotesRepository @@ -11,9 +14,10 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -@Suppress("UnusedPrivateMember") class GetCurrencyQuotesUseCase( private val quotesRepository: QuotesRepository, + private val singleQuoteSupplier: SingleQuoteSupplier, + private val tokensFeatureToggles: TokensFeatureToggles, ) { // TODO apply interval parameter [REDACTED_TASK_KEY] operator fun invoke( @@ -23,9 +27,18 @@ class GetCurrencyQuotesUseCase( ): Flow> { val rawId = currencyID.rawCurrencyId ?: return flowOf(None) - return quotesRepository.getQuotesUpdatesLegacy( - currenciesIds = setOf(rawId), - refresh = refresh, - ).map { it.filterIsInstance().firstOrNull().toOption() }.catch { emit(None) } + return if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + singleQuoteSupplier( + params = SingleQuoteProducer.Params(rawCurrencyId = rawId), + ) + .map { (it as? Quote.Value).toOption() } + } else { + quotesRepository.getQuotesUpdatesLegacy( + currenciesIds = setOf(rawId), + refresh = refresh, + ) + .map { it.filterIsInstance().firstOrNull().toOption() } + } + .catch { emit(None) } } } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 6ba910ceae..f24fb5baf6 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.domain.promo.models) implementation(projects.domain.promo) implementation(projects.domain.networks) + implementation(projects.domain.quotes) /** Project - Api */ implementation(projects.features.send.api) 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 3334482cf9..d8d8c6b538 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 @@ -14,6 +14,9 @@ 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.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository @@ -30,7 +33,7 @@ import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, @@ -39,6 +42,8 @@ class CachedCurrenciesStatusesOperations( private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val multiQuoteFetcher: MultiQuoteFetcher, + private val singleQuoteSupplier: SingleQuoteSupplier, private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrenciesStatusesOperations, BaseCurrencyStatusOperations( @@ -176,7 +181,14 @@ class CachedCurrenciesStatusesOperations( }, async { val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } - quotesRepository.fetchQuotes(rawCurrenciesIds) + + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + multiQuoteFetcher( + params = MultiQuoteFetcher.Params(currenciesIds = rawCurrenciesIds, appCurrencyId = null), + ) + } else { + quotesRepository.fetchQuotes(rawCurrenciesIds) + } }, async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) }, ) @@ -247,27 +259,44 @@ class CachedCurrenciesStatusesOperations( } private fun getQuotes(tokensIds: NonEmptySet): Flow>> { - return quotesRepository.getQuotesUpdates(tokensIds.mapNotNull { it.rawCurrencyId }.toSet()) - .map, Either>> { it.right() } - .retryWhen { cause, _ -> - emit(TokenListError.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_DELAY) - true - } - .distinctUntilChanged() + return if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + getQuotesUpdates( + rawCurrencyIds = tokensIds.mapNotNullTo( + destination = hashSetOf(), + transform = CryptoCurrency.ID::rawCurrencyId, + ), + ) + } else { + quotesRepository.getQuotesUpdates(tokensIds.mapNotNull { it.rawCurrencyId }.toSet()) + .map, Either>> { it.right() } + .retryWhen { cause, _ -> + emit(TokenListError.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } } override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { - return quotesRepository.getQuotesUpdates(setOf(id)) - .map, Either>> { it.right() } - .retryWhen { cause, _ -> - emit(Error.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_DELAY) - true - } - .distinctUntilChanged() + return if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + singleQuoteSupplier( + params = SingleQuoteProducer.Params(rawCurrencyId = id), + ) + .map>> { setOf(it).right() } + .distinctUntilChanged() + } else { + quotesRepository.getQuotesUpdates(setOf(id)) + .map, Either>> { it.right() } + .retryWhen { cause, _ -> + emit(Error.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } } override fun getNetworksStatuses( @@ -361,6 +390,33 @@ class CachedCurrenciesStatusesOperations( .distinctUntilChanged() } + // temporary code because token list is built using networks list + private fun getQuotesUpdates(rawCurrencyIds: Set): EitherFlow> { + return channelFlow { + val state = MutableStateFlow(emptySet()) + + rawCurrencyIds.onEach { + launch { + singleQuoteSupplier( + params = SingleQuoteProducer.Params(rawCurrencyId = it), + ) + .onEach { quote -> + state.update { loadedStatuses -> + loadedStatuses.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId } + } + } + .launchIn(scope = this) + } + } + + state + .onEach(::send) + .launchIn(scope = this) + } + .map, Either>> { it.right() } + .distinctUntilChanged() + } + private fun isFetchingStarted(userWalletId: UserWalletId): Boolean { return fetchingState.value[userWalletId] ?: false } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index b515f71dd9..8f08bc34ea 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -186,6 +186,8 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { singleNetworkStatusSupplier = mockk(), multiNetworkStatusFetcher = mockk(), multiNetworkStatusSupplier = mockk(), + multiQuoteFetcher = mockk(), + singleQuoteSupplier = mockk(), ), dispatchers = dispatchers, ) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index da4e888dfe..b2a3af1b04 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { implementation(projects.domain.onramp.models) implementation(projects.domain.promo) implementation(projects.domain.promo.models) + implementation(projects.domain.quotes) implementation(projects.domain.settings) implementation(projects.domain.staking) implementation(projects.domain.staking.models) From da088d3ed16f486c2af2297745c48b29b538083f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 14 Apr 2025 15:49:28 +0300 Subject: [PATCH 47/72] 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 884ee07a79..55d8cba764 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-1014" +tangemBlockchainSdk = "develop-1022" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 698e2a8660b6d8ccf93acae0f8f9d1b9e1db3101 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 14 Apr 2025 15:41:37 +0000 Subject: [PATCH 48/72] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 3ac68363ee..55d8cba764 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,9 +1,15 @@ [versions] -tangemBlockchainSdk = "releases-5.22-1016" +# Blockchain SDK, Card SDK and Vico artifacts are built in a private namespace and then published to a +# public Maven repository, so anyone can build the app from source +# https://github.com/tangem/blockchain-sdk-kotlin/packages/ +# https://github.com/tangem/tangem-sdk-android/ +# https://github.com/tangem/vico + +tangemBlockchainSdk = "develop-1022" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemVico = "2.0.0-alpha.25-tangem-developments8" +tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 13a474b281462dc6e7376c11eb73e4d0d84f3091 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Apr 2025 15:57:54 +0400 Subject: [PATCH 49/72] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 3 ++ .../tap/di/domain/MarketsDomainModule.kt | 3 ++ .../tap/di/domain/TokensDomainModule.kt | 6 +++ domain/manage-tokens/build.gradle.kts | 1 + .../managetokens/SaveManagedTokensUseCase.kt | 19 ++++++-- .../domain/markets/SaveMarketTokensUseCase.kt | 19 ++++++-- .../tokens/AddCryptoCurrenciesUseCase.kt | 46 +++++++++++++++---- ...RefreshMultiCurrencyWalletQuotesUseCase.kt | 28 +++++++---- .../BaseCurrencyStatusOperations.kt | 38 +++++++++++---- .../CachedCurrenciesStatusesOperations.kt | 1 + 10 files changed, 130 insertions(+), 34 deletions(-) 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 f6467cc073..07f1c7e97f 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,6 +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.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -73,6 +74,7 @@ internal object ManageTokensDomainModule { stakingRepository: StakingRepository, quotesRepository: QuotesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteFetcher: MultiQuoteFetcher, tokensFeatureToggles: TokensFeatureToggles, ): SaveManagedTokensUseCase { return SaveManagedTokensUseCase( @@ -84,6 +86,7 @@ internal object ManageTokensDomainModule { stakingRepository = stakingRepository, quotesRepository = quotesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteFetcher = multiQuoteFetcher, 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 0e18657362..b6efd9bd4f 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 @@ -5,6 +5,7 @@ import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles @@ -72,6 +73,7 @@ object MarketsDomainModule { stakingRepository: StakingRepository, quotesRepository: QuotesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteFetcher: MultiQuoteFetcher, tokensFeatureToggles: TokensFeatureToggles, ): SaveMarketTokensUseCase { return SaveMarketTokensUseCase( @@ -82,6 +84,7 @@ object MarketsDomainModule { stakingRepository = stakingRepository, quotesRepository = quotesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteFetcher = multiQuoteFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } 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 eedc9d7f2b..eccaa7a678 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 @@ -37,6 +37,7 @@ internal object TokensDomainModule { stakingRepository: StakingRepository, quotesRepository: QuotesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteFetcher: MultiQuoteFetcher, tokensFeatureToggles: TokensFeatureToggles, ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( @@ -45,6 +46,7 @@ internal object TokensDomainModule { stakingRepository = stakingRepository, quotesRepository = quotesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteFetcher = multiQuoteFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -366,10 +368,14 @@ internal object TokensDomainModule { fun provideRefreshMultiCurrencyWalletQuotesUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, + multiQuoteFetcher: MultiQuoteFetcher, + tokensFeatureToggles: TokensFeatureToggles, ): RefreshMultiCurrencyWalletQuotesUseCase { return RefreshMultiCurrencyWalletQuotesUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, + multiQuoteFetcher = multiQuoteFetcher, + tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index ef5facece8..cff1f6e9aa 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { api(projects.domain.core) api(projects.domain.manageTokens.models) api(projects.domain.networks) + api(projects.domain.quotes) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) implementation(projects.domain.staking) 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 9f78e90712..8379c39929 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 @@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency @@ -26,6 +27,7 @@ class SaveManagedTokensUseCase( private val stakingRepository: StakingRepository, private val quotesRepository: QuotesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val multiQuoteFetcher: MultiQuoteFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -124,10 +126,19 @@ class SaveManagedTokensUseCase( } private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { - quotesRepository.fetchQuotes( - currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, - refresh = true, - ) + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + multiQuoteFetcher( + params = MultiQuoteFetcher.Params( + currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + appCurrencyId = null, + ), + ) + } else { + quotesRepository.fetchQuotes( + currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + refresh = true, + ) + } } /** 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 4d086677b5..06b6a81591 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 @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency @@ -31,6 +32,7 @@ class SaveMarketTokensUseCase( private val stakingRepository: StakingRepository, private val quotesRepository: QuotesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val multiQuoteFetcher: MultiQuoteFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -106,9 +108,18 @@ class SaveMarketTokensUseCase( } private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { - quotesRepository.fetchQuotes( - currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, - refresh = true, - ) + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + multiQuoteFetcher( + params = MultiQuoteFetcher.Params( + currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + appCurrencyId = null, + ), + ) + } else { + quotesRepository.fetchQuotes( + currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + refresh = true, + ) + } } } \ 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 56fc636d4f..e2354e1813 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 @@ -5,6 +5,7 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -12,6 +13,9 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope /** * A use case for adding multiple cryptocurrencies to a user's wallet. @@ -20,12 +24,14 @@ import com.tangem.domain.wallets.models.UserWalletId * network statuses, particularly after the addition of new tokens. */ // TODO: Add tests +@Suppress("LongParameterList") class AddCryptoCurrenciesUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val quotesRepository: QuotesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val multiQuoteFetcher: MultiQuoteFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -68,9 +74,14 @@ class AddCryptoCurrenciesUseCase( val currencyToAdd = currency.takeUnless(existingCurrencies::contains) ?: return@either addCurrencies(userWalletId, currencyToAdd) - refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) - refreshUpdatedYieldBalances(userWalletId, currencyToAdd) - refreshUpdatedQuotes(currencyToAdd) + + coroutineScope { + awaitAll( + async { refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) }, + async { refreshUpdatedYieldBalances(userWalletId, currencyToAdd) }, + async { refreshUpdatedQuotes(currencyToAdd) }, + ) + } } suspend operator fun invoke( @@ -94,9 +105,15 @@ class AddCryptoCurrenciesUseCase( } val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId) addCurrencies(userWalletId, tokenToAdd) - refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) - refreshUpdatedYieldBalances(userWalletId, tokenToAdd) - refreshUpdatedQuotes(tokenToAdd) + + coroutineScope { + awaitAll( + async { refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) }, + async { refreshUpdatedYieldBalances(userWalletId, tokenToAdd) }, + async { refreshUpdatedQuotes(tokenToAdd) }, + ) + } + tokenToAdd } @@ -150,10 +167,19 @@ class AddCryptoCurrenciesUseCase( } private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { - quotesRepository.fetchQuotes( - currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId), - refresh = true, - ) + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + multiQuoteFetcher( + params = MultiQuoteFetcher.Params( + currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId), + appCurrencyId = null, + ), + ) + } else { + quotesRepository.fetchQuotes( + currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId), + refresh = true, + ) + } } private suspend fun Raise.createTokenCurrency( 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 1688660d10..9ebfe0bced 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 @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.getOrElse import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.tokens.error.QuotesError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -16,6 +17,8 @@ import kotlinx.coroutines.coroutineScope class RefreshMultiCurrencyWalletQuotesUseCase( private val quotesRepository: QuotesRepository, private val currenciesRepository: CurrenciesRepository, + private val multiQuoteFetcher: MultiQuoteFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { @@ -45,14 +48,23 @@ class RefreshMultiCurrencyWalletQuotesUseCase( } private suspend fun fetchQuotes(currenciesIds: Set) { - catch( - block = { - quotesRepository.fetchQuotes( + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + multiQuoteFetcher( + params = MultiQuoteFetcher.Params( currenciesIds = currenciesIds.mapNotNullTo(hashSetOf(), CryptoCurrency.ID::rawCurrencyId), - refresh = true, - ) - }, - catch = { /* Ignore error */ }, - ) + appCurrencyId = null, + ), + ) + } else { + catch( + block = { + quotesRepository.fetchQuotes( + currenciesIds = currenciesIds.mapNotNullTo(hashSetOf(), CryptoCurrency.ID::rawCurrencyId), + refresh = true, + ) + }, + catch = { /* Ignore error */ }, + ) + } } } \ No newline at end of file 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 ac008a8ac8..7c11e772f1 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 @@ -10,6 +10,8 @@ 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.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository @@ -41,6 +43,7 @@ abstract class BaseCurrencyStatusOperations( private val stakingRepository: StakingRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + private val singleQuoteSupplier: SingleQuoteSupplier, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -171,7 +174,16 @@ abstract class BaseCurrencyStatusOperations( } else { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId) } - val quote = cryptoCurrencyId.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) }?.right() + + val quote = cryptoCurrencyId.rawCurrencyId?.let { rawId -> + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + singleQuoteSupplier(params = SingleQuoteProducer.Params(rawCurrencyId = rawId)) + .firstOrNull() + } else { + quotesRepository.getQuoteSync(rawId) + } + } + ?.right() ?: Error.EmptyQuotes.left() val networkStatuses = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { @@ -268,13 +280,23 @@ abstract class BaseCurrencyStatusOperations( block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, catch = { raise(Error.DataError(it)) }, ) - val quotes = catch( - block = { - currency.id.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) } - ?.right() ?: Error.EmptyQuotes.left() - }, - catch = { Error.DataError(it).left() }, - ) + + val quotes = if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + currency.id.rawCurrencyId?.let { + singleQuoteSupplier(params = SingleQuoteProducer.Params(rawCurrencyId = it)) + .firstOrNull() + } + ?.right() + ?: Error.EmptyQuotes.left() + } else { + catch( + block = { + currency.id.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) } + ?.right() ?: Error.EmptyQuotes.left() + }, + catch = { Error.DataError(it).left() }, + ) + } val networkStatus = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { singleNetworkStatusSupplier( 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 d8d8c6b538..cfb27d170e 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 @@ -53,6 +53,7 @@ class CachedCurrenciesStatusesOperations( stakingRepository = stakingRepository, multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier, + singleQuoteSupplier = singleQuoteSupplier, tokensFeatureToggles = tokensFeatureToggles, ) { From 29c57b05882e957bdc041d281509ec46ec6f74c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Apr 2025 09:19:18 +0000 Subject: [PATCH 50/72] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index c6bc2129f5..55d8cba764 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.23-1026" +tangemBlockchainSdk = "develop-1022" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-456" +tangemCardSdk = "develop-455" #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 ^ @@ -21,4 +21,4 @@ card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tange vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } -vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } +vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } \ No newline at end of file From a42383430be937d673d528cf91b3c234019c0057 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Apr 2025 16:40:35 +0700 Subject: [PATCH 51/72] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 18 +++++++++ .../DefaultTransactionRepository.kt | 23 +++++++++++ data/wallet-connect/build.gradle.kts | 2 +- .../DefaultWcPersonalEthSignUseCase.kt | 22 ++-------- .../transaction/TransactionRepository.kt | 7 ++++ .../usecase/PrepareForSendUseCase.kt | 38 ++++++++++++++++++ .../domain/transaction/usecase/SignUseCase.kt | 40 +++++++++++++++++++ gradle/tangem_dependencies.toml | 2 +- 8 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.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 adc63cbd3d..55d35c9244 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 @@ -147,4 +147,22 @@ internal object TransactionDomainModule { fun provideGetAllowanceUseCase(transactionRepository: TransactionRepository): GetAllowanceUseCase { return GetAllowanceUseCase(transactionRepository) } + + @Provides + @Singleton + fun providePrepareForSendUseCase( + transactionRepository: TransactionRepository, + cardSdkConfigRepository: CardSdkConfigRepository, + ): PrepareForSendUseCase { + return PrepareForSendUseCase(transactionRepository, cardSdkConfigRepository) + } + + @Provides + @Singleton + fun provideSignUseCase( + walletManagersFacade: WalletManagersFacade, + cardSdkConfigRepository: CardSdkConfigRepository, + ): SignUseCase { + return SignUseCase(cardSdkConfigRepository, walletManagersFacade) + } } \ No newline at end of file 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 07f16b38b8..6d3ddca431 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 @@ -280,4 +280,27 @@ internal class DefaultTransactionRepository( else -> null } } + + override suspend fun prepareForSend( + transactionData: TransactionData, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ): Result = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + val preparer = walletManager as? TransactionPreparer ?: kotlin.run { + Timber.e("${walletManager?.wallet?.blockchain} does not support TransactionBuilder") + error("Wallet manager does not support TransactionPreparer") + } + + when (val prepareForSend = preparer.prepareForSend(transactionData, signer)) { + is com.tangem.blockchain.extensions.Result.Failure -> Result.failure(prepareForSend.error) + is com.tangem.blockchain.extensions.Result.Success -> Result.success(prepareForSend.data) + } + } } \ No newline at end of file diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index d1da51e0b0..e130216844 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -14,9 +14,9 @@ dependencies { /* Project - Domain */ implementation(projects.domain.walletConnect) implementation(projects.domain.walletConnect.models) + implementation(projects.domain.transaction) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) - implementation(projects.domain.card) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.models) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt index d6d7cd9c1f..98e6f2db49 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt @@ -1,12 +1,10 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left -import arrow.core.right import com.tangem.blockchain.blockchains.ethereum.EthereumUtils import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.HEX_PREFIX import com.tangem.blockchain.extensions.isAscii -import com.tangem.common.CompletionResult import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.data.walletconnect.respond.WcRespondService @@ -14,9 +12,7 @@ import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.OnSign import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext -import com.tangem.domain.card.models.TwinKey -import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.domain.common.TapWorkarounds.isTangemTwins +import com.tangem.domain.transaction.usecase.SignUseCase import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.usecase.ethereum.WcPersonalEthSignUseCase import com.tangem.domain.walletconnect.usecase.sign.WcSignState @@ -30,29 +26,17 @@ internal class DefaultWcPersonalEthSignUseCase( override val context: WcMethodUseCaseContext, private val method: WcEthMethod.PersonalEthSign, private val walletManagersFacade: WalletManagersFacade, - private val cardRepository: CardSdkConfigRepository, + private val signUseCase: SignUseCase, ) : BaseWcSignUseCase(), WcPersonalEthSignUseCase { override val onSign: OnSign = collector@{ state -> val hashToSign = LegacySdkHelper.createMessageData(state.signModel.rawMsg) val userWallet = session.wallet - val card = userWallet.scanResponse.card - val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins val walletManager = walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network) ?: return@collector - val signer = cardRepository.getCommonSigner( - cardId = card.cardId.takeIf { isCardNotBackedUp }, - twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), - ) - - val signResult = when (val signResult = signer.sign(hashToSign, walletManager.wallet.publicKey)) { - is CompletionResult.Failure -> signResult.error.left() - is CompletionResult.Success -> signResult.data.right() - } - - val signedHash = signResult + val signedHash = signUseCase(hashToSign, userWallet, network) .onLeft { emit(state.toResult(it.left())) } .getOrNull() ?: return@collector 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 61ac813b52..892fa06f78 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 @@ -78,4 +78,11 @@ interface TransactionRepository { cryptoCurrency: CryptoCurrency.Token, spenderAddress: String, ): BigDecimal + + suspend fun prepareForSend( + transactionData: TransactionData, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ): Result } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt new file mode 100644 index 0000000000..58d073be30 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt @@ -0,0 +1,38 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.TransactionData +import com.tangem.domain.card.models.TwinKey +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.common.TapWorkarounds.isTangemTwins +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWallet + +class PrepareForSendUseCase( + private val transactionRepository: TransactionRepository, + private val cardSdkConfigRepository: CardSdkConfigRepository, +) { + suspend operator fun invoke( + transactionData: TransactionData, + userWallet: UserWallet, + network: Network, + ): Either { + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + + val signer = cardSdkConfigRepository.getCommonSigner( + cardId = card.cardId.takeIf { isCardNotBackedUp }, + twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), + ) + return transactionRepository.prepareForSend( + transactionData = transactionData, + userWalletId = userWallet.walletId, + network = network, + signer = signer, + ) + .fold(onSuccess = { it.right() }, onFailure = { it.left() }) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt new file mode 100644 index 0000000000..d5496faf38 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemError +import com.tangem.domain.card.models.TwinKey +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.common.TapWorkarounds.isTangemTwins +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet + +class SignUseCase( + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val walletManagersFacade: WalletManagersFacade, +) { + suspend operator fun invoke( + hash: ByteArray, + userWallet: UserWallet, + network: Network, + ): Either { + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + + val signer = cardSdkConfigRepository.getCommonSigner( + cardId = card.cardId.takeIf { isCardNotBackedUp }, + twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), + ) + + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network) + ?: error("WalletManager not found") + + return when (val signResult = signer.sign(hash, walletManager.wallet.publicKey)) { + is CompletionResult.Failure -> signResult.error.left() + is CompletionResult.Success -> signResult.data.right() + } + } +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 55d8cba764..a32c8679a0 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-1022" +tangemBlockchainSdk = "develop-1027" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 3d214007f4bacbcc9a9c005593198bd11968a3a4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Apr 2025 16:19:02 +0400 Subject: [PATCH 52/72] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 11 ++++++++ .../data/quotes/DefaultQuotesRepositoryV2.kt | 25 ++++++++++++++++++ .../tangem/data/quotes/di/QuotesDataModule.kt | 18 +++++++++++++ .../domain/quotes/QuotesRepositoryV2.kt | 15 +++++++++++ .../tokens/FetchCardTokenListUseCase.kt | 17 +++++++++--- .../tokens/FetchCurrencyStatusUseCase.kt | 20 +++++++++++--- .../domain/tokens/FetchTokenListUseCase.kt | 26 ++++++++++++++----- .../BaseCurrencyStatusOperations.kt | 9 ++++++- .../CachedCurrenciesStatusesOperations.kt | 3 +++ ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 1 + features/swap/domain/build.gradle.kts | 22 ++++++++-------- .../feature/swap/domain/SwapInteractorImpl.kt | 10 ++++++- features/tokendetails/impl/build.gradle.kts | 1 + .../factory/express/ExchangeStatusFactory.kt | 13 ++++++++-- 14 files changed, 161 insertions(+), 30 deletions(-) create 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/di/QuotesDataModule.kt create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt 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 eccaa7a678..695196deaf 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 @@ -7,6 +7,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier 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.multi.MultiQuoteFetcher import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.repositories.StakingRepository @@ -59,6 +60,7 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteFetcher: MultiQuoteFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchTokenListUseCase { return FetchTokenListUseCase( @@ -67,6 +69,7 @@ internal object TokensDomainModule { quotesRepository = quotesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteFetcher = multiQuoteFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -172,6 +175,7 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + multiQuoteFetcher: MultiQuoteFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchCurrencyStatusUseCase { return FetchCurrencyStatusUseCase( @@ -180,6 +184,7 @@ internal object TokensDomainModule { quotesRepository = quotesRepository, stakingRepository = stakingRepository, singleNetworkStatusFetcher = singleNetworkStatusFetcher, + multiQuoteFetcher = multiQuoteFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -192,6 +197,7 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteFetcher: MultiQuoteFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchCardTokenListUseCase { return FetchCardTokenListUseCase( @@ -200,6 +206,7 @@ internal object TokensDomainModule { quotesRepository = quotesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteFetcher = multiQuoteFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -394,6 +401,7 @@ internal object TokensDomainModule { tokensFeatureToggles: TokensFeatureToggles, currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, + quotesRepositoryV2: QuotesRepositoryV2, networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, @@ -405,6 +413,7 @@ internal object TokensDomainModule { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, + quotesRepositoryV2 = quotesRepositoryV2, networksRepository = networksRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, @@ -422,6 +431,7 @@ internal object TokensDomainModule { tokensFeatureToggles: TokensFeatureToggles, currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, + quotesRepositoryV2: QuotesRepositoryV2, networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, @@ -433,6 +443,7 @@ internal object TokensDomainModule { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, + quotesRepositoryV2 = quotesRepositoryV2, networksRepository = networksRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, 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 new file mode 100644 index 0000000000..a39da5f600 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt @@ -0,0 +1,25 @@ +package com.tangem.data.quotes + +import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.domain.quotes.QuotesRepositoryV2 +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import javax.inject.Inject + +/** + * Default implementation of [QuotesRepositoryV2] + * + * @property quotesStore quotes store + * +[REDACTED_AUTHOR] + */ +internal class DefaultQuotesRepositoryV2 @Inject constructor( + private val quotesStore: QuotesStoreV2, +) : QuotesRepositoryV2 { + + override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? { + return quotesStore.getAllSyncOrNull()?.mapTo(hashSetOf()) { + it.takeIf { it.rawCurrencyId in currenciesIds } ?: Quote.Empty(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 new file mode 100644 index 0000000000..e3c96931a8 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt @@ -0,0 +1,18 @@ +package com.tangem.data.quotes.di + +import com.tangem.data.quotes.DefaultQuotesRepositoryV2 +import com.tangem.domain.quotes.QuotesRepositoryV2 +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 QuotesDataModule { + + @Binds + @Singleton + fun bindQuotesRepositoryV2(impl: DefaultQuotesRepositoryV2): QuotesRepositoryV2 +} \ 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 new file mode 100644 index 0000000000..347104dbfb --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.quotes + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote + +/** + * Quotes repository + * +[REDACTED_AUTHOR] + */ +interface QuotesRepositoryV2 { + + /** Get quotes by [currenciesIds] synchronously or null */ + suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? +} \ No newline at end of file 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 2098e11d7b..20e19dfaf0 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 @@ -5,6 +5,7 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -17,12 +18,14 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +@Suppress("LongParameterList") class FetchCardTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val multiQuoteFetcher: MultiQuoteFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -90,10 +93,16 @@ class FetchCardTokenListUseCase( } private suspend fun fetchQuotes(currenciesIds: Set, refresh: Boolean) { - catch( - block = { quotesRepository.getQuotesSync(currenciesIds, refresh) }, - catch = { /* Ignore error */ }, - ) + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + multiQuoteFetcher( + params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null), + ) + } else { + catch( + block = { quotesRepository.getQuotesSync(currenciesIds, refresh) }, + catch = { /* Ignore error */ }, + ) + } } private suspend fun fetchYieldBalances( 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 fa725b8c75..a38ae046d5 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 @@ -5,6 +5,7 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency @@ -27,12 +28,14 @@ import kotlinx.coroutines.coroutineScope * @param quotesRepository The repository for retrieving cryptocurrency quotes. */ // TODO: Add tests +@Suppress("LongParameterList") class FetchCurrencyStatusUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + private val multiQuoteFetcher: MultiQuoteFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -134,10 +137,19 @@ class FetchCurrencyStatusUseCase( } private suspend fun Raise.fetchQuote(currencyId: CryptoCurrency.ID, refresh: Boolean) { - catch( - block = { quotesRepository.getQuotesSync(setOfNotNull(currencyId.rawCurrencyId), refresh) }, - ) { - raise(CurrencyStatusError.DataError(it)) + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + multiQuoteFetcher( + params = MultiQuoteFetcher.Params( + currenciesIds = setOfNotNull(currencyId.rawCurrencyId), + appCurrencyId = null, + ), + ) + } else { + catch( + block = { quotesRepository.getQuotesSync(setOfNotNull(currencyId.rawCurrencyId), refresh) }, + ) { + raise(CurrencyStatusError.DataError(it)) + } } } 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 5344fecfd0..4602529c72 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 @@ -7,6 +7,7 @@ import arrow.core.raise.either import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -29,12 +30,14 @@ import kotlinx.coroutines.coroutineScope * @param stakingRepository The repository for retrieving staking-related data. */ // TODO: Add tests +@Suppress("LongParameterList") class FetchTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val multiQuoteFetcher: MultiQuoteFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -125,13 +128,22 @@ class FetchTokenListUseCase( } private suspend fun fetchQuotes(currenciesIds: Set, refresh: Boolean) { - catch( - block = { - val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() - quotesRepository.getQuotesSync(rawIds, refresh) - }, - ) { - /* Ignore error */ + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + multiQuoteFetcher( + params = MultiQuoteFetcher.Params( + currenciesIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet(), + appCurrencyId = null, + ), + ) + } else { + catch( + block = { + val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() + quotesRepository.getQuotesSync(rawIds, refresh) + }, + ) { + /* Ignore error */ + } } } 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 7c11e772f1..14c18382e1 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 @@ -10,6 +10,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.single.SingleQuoteProducer import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -39,6 +40,7 @@ import kotlinx.coroutines.flow.* abstract class BaseCurrencyStatusOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, + private val quotesRepositoryV2: QuotesRepositoryV2, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -249,7 +251,12 @@ abstract class BaseCurrencyStatusOperations( ?: return emptyList().right() val (networks, currenciesIds) = getIds(nonEmptyCurrencies) val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() - val quotes = quotesRepository.getQuotesSync(rawIds, false).right() + + val quotes = if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = rawIds)?.right() + } else { + quotesRepository.getQuotesSync(rawIds, false).right() + } val networkStatuses = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { multiNetworkStatusSupplier( 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 d4c4fcb9d9..919ee39358 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 @@ -14,6 +14,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.multi.MultiQuoteFetcher import com.tangem.domain.quotes.single.SingleQuoteProducer import com.tangem.domain.quotes.single.SingleQuoteSupplier @@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.* class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, + quotesRepositoryV2: QuotesRepositoryV2, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, @@ -49,6 +51,7 @@ class CachedCurrenciesStatusesOperations( BaseCurrencyStatusOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, + quotesRepositoryV2 = quotesRepositoryV2, networksRepository = networksRepository, stakingRepository = stakingRepository, multiNetworkStatusSupplier = multiNetworkStatusSupplier, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 8f08bc34ea..841fbf11ab 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -188,6 +188,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { multiNetworkStatusSupplier = mockk(), multiQuoteFetcher = mockk(), singleQuoteSupplier = mockk(), + quotesRepositoryV2 = mockk(), ), dispatchers = dispatchers, ) diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 14278f36c8..7d52289e23 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -1,5 +1,3 @@ -import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants - plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -23,22 +21,24 @@ dependencies { /** Domain */ implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.card) + implementation(projects.domain.demo) + implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.quotes) + implementation(projects.domain.staking) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) + implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) - implementation(projects.domain.transaction) - implementation(projects.domain.legacy) - implementation(projects.libs.blockchainSdk) - implementation(projects.domain.demo) - implementation(projects.domain.card) - implementation(projects.domain.appCurrency.models) - implementation(projects.domain.txhistory.models) - implementation(projects.domain.transaction.models) + implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) - implementation(projects.domain.staking) + implementation(projects.libs.blockchainSdk) /** Core modules */ implementation(projects.core.utils) 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 29e7ee6c0d..041739a131 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 @@ -15,8 +15,10 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.quotes.QuotesRepositoryV2 import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.GetCurrencyCheckUseCase +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -62,6 +64,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val quotesRepository: QuotesRepository, + private val quotesRepositoryV2: QuotesRepositoryV2, + private val tokensFeatureToggles: TokensFeatureToggles, private val swapTransactionRepository: SwapTransactionRepository, private val currencyChecksRepository: CurrencyChecksRepository, private val appCurrencyRepository: AppCurrencyRepository, @@ -2166,7 +2170,11 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun Set.getQuotesOrEmpty(refresh: Boolean): Set { return try { - quotesRepository.getQuotesSync(this, refresh) + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = this).orEmpty() + } else { + quotesRepository.getQuotesSync(this, refresh) + } } catch (t: Throwable) { emptySet() } diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index e725d7833a..b80dda2c34 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -86,6 +86,7 @@ dependencies { implementation(projects.domain.onramp.models) implementation(projects.domain.promo) implementation(projects.domain.promo.models) + implementation(projects.domain.quotes) /** Temp dependency to swap domain */ implementation(projects.features.swap.domain) 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 f9d9aed682..6890405a1d 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 @@ -5,7 +5,9 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler 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.quotes.QuotesRepositoryV2 import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent @@ -15,10 +17,10 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter -import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.utils.Provider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -35,6 +37,8 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private val swapTransactionRepository: SwapTransactionRepository, private val swapRepository: SwapRepository, private val quotesRepository: QuotesRepository, + private val quotesRepositoryV2: QuotesRepositoryV2, + private val tokensFeatureToggles: TokensFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val swapTransactionStatusStore: SwapTransactionStatusStore, @@ -198,7 +202,12 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private suspend fun Set.getQuotesOrEmpty(refresh: Boolean): Set { return try { val rawIds = mapNotNull { it.rawCurrencyId }.toSet() - quotesRepository.getQuotesSync(rawIds, refresh) + + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = rawIds).orEmpty() + } else { + quotesRepository.getQuotesSync(rawIds, refresh) + } } catch (t: Throwable) { emptySet() } From fd4c93ce8d3b0e5abf24c8b96941f4da2312dc16 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Apr 2025 11:27:57 +0500 Subject: [PATCH 53/72] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 15 + .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../main/res/drawable/ic_network_new_24.xml | 13 + .../main/res/drawable/img_approvale2_20.xml | 18 + .../res/drawable/img_knight_shield_32.xml | 13 + .../connections/components/AlertsComponent.kt | 51 +++ .../connections/ui/AlertsModalBottomSheet.kt | 355 ++++++++++++++++++ 7 files changed, 466 insertions(+) create mode 100644 core/ui/src/main/res/drawable/ic_network_new_24.xml create mode 100644 core/ui/src/main/res/drawable/img_approvale2_20.xml create mode 100644 core/ui/src/main/res/drawable/img_knight_shield_32.xml create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 440dac2a29..7a0bd7ff8e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1236,6 +1236,21 @@ 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. + Unknown domain + Connect anyway + Timeout error. Please, try again later. + Failed to establish WalletConnect + This domain cannot be verified. Check the request carefully approving. + Go back to your browser and connect via WalletConnect again. + WalletConnect session was disconnected + Error code: %s. If the problem persists — feel free to contact our support. + We\'ve encountered unknown error + Tangem does not currently support a required network by %s. + Unsuported networks + Tangem support a required network by %s. + Verified domain + Wrong card or ring selected in Tangem App + We\'ve got some kind of problem Connections Disconnect all Text about discnected all dApps diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 7698a8067d..1d5172f597 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -95,6 +95,7 @@ data class TangemDimens internal constructor( val size142: Dp = 142.dp, val size158: Dp = 158.dp, val size164: Dp = 164.dp, + val size180: Dp = 180.dp, val size200: Dp = 200.dp, val size248: Dp = 248.dp, val size350: Dp = 350.dp, diff --git a/core/ui/src/main/res/drawable/ic_network_new_24.xml b/core/ui/src/main/res/drawable/ic_network_new_24.xml new file mode 100644 index 0000000000..2e2f3e36ed --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_network_new_24.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/img_approvale2_20.xml b/core/ui/src/main/res/drawable/img_approvale2_20.xml new file mode 100644 index 0000000000..d5e74adaf9 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_approvale2_20.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_knight_shield_32.xml b/core/ui/src/main/res/drawable/img_knight_shield_32.xml new file mode 100644 index 0000000000..31dd7900c0 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_knight_shield_32.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt new file mode 100644 index 0000000000..a03ae3a26d --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt @@ -0,0 +1,51 @@ +package com.tangem.features.walletconnect.connections.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import com.tangem.core.decompose.context.AppComponentContext +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.features.walletconnect.connections.ui.AlertsModalBottomSheet + +internal class AlertsComponent( + appComponentContext: AppComponentContext, + private val params: Params, +) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + + override fun dismiss() { + params.alertType.onDismiss() + } + + @Composable + override fun BottomSheet() { + AlertsModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = params.alertType.onDismiss, + content = params.alertType, + ), + ) + } + + internal data class Params(val alertType: AlertType) + + @Immutable + internal sealed class AlertType : TangemBottomSheetConfigContent { + abstract val onDismiss: () -> Unit + + data class VerifiedDomain(val appName: String, override val onDismiss: () -> Unit) : AlertType() + data class UnknownDomain(val onConnect: () -> Unit, override val onDismiss: () -> Unit) : AlertType() + data class UnsafeDomain(val onConnect: () -> Unit, override val onDismiss: () -> Unit) : AlertType() + data class UnsupportedNetworks(val appName: String, override val onDismiss: () -> Unit) : AlertType() + data class WcDisconnected(override val onDismiss: () -> Unit) : AlertType() + data class UnknownError(val errorCode: Int, override val onDismiss: () -> Unit) : AlertType() + data class WrongCardSelected(override val onDismiss: () -> Unit) : AlertType() + data class ConnectionTimeout(val onTryAgain: () -> Unit, override val onDismiss: () -> Unit) : AlertType() + data class MaliciousTransaction( + val descriptionMessage: String, + val onConnect: () -> Unit, + override val onDismiss: () -> Unit, + ) : AlertType() + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt new file mode 100644 index 0000000000..ae14e28008 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt @@ -0,0 +1,355 @@ +package com.tangem.features.walletconnect.connections.ui + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +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.datasource.CollectionPreviewParameterProvider +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.SecondaryButton +import com.tangem.core.ui.components.SecondaryButtonIconEnd +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.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.connections.components.AlertsComponent + +@Composable +internal fun AlertsModalBottomSheet(config: TangemBottomSheetConfig) { + TangemModalBottomSheet( + config = config, + title = { alert -> + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = alert.onDismiss, + ) + }, + content = { alert -> Content(alert = alert) }, + ) +} + +@Composable +private fun Content(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + ContentContainer( + modifier = Modifier + .heightIn(min = TangemTheme.dimens.size180) + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(bottom = 32.dp), + alert = alert, + ) + ButtonsContainer(modifier = Modifier.fillMaxWidth(), alert = alert) + } +} + +@Composable +private fun ContentContainer(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) { + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { + AlertIcon(alert = alert) + AlertContentTitle( + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing24), + alert = alert, + ) + AlertContentDescription( + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing8), + alert = alert, + ) + AlertAudit(alert, modifier = Modifier.padding(top = TangemTheme.dimens.spacing16)) + } +} + +@Suppress("LongMethod") +@Composable +private fun ButtonsContainer(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(all = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + val buttonsModifier = Modifier.fillMaxWidth() + when (alert) { + is AlertsComponent.AlertType.UnsafeDomain -> { + PrimaryButton( + modifier = buttonsModifier, + text = stringResourceSafe(R.string.common_cancel), + onClick = alert.onDismiss, + ) + SecondaryButton( + modifier = buttonsModifier, + onClick = alert.onConnect, + text = stringResourceSafe(R.string.wc_alert_connect_anyway), + ) + } + is AlertsComponent.AlertType.MaliciousTransaction -> { + PrimaryButton( + modifier = buttonsModifier, + text = stringResourceSafe(R.string.common_cancel), + onClick = alert.onDismiss, + ) + SecondaryButtonIconEnd( + modifier = buttonsModifier, + onClick = alert.onConnect, + iconResId = R.drawable.ic_tangem_24, + text = stringResourceSafe(R.string.wc_alert_connect_anyway), + ) + } + is AlertsComponent.AlertType.UnknownDomain -> { + PrimaryButton( + modifier = buttonsModifier, + text = stringResourceSafe(R.string.common_cancel), + onClick = alert.onDismiss, + ) + SecondaryButton( + modifier = buttonsModifier, + onClick = alert.onConnect, + text = stringResourceSafe(R.string.wc_alert_connect_anyway), + ) + } + is AlertsComponent.AlertType.VerifiedDomain -> SecondaryButton( + modifier = buttonsModifier, + text = stringResourceSafe(R.string.common_done), + onClick = alert.onDismiss, + ) + is AlertsComponent.AlertType.ConnectionTimeout -> { + PrimaryButton( + modifier = buttonsModifier, + onClick = alert.onTryAgain, + text = stringResourceSafe(R.string.alert_button_try_again), + ) + SecondaryButton( + modifier = buttonsModifier, + onClick = alert.onDismiss, + text = stringResourceSafe(R.string.balance_hidden_got_it_button), + ) + } + is AlertsComponent.AlertType.WrongCardSelected, + is AlertsComponent.AlertType.UnknownError, + -> SecondaryButton( + modifier = buttonsModifier, + onClick = alert.onDismiss, + text = stringResourceSafe(R.string.balance_hidden_got_it_button), + ) + is AlertsComponent.AlertType.WcDisconnected, + is AlertsComponent.AlertType.UnsupportedNetworks, + -> PrimaryButton( + modifier = buttonsModifier, + onClick = alert.onDismiss, + text = stringResourceSafe(R.string.balance_hidden_got_it_button), + ) + } + } +} + +@Composable +private fun AlertIcon(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) { + val color = when (alert) { + is AlertsComponent.AlertType.VerifiedDomain -> TangemTheme.colors.icon.accent + is AlertsComponent.AlertType.WcDisconnected, + is AlertsComponent.AlertType.ConnectionTimeout, + is AlertsComponent.AlertType.UnsupportedNetworks, + -> TangemTheme.colors.icon.informative + is AlertsComponent.AlertType.WrongCardSelected, + is AlertsComponent.AlertType.UnknownDomain, + is AlertsComponent.AlertType.UnknownError, + -> TangemTheme.colors.icon.attention + is AlertsComponent.AlertType.MaliciousTransaction, + is AlertsComponent.AlertType.UnsafeDomain, + -> TangemTheme.colors.icon.warning + } + + @DrawableRes val drawableId = when (alert) { + is AlertsComponent.AlertType.WcDisconnected, + is AlertsComponent.AlertType.ConnectionTimeout, + -> R.drawable.ic_wallet_connect_24 + is AlertsComponent.AlertType.MaliciousTransaction -> R.drawable.img_knight_shield_32 + is AlertsComponent.AlertType.UnknownDomain, + is AlertsComponent.AlertType.UnsafeDomain, + -> R.drawable.img_knight_shield_32 + is AlertsComponent.AlertType.UnsupportedNetworks -> R.drawable.ic_network_new_24 + is AlertsComponent.AlertType.WrongCardSelected, + is AlertsComponent.AlertType.UnknownError, + -> R.drawable.img_attention_20 + is AlertsComponent.AlertType.VerifiedDomain -> R.drawable.img_approvale2_20 + } + val iconTint = when (alert) { + is AlertsComponent.AlertType.UnknownDomain, + is AlertsComponent.AlertType.UnsafeDomain, + is AlertsComponent.AlertType.MaliciousTransaction, + is AlertsComponent.AlertType.UnsupportedNetworks, + is AlertsComponent.AlertType.ConnectionTimeout, + is AlertsComponent.AlertType.WcDisconnected, + -> color + is AlertsComponent.AlertType.VerifiedDomain, + is AlertsComponent.AlertType.UnknownError, + is AlertsComponent.AlertType.WrongCardSelected, + -> Color.Unspecified + } + Box( + modifier = modifier + .size(TangemTheme.dimens.size56) + .clip(CircleShape) + .background(color.copy(alpha = 0.1F)), + contentAlignment = Alignment.Center, + content = { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size32), + painter = painterResource(drawableId), + contentDescription = null, + tint = iconTint, + ) + }, + ) +} + +@Composable +private fun AlertContentTitle(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) { + @StringRes val titleRes: Int = when (alert) { + is AlertsComponent.AlertType.UnknownDomain, + is AlertsComponent.AlertType.UnsafeDomain, + is AlertsComponent.AlertType.MaliciousTransaction, + -> R.string.security_alert_title + is AlertsComponent.AlertType.ConnectionTimeout -> R.string.wc_alert_connection_timeout_title + is AlertsComponent.AlertType.UnknownError -> R.string.wc_alert_unknown_error_title + is AlertsComponent.AlertType.UnsupportedNetworks -> R.string.wc_alert_unsupported_networks_title + is AlertsComponent.AlertType.VerifiedDomain -> R.string.wc_alert_verified_domain_title + is AlertsComponent.AlertType.WcDisconnected -> R.string.wc_alert_session_disconnected_title + is AlertsComponent.AlertType.WrongCardSelected -> R.string.wc_alert_wrong_card_title + } + Text( + modifier = modifier, + text = stringResourceSafe(titleRes), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun AlertContentDescription(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) { + val message = when (alert) { + is AlertsComponent.AlertType.ConnectionTimeout -> stringResourceSafe( + R.string.wc_alert_connection_timeout_description, + ) + is AlertsComponent.AlertType.MaliciousTransaction -> alert.descriptionMessage + is AlertsComponent.AlertType.UnknownDomain, + is AlertsComponent.AlertType.UnsafeDomain, + -> stringResourceSafe(R.string.wc_alert_domain_issues_description) + is AlertsComponent.AlertType.UnknownError -> stringResourceSafe( + R.string.wc_alert_unknown_error_description, + alert.errorCode, + ) + is AlertsComponent.AlertType.UnsupportedNetworks -> stringResourceSafe( + R.string.wc_alert_unsupported_networks_description, + alert.appName, + ) + is AlertsComponent.AlertType.VerifiedDomain -> stringResourceSafe( + R.string.wc_alert_verified_domain_description, + alert.appName, + ) + is AlertsComponent.AlertType.WcDisconnected -> stringResourceSafe( + R.string.wc_alert_session_disconnected_description, + ) + is AlertsComponent.AlertType.WrongCardSelected -> stringResourceSafe(R.string.wc_alert_wrong_card_description) + } + Text( + modifier = modifier, + text = message, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun AlertAudit(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) { + when (alert) { + is AlertsComponent.AlertType.UnknownDomain -> Text( + modifier = modifier + .background( + shape = RoundedCornerShape(TangemTheme.dimens.radius16), + color = TangemTheme.colors.text.primary1.copy(alpha = 0.1F), + ) + .padding(vertical = TangemTheme.dimens.spacing4, horizontal = TangemTheme.dimens.spacing12), + text = stringResourceSafe(R.string.wc_alert_audit_unknown_domain), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + is AlertsComponent.AlertType.UnsafeDomain -> Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16) + .background( + shape = RoundedCornerShape(TangemTheme.dimens.radius16), + color = TangemTheme.colors.text.warning.copy(alpha = 0.1F), + ) + .padding(vertical = TangemTheme.dimens.spacing4, horizontal = TangemTheme.dimens.spacing12), + text = stringResourceSafe(R.string.wc_alert_audit_unknown_domain), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.warning, + ) + is AlertsComponent.AlertType.ConnectionTimeout, + is AlertsComponent.AlertType.MaliciousTransaction, + is AlertsComponent.AlertType.UnknownError, + is AlertsComponent.AlertType.UnsupportedNetworks, + is AlertsComponent.AlertType.VerifiedDomain, + is AlertsComponent.AlertType.WcDisconnected, + is AlertsComponent.AlertType.WrongCardSelected, + -> Unit + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AlertsModalBottomSheet_Preview( + @PreviewParameter(AlertTypesProvider::class) alert: AlertsComponent.AlertType, +) { + TangemThemePreview { + AlertsModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = alert, + ), + ) + } +} + +private class AlertTypesProvider : CollectionPreviewParameterProvider( + collection = listOf( + AlertsComponent.AlertType.VerifiedDomain(appName = "React App", onDismiss = {}), + AlertsComponent.AlertType.UnknownDomain(onConnect = {}, onDismiss = {}), + AlertsComponent.AlertType.UnsafeDomain(onConnect = {}, onDismiss = {}), + AlertsComponent.AlertType.MaliciousTransaction( + descriptionMessage = "The transaction approves erc20 tokens to a known malicious address", + onConnect = {}, + onDismiss = {}, + ), + AlertsComponent.AlertType.UnsupportedNetworks(appName = "React App", onDismiss = {}), + AlertsComponent.AlertType.WcDisconnected(onDismiss = {}), + AlertsComponent.AlertType.UnknownError(errorCode = 8005, onDismiss = {}), + AlertsComponent.AlertType.WrongCardSelected(onDismiss = {}), + AlertsComponent.AlertType.ConnectionTimeout(onTryAgain = {}, onDismiss = {}), + ), +) \ No newline at end of file From 27686aad7c8694aea7e537a810d0806649c866f2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Apr 2025 07:26:36 +0000 Subject: [PATCH 54/72] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index c6bc2129f5..a32c8679a0 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.23-1026" +tangemBlockchainSdk = "develop-1027" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-456" +tangemCardSdk = "develop-455" #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 ^ @@ -21,4 +21,4 @@ card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tange vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } -vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } +vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } \ No newline at end of file From 155ade9512e090495d7b6015e12c8114d774c9ac Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Apr 2025 07:30:49 +0000 Subject: [PATCH 55/72] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 42f5de2a7f..a32c8679a0 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.23-1032" +tangemBlockchainSdk = "develop-1027" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-456" +tangemCardSdk = "develop-455" #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 ^ @@ -21,4 +21,4 @@ card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tange vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } -vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } +vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } \ No newline at end of file From 681849abad1f6cd19ef955e3e8b5ab79ace888c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Apr 2025 10:42:13 +0300 Subject: [PATCH 56/72] 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 a32c8679a0..7698d4b61b 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-1027" +tangemBlockchainSdk = "develop-1033" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From b48bb8236c5fa4c740e774d3a9ee9d27a6fb2460 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Apr 2025 11:03:35 +0300 Subject: [PATCH 57/72] Updated on 2026-08-14 --- .../send/v2/send/DefaultSendComponent.kt | 16 +++++++++++++++- .../amount/model/SendAmountModel.kt | 1 - 2 files changed, 15 insertions(+), 2 deletions(-) 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 1f12ff1fdd..645441fe6e 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 @@ -24,6 +24,7 @@ import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.ui.SendContent import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.send.confirm.SendConfirmComponent import com.tangem.features.send.v2.send.model.SendModel import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent @@ -161,7 +162,20 @@ internal class DefaultSendComponent @AssistedInject constructor( cryptoCurrencyStatus = model.cryptoCurrencyStatus, callback = model, predefinedValues = model.predefinedValues, - onBackClick = ::onChildBack, + onBackClick = { + if (route.isEditMode) { + onChildBack() + } else { + analyticsEventHandler.send( + SendAnalyticEvents.CloseButtonClicked( + source = SendScreenSource.Amount, + isFromSummary = false, + isValid = model.uiState.value.amountUM.isPrimaryButtonEnabled, + ), + ) + router.pop() + } + }, onNextClick = { if (route.isEditMode) { onChildBack() 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 60a02b2bcb..7e82ea275e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -1,7 +1,6 @@ package com.tangem.features.send.v2.subcomponents.amount.model import androidx.compose.runtime.Stable -import com.tangem.common.routing.AppRouter import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.converters.* import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer From f4bb2bbc2dad8c0456aaa1994dd300a045cdf8b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 14:22:23 +0500 Subject: [PATCH 58/72] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 17 +++ .../walletconnect/WalletConnectSdkHelper.kt | 5 +- .../tap/proxy/TransactionManagerImpl.kt | 25 ++-- .../DefaultTransactionRepository.kt | 116 ++++++++---------- .../domain/demo/DemoTransactionSender.kt | 9 +- .../DefaultWalletManagersFacade.kt | 12 ++ .../transaction/TransactionRepository.kt | 20 +-- .../CreateApprovalTransactionUseCase.kt | 5 +- .../CreateTransactionDataExtrasUseCase.kt | 31 ++--- .../usecase/CreateTransactionUseCase.kt | 8 +- .../CreateTransferTransactionUseCase.kt | 41 +++++++ .../transaction/usecase/GetFeeUseCase.kt | 3 + .../usecase/GetTransferFeeUseCase.kt | 102 +++++++++++++++ .../usecase/ValidateTransactionUseCase.kt | 8 +- .../send/impl/presentation/model/SendModel.kt | 8 +- .../feature/swap/DefaultSwapRepository.kt | 41 ------- .../feature/swap/domain/api/SwapRepository.kt | 11 -- .../swap/domain/models/ui/SwapState.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 93 +++++--------- gradle/tangem_dependencies.toml | 2 +- .../tangem/lib/crypto/TransactionManager.kt | 5 +- 21 files changed, 327 insertions(+), 236 deletions(-) create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransferTransactionUseCase.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetTransferFeeUseCase.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 55d35c9244..3e76a02638 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 @@ -30,6 +30,15 @@ internal object TransactionDomainModule { ) } + @Provides + @Singleton + fun provideTransferGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetTransferFeeUseCase { + return GetTransferFeeUseCase( + walletManagersFacade = walletManagersFacade, + demoConfig = DemoConfig(), + ) + } + @Provides @Singleton fun provideSendTransactionUseCase( @@ -148,6 +157,14 @@ internal object TransactionDomainModule { return GetAllowanceUseCase(transactionRepository) } + @Provides + @Singleton + fun provideCreateTransferTransactionUseCase( + transactionRepository: TransactionRepository, + ): CreateTransferTransactionUseCase { + return CreateTransferTransactionUseCase(transactionRepository) + } + @Provides @Singleton fun providePrepareForSendUseCase( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index cc73660628..ea5b87285c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumUtils import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.* import com.tangem.blockchainsdk.utils.fromNetworkId @@ -101,7 +102,7 @@ class WalletConnectSdkHelper { sourceAddress = transaction.from, destinationAddress = destinationAddress, extras = EthereumTransactionExtras( - data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(), + callData = CompiledSmartContractCallData(transaction.data.removePrefix(HEX_PREFIX).hexToBytes()), gasLimit = gasLimit.toBigInteger(), nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger(), ), @@ -191,7 +192,7 @@ class WalletConnectSdkHelper { val gasLimitResult = (walletManager as? EthereumGasLoader)?.getGasLimit( amount = Amount(value, walletManager.wallet.blockchain), destination = transaction.to ?: "", - data = transaction.data, + callData = CompiledSmartContractCallData(transaction.data.hexToBytes()), ) return when (gasLimitResult) { is Result.Success -> gasLimitResult.data.toBigDecimal().multiply(BigDecimal("1.2")) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 621ce6f4e9..396fc681f2 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -3,6 +3,7 @@ package com.tangem.tap.proxy import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.smartcontract.SmartContractCallData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result @@ -43,7 +44,7 @@ class TransactionManagerImpl( currencyToSend: Currency, destinationAddress: String, increaseBy: Int?, - data: String?, + callData: SmartContractCallData?, derivationPath: String?, ): ProxyFees { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } @@ -54,7 +55,7 @@ class TransactionManagerImpl( walletManager = walletManager, amount = amountToSend, destinationAddress = destinationAddress, - data = data, + callData = callData, ) } return getFeeForEthereumBlockchain( @@ -62,7 +63,7 @@ class TransactionManagerImpl( blockchain = blockchain, amountToSend = amountToSend, destinationAddress = destinationAddress, - data = data, + callData = callData, increaseBy = increaseBy, ) } else { @@ -160,14 +161,14 @@ class TransactionManagerImpl( blockchain: Blockchain, amountToSend: Amount, destinationAddress: String, - data: String?, + callData: SmartContractCallData?, increaseBy: Int?, ): ProxyFees { val gasLimit = getGasLimit( evmWalletManager = walletManager, amount = amountToSend, destinationAddress = destinationAddress, - data = data, + callData = callData, ).increaseBigIntegerByPercents(increaseBy) return when (val gasPrice = walletManager.getGasPrice()) { is Result.Success -> { @@ -183,16 +184,16 @@ class TransactionManagerImpl( walletManager: EthereumOptimisticRollupWalletManager, amount: Amount, destinationAddress: String, - data: String?, + callData: SmartContractCallData?, ): ProxyFees { - val fee = if (data.isNullOrEmpty()) { + val fee = if (callData == null) { walletManager.getFee(amount, destinationAddress) } else { - walletManager.getFee(amount, destinationAddress, data) + walletManager.getFee(amount, destinationAddress, callData) } return when (fee) { is Result.Success -> { - val choosableFee = fee.data + val choosableFee = fee.data as? TransactionFee.Choosable ?: error("Incorrect fee type") val minProxyFee = ProxyFee.Common( gasLimit = (choosableFee.minimum as Fee.Ethereum).gasLimit, @@ -223,9 +224,9 @@ class TransactionManagerImpl( evmWalletManager: EthereumWalletManager, amount: Amount, destinationAddress: String, - data: String?, + callData: SmartContractCallData?, ): BigInteger { - val result = if (data.isNullOrEmpty()) { + val result = if (callData == null) { evmWalletManager.getGasLimit( amount = amount, destination = destinationAddress, @@ -234,7 +235,7 @@ class TransactionManagerImpl( evmWalletManager.getGasLimit( amount = amount, destination = destinationAddress, - data = data, + callData = callData, ) } when (result) { 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 6d3ddca431..da6951cafd 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 @@ -14,14 +14,14 @@ import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.tron.TronTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.smartcontract.SmartContractCallData +import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.common.extensions.hexToBytes import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository -import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -29,7 +29,6 @@ import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal import java.math.BigInteger -import com.tangem.blockchain.blockchains.tron.TransactionType as SdkTransactionType internal class DefaultTransactionRepository( private val walletManagersFacade: WalletManagersFacade, @@ -45,7 +44,6 @@ internal class DefaultTransactionRepository( userWalletId: UserWalletId, network: Network, txExtras: TransactionExtras?, - hash: String?, ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { val blockchain = Blockchain.fromId(network.id.value) val walletManager = walletManagersFacade.getOrCreateWalletManager( @@ -54,14 +52,50 @@ internal class DefaultTransactionRepository( derivationPath = network.derivationPath.value, ) ?: error("Wallet manager not found") - return@withContext walletManager.createTransactionDataInternal( + return@withContext walletManager.createTransaction( amount = amount, fee = fee, - memo = memo, destination = destination, + ).copy( + extras = txExtras ?: getMemoExtras(networkId = network.id.value, memo), + ) + } + + override suspend fun createTransferTransaction( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + + val callData = SmartContractCallDataProviderFactory.getTokenTransferCallData( + destinationAddress = destination, + amount = amount, + blockchain = blockchain, + ) + + val extras = if (amount.type is AmountType.Token && callData != null) { + createTransactionDataExtras( + callData = callData, + network = network, + nonce = null, + gasLimit = null, + ) + } else { + null + } + + return@withContext createTransaction( + amount = amount, + fee = fee, + memo = null, + destination = destination, + userWalletId = userWalletId, network = network, - txExtras = txExtras, - hash = hash, + txExtras = getMemoExtras(networkId = network.id.value, memo = memo) ?: extras, ) } @@ -73,25 +107,16 @@ internal class DefaultTransactionRepository( spenderAddress: String, userWalletId: UserWalletId, network: Network, - hash: String?, ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { val blockchain = Blockchain.fromId(network.id.value) - val walletManager = walletManagersFacade.getOrCreateWalletManager( - userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, - ) ?: error("Wallet manager not found") - val approver = walletManager as? Approver ?: error("Cannot cast to Approver") - - val approvalData = approver.getApproveData( - spenderAddress = spenderAddress, - value = approvalAmount, - ) val extras = createTransactionDataExtras( - data = approvalData, + callData = SmartContractCallDataProviderFactory.getApprovalCallData( + spenderAddress = spenderAddress, + amount = approvalAmount, + blockchain = blockchain, + ), network = network, - transactionType = TransactionType.APPROVE, nonce = null, gasLimit = null, ) @@ -104,7 +129,6 @@ internal class DefaultTransactionRepository( userWalletId = userWalletId, network = network, txExtras = extras, - hash = hash, ) } @@ -115,9 +139,6 @@ internal class DefaultTransactionRepository( destination: String, userWalletId: UserWalletId, network: Network, - isSwap: Boolean, - txExtras: TransactionExtras?, - hash: String?, ): Result = withContext(coroutineDispatcherProvider.io) { val blockchain = Blockchain.fromId(network.id.value) val walletManager = walletManagersStore.getSyncOrNull( @@ -129,14 +150,12 @@ internal class DefaultTransactionRepository( val validator = walletManager as? TransactionValidator if (validator != null) { - val transactionData = walletManager.createTransactionDataInternal( + val transactionData = walletManager.createTransaction( amount = amount, fee = fee ?: Fee.Common(amount = amount), - memo = memo, destination = destination, - network = network, - txExtras = txExtras, - hash = hash, + ).copy( + extras = getMemoExtras(networkId = network.id.value, memo = memo), ) validator.validate(transactionData = transactionData) @@ -178,9 +197,8 @@ internal class DefaultTransactionRepository( } override fun createTransactionDataExtras( - data: String, + callData: SmartContractCallData, network: Network, - transactionType: TransactionType, nonce: BigInteger?, gasLimit: BigInteger?, ): TransactionExtras { @@ -189,15 +207,14 @@ internal class DefaultTransactionRepository( return when { blockchain.isEvm() -> { EthereumTransactionExtras( - data = data.hexToBytes(), + callData = callData, gasLimit = gasLimit, nonce = nonce, ) } blockchain == Blockchain.Tron -> { TronTransactionExtras( - data = data.hexToBytes(), - txType = convertToSdkTransactionType(transactionType), + callData = callData, ) } else -> error("Data extras not supported for $blockchain") @@ -226,33 +243,6 @@ internal class DefaultTransactionRepository( ) } - private fun convertToSdkTransactionType(transactionType: TransactionType): SdkTransactionType { - return when (transactionType) { - TransactionType.APPROVE -> SdkTransactionType.APPROVE - } - } - - @Suppress("LongParameterList") - private fun WalletManager.createTransactionDataInternal( - amount: Amount, - fee: Fee, - memo: String?, - destination: String, - network: Network, - txExtras: TransactionExtras?, - hash: String?, - ): TransactionData.Uncompiled { - if (txExtras != null && memo != null) { - // throw error for now to avoid programmers errors when use extras - error("Both txExtras and memo provided, use only one of them") - } - val extras = txExtras ?: getMemoExtras(network.id.value, memo) - return createTransaction(amount, fee, destination).copy( - hash = hash, - extras = extras, - ) - } - @Suppress("CyclomaticComplexMethod") private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { val blockchain = Blockchain.fromId(networkId) diff --git a/domain/demo/src/main/java/com/tangem/domain/demo/DemoTransactionSender.kt b/domain/demo/src/main/java/com/tangem/domain/demo/DemoTransactionSender.kt index 5c667f1f53..8ed8931539 100644 --- a/domain/demo/src/main/java/com/tangem/domain/demo/DemoTransactionSender.kt +++ b/domain/demo/src/main/java/com/tangem/domain/demo/DemoTransactionSender.kt @@ -1,6 +1,7 @@ package com.tangem.domain.demo import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.smartcontract.SmartContractCallData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.transaction.TransactionSendResult @@ -24,8 +25,12 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact ) } - override suspend fun estimateFee(amount: Amount, destination: String): Result { - return getFee(amount, walletManager.wallet.address) + override suspend fun estimateFee( + amount: Amount, + destination: String, + callData: SmartContractCallData?, + ): Result { + return getFee(amount, walletManager.wallet.address, callData) } override suspend fun send( diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index d15b9f68bd..1e16b212a1 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -10,6 +10,7 @@ import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchain.common.address.EstimationFeeAddressFactory import com.tangem.blockchain.common.pagination.Page +import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.trustlines.AssetRequirementsManager @@ -488,9 +489,20 @@ class DefaultWalletManagersFacade( val destination = estimationFeeAddressFactory.makeAddress(blockchain) + val callData = if (amount.type is AmountType.Token) { + SmartContractCallDataProviderFactory.getTokenTransferCallData( + destinationAddress = destination, + amount = amount, + blockchain = blockchain, + ) + } else { + null + } + (walletManager as? TransactionSender)?.estimateFee( amount = amount, destination = destination, + callData = callData, ) } 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 892fa06f78..9ef573a666 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 @@ -1,12 +1,12 @@ package com.tangem.domain.transaction import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.smartcontract.SmartContractCallData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.blockchain.common.transaction.TransactionsSendResult import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network -import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.wallets.models.UserWalletId import java.math.BigDecimal import java.math.BigInteger @@ -22,7 +22,16 @@ interface TransactionRepository { userWalletId: UserWalletId, network: Network, txExtras: TransactionExtras?, - hash: String?, + ): TransactionData.Uncompiled + + @Suppress("LongParameterList") + suspend fun createTransferTransaction( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, ): TransactionData.Uncompiled @Suppress("LongParameterList") @@ -34,7 +43,6 @@ interface TransactionRepository { spenderAddress: String, userWalletId: UserWalletId, network: Network, - hash: String?, ): TransactionData.Uncompiled @Suppress("LongParameterList") @@ -45,9 +53,6 @@ interface TransactionRepository { destination: String, userWalletId: UserWalletId, network: Network, - isSwap: Boolean = false, - txExtras: TransactionExtras?, - hash: String? = null, ): Result suspend fun sendTransaction( @@ -66,9 +71,8 @@ interface TransactionRepository { ): com.tangem.blockchain.extensions.Result fun createTransactionDataExtras( - data: String, + callData: SmartContractCallData, network: Network, - transactionType: TransactionType, nonce: BigInteger?, gasLimit: BigInteger?, ): TransactionExtras diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt index 65d69a9d0a..d3b136ff3e 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt @@ -8,6 +8,9 @@ import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWalletId import java.math.BigDecimal +/** + * Use case to create and get approval transaction + */ class CreateApprovalTransactionUseCase( private val transactionRepository: TransactionRepository, ) { @@ -20,7 +23,6 @@ class CreateApprovalTransactionUseCase( fee: Fee, contractAddress: String, spenderAddress: String, - hash: String? = null, ) = Either.catch { transactionRepository.createApprovalTransaction( amount = BigDecimal.ZERO.convertToSdkAmount(cryptoCurrency), @@ -30,7 +32,6 @@ class CreateApprovalTransactionUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, fee = fee, - hash = hash, ) } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt index 52d686e403..c63384ba5e 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt @@ -1,30 +1,25 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either +import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData +import com.tangem.common.extensions.hexToBytes import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository -import com.tangem.domain.transaction.models.TransactionType import java.math.BigInteger class CreateTransactionDataExtrasUseCase( private val transactionRepository: TransactionRepository, ) { - operator fun invoke( - data: String, - network: Network, - transactionType: TransactionType, - gasLimit: BigInteger? = null, - nonce: BigInteger? = null, - ) = Either.catch { - requireNotNull( - transactionRepository.createTransactionDataExtras( - data = data, - network = network, - transactionType = transactionType, - nonce = nonce, - gasLimit = gasLimit, - ), - ) { "Failed to create transaction" } - } + operator fun invoke(data: String, network: Network, gasLimit: BigInteger? = null, nonce: BigInteger? = null) = + Either.catch { + requireNotNull( + transactionRepository.createTransactionDataExtras( + callData = CompiledSmartContractCallData(data.hexToBytes()), + network = network, + nonce = nonce, + gasLimit = gasLimit, + ), + ) { "Failed to create transaction" } + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt index 87f10ea630..a9d080eb2e 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt @@ -8,6 +8,12 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.wallets.models.UserWalletId +/** + * Use case to create and get transaction + * + * !!!IMPORTANT + * Use when transaction data is already compiled by external service or provider + */ class CreateTransactionUseCase( private val transactionRepository: TransactionRepository, ) { @@ -24,7 +30,6 @@ class CreateTransactionUseCase( userWalletId: UserWalletId, network: Network, txExtras: TransactionExtras? = null, - hash: String? = null, ) = Either.catch { transactionRepository.createTransaction( amount = amount, @@ -34,7 +39,6 @@ class CreateTransactionUseCase( userWalletId = userWalletId, network = network, txExtras = txExtras, - hash = hash, ) } } \ No newline at end of file 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 new file mode 100644 index 0000000000..64457a93b8 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransferTransactionUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case to create and get transfer transaction + * + * !!!IMPORTANT + * Use when transaction data is compiled by us using BlockchainSDK methods + */ +class CreateTransferTransactionUseCase( + private val transactionRepository: TransactionRepository, +) { + + /** + * [REDACTED_TODO_COMMENT] + */ + @Suppress("LongParameterList") + suspend operator fun invoke( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ) = Either.catch { + transactionRepository.createTransferTransaction( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + userWalletId = userWalletId, + network = network, + ) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt index 548a43ab6c..c370ac644d 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt @@ -17,6 +17,9 @@ import java.math.BigDecimal /** * Use case to get transaction fee + * + * !!!IMPORTANT!!! + * Use when transaction data is already compiled by external service or provider */ class GetFeeUseCase( private val walletManagersFacade: WalletManagersFacade, diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetTransferFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetTransferFeeUseCase.kt new file mode 100644 index 0000000000..0a1582967d --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetTransferFeeUseCase.kt @@ -0,0 +1,102 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.demo.DemoTransactionSender +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.error.mapToFeeError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import java.math.BigDecimal + +/** + * Use case to get transfer transaction fee + * + * !!!IMPORTANT + * Use when transaction data is compiled by us using BlockchainSDK methods + */ +class GetTransferFeeUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val demoConfig: DemoConfig, +) { + suspend operator fun invoke( + amount: BigDecimal, + destination: String, + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + ) = either { + catch( + block = { + val amountData = convertCryptoCurrencyToAmount(cryptoCurrency, amount) + + val result = if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { + demoTransactionSender(userWallet, cryptoCurrency).getFee( + amount = amountData, + destination = destination, + ) + } else { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + val smartContractCallData = if (amountData.type is AmountType.Token) { + SmartContractCallDataProviderFactory.getTokenTransferCallData( + amount = amountData, + destinationAddress = destination, + blockchain = Blockchain.fromId(cryptoCurrency.network.id.value), + ) + } else { + null + } + + (walletManager as? TransactionSender)?.getFee( + amount = amountData, + destination = destination, + callData = smartContractCallData, + ) ?: error("Fee is null") + } + + val maybeFee = when (result) { + is Result.Success -> result.data + is Result.Failure -> raise(result.mapToFeeError()) + } + maybeFee + }, + catch = { + raise(GetFeeError.DataError(it)) + }, + ) + } + + private suspend fun demoTransactionSender( + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + ): DemoTransactionSender { + return DemoTransactionSender( + walletManagersFacade + .getOrCreateWalletManager(userWallet.walletId, cryptoCurrency.network) + ?: error("WalletManager is null"), + ) + } + + private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = amount, + decimals = cryptoCurrency.decimals, + type = when (cryptoCurrency) { + is CryptoCurrency.Coin -> AmountType.Coin + is CryptoCurrency.Token -> AmountType.Token( + token = Token( + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) + }, + ) +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt index 61b2f26817..11b429be0f 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt @@ -21,8 +21,6 @@ class ValidateTransactionUseCase( destination: String, userWalletId: UserWalletId, network: Network, - isSwap: Boolean = false, - hash: String? = null, ): Either { return transactionRepository.validateTransaction( amount = amount, @@ -31,10 +29,6 @@ class ValidateTransactionUseCase( destination = destination, userWalletId = userWalletId, network = network, - isSwap = isSwap, - txExtras = null, - hash = hash, - ) - .fold(onSuccess = { Unit.right() }, onFailure = { it.left() }) + ).fold(onSuccess = { Unit.right() }, onFailure = { it.left() }) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt index 9f4e696e39..fbcd15cc1f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt @@ -92,9 +92,9 @@ internal class SendModel @Inject constructor( private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, - private val getFeeUseCase: GetFeeUseCase, + private val getTransferFeeUseCase: GetTransferFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, - private val createTransactionUseCase: CreateTransactionUseCase, + private val createTransferTransactionUseCase: CreateTransferTransactionUseCase, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -826,7 +826,7 @@ internal class SendModel @Inject constructor( val recipientState = uiState.value.getRecipientState(isFromConfirmation) ?: return null val amount = amountState.amountTextField.cryptoAmount.value ?: return null - return getFeeUseCase.invoke( + return getTransferFeeUseCase.invoke( amount = amount, destination = recipientState.addressTextField.value, userWallet = userWallet, @@ -929,7 +929,7 @@ internal class SendModel @Inject constructor( ) modelScope.launch { - createTransactionUseCase( + createTransferTransactionUseCase( amount = receivingAmount.convertToSdkAmount(cryptoCurrency), fee = fee, memo = memo, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 8e465b9c2b..f872cd9c2c 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -344,47 +344,6 @@ internal class DefaultSwapRepository( ) } - override suspend fun getApproveData( - userWalletId: UserWalletId, - networkId: String, - derivationPath: String?, - currency: CryptoCurrency, - amount: BigDecimal?, - spenderAddress: String, - ): String { - val blockchain = - requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = walletManagersFacade.getOrCreateWalletManager( - userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = derivationPath, - ) - - return (walletManager as? Approver)?.getApproveData( - spenderAddress, - amount?.let { convertToAmount(it, currency) }, - ) ?: error("Cannot cast to Approver") - } - - private fun convertToAmount(amount: BigDecimal, currency: CryptoCurrency): Amount { - return Amount( - currencySymbol = currency.symbol, - value = amount, - decimals = currency.decimals, - type = if (currency is CryptoCurrency.Token) { - AmountType.Token( - Token( - symbol = currency.symbol, - contractAddress = currency.contractAddress, - decimals = currency.decimals, - ), - ) - } else { - AmountType.Coin - }, - ) - } - override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } diff --git a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index 46ebd1d768..bacf1b8df5 100644 --- a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -41,17 +41,6 @@ interface SwapRepository { spenderAddress: String, ): BigDecimal - @Suppress("LongParameterList") - @Throws(IllegalStateException::class) - suspend fun getApproveData( - userWalletId: UserWalletId, - networkId: String, - derivationPath: String?, - currency: CryptoCurrency, - amount: BigDecimal?, - spenderAddress: String, - ): String - @Suppress("LongParameterList") suspend fun getExchangeData( fromContractAddress: String, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index a4336b8eae..f6aa62d8cd 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -82,7 +82,6 @@ data class TokenSwapInfo( data class RequestApproveStateData( val fee: TxFeeState, - val approveData: String, val fromTokenAmount: SwapAmount, val spenderAddress: 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 041739a131..d5b93c4276 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 @@ -7,10 +7,14 @@ import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain.* import com.tangem.blockchain.common.TransactionExtras +import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData +import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.common.extensions.hexToBytes +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository @@ -25,7 +29,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.transaction.usecase.* import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet @@ -61,7 +64,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, + private val createTransferTransactionUseCase: CreateTransferTransactionUseCase, private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase, + private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val quotesRepository: QuotesRepository, private val quotesRepositoryV2: QuotesRepositoryV2, @@ -202,32 +207,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( networkId: String, permissionOptions: PermissionOptions, ): SwapTransactionState { - val derivationPath = permissionOptions.fromToken.network.derivationPath.value - val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { - getApproveData( - networkId = networkId, - derivationPath = derivationPath, - fromToken = permissionOptions.fromToken, - spenderAddress = permissionOptions.spenderAddress, - ) - } else { - permissionOptions.approveData.approveData + val amount = permissionOptions.approveData.fromTokenAmount.takeIf { + permissionOptions.approveType == SwapApproveType.LIMITED } - val approveTransaction = createTransactionUseCase( - amount = BigDecimal.ZERO.convertToSdkAmount(permissionOptions.fromToken), + + val approveTransaction = createApprovalTransactionUseCase( fee = getFeeForTransaction( fee = permissionOptions.txFee, blockchain = Blockchain.fromId(permissionOptions.fromToken.network.id.value), ), - memo = null, - destination = getTokenAddress(permissionOptions.fromToken), - network = permissionOptions.fromToken.network, userWalletId = userWalletId, - txExtras = createDexTxExtras( - dataToSign, - permissionOptions.fromToken.network, - permissionOptions.txFee.gasLimit, - ), + cryptoCurrency = permissionOptions.fromToken as CryptoCurrency.Token, + amount = amount?.value, + contractAddress = permissionOptions.forTokenContractAddress, + spenderAddress = permissionOptions.spenderAddress, ).getOrElse { Timber.e(it, "Failed to create approveTransaction") return SwapTransactionState.Error.UnknownError @@ -613,7 +606,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( userWalletId = userWalletId, network = currencyToSendStatus.currency.network, txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, fee.gasLimit), - hash = dataToSign, ).getOrElse { Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.Error.UnknownError @@ -669,7 +661,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( return createTransactionExtrasUseCase( data = data, network = network, - transactionType = TransactionType.APPROVE, gasLimit = gasLimit?.toBigInteger(), ).getOrNull() ?: error("failed to create extras") } @@ -705,7 +696,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (isDemoCardUseCase(cardId)) return SwapTransactionState.Error.UnknownError - val txData = createTransactionUseCase( + val txData = createTransferTransactionUseCase( amount = amount.value.convertToSdkAmount(currencyToSend.currency), fee = getFeeForTransaction( fee = txFee, @@ -1087,11 +1078,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun createEmptyAmountState(): SwapState { val appCurrency = getSelectedAppCurrencyUseCase.unwrap() return SwapState.EmptyAmountState( - zeroAmountEquivalent = BigDecimalFormatter.formatFiatAmount( - fiatAmount = BigDecimal.ZERO, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), + zeroAmountEquivalent = BigDecimal.ZERO.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, ) } @@ -1373,11 +1365,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( val rates = getQuotes(feeCurrencyId) return rates[feeCurrencyId]?.fiatRate?.let { rate -> fees.map { fee -> - BigDecimalFormatter.formatFiatAmount( - fiatAmount = rate.multiply(fee), - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + rate.multiply(fee).format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } }.orEmpty() } @@ -1513,7 +1506,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( currencyToSend = swapCurrencyConverter.convert(fromToken), destinationAddress = transaction.txTo, increaseBy = INCREASE_GAS_LIMIT_BY, - data = transaction.txData, + callData = CompiledSmartContractCallData(transaction.txData.hexToBytes()), derivationPath = fromToken.network.derivationPath.value, ) } catch (e: IllegalStateException) { @@ -1617,12 +1610,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( } val derivationPath = fromToken.network.derivationPath.value // setting up amount for approve with given amount for swap [SwapApproveType.Limited] - val transactionData = getApproveData( - networkId = networkId, - derivationPath = derivationPath, - fromToken = fromToken, - swapAmount = swapAmount, + val callData = SmartContractCallDataProviderFactory.getApprovalCallData( spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" }, + amount = swapAmount.value.convertToSdkAmount(fromToken), + blockchain = Blockchain.fromId(fromToken.network.id.value), ) val cardId = userWallet.scanResponse.card.cardId val feeData = if (isDemoCardUseCase(cardId)) { @@ -1635,7 +1626,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)), destinationAddress = fromToken.getContractAddress(), increaseBy = INCREASE_GAS_LIMIT_BY, - data = transactionData, + callData = callData, derivationPath = derivationPath, ) } catch (e: Exception) { @@ -1672,7 +1663,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( spenderAddress = getTokenAddress(fromToken), requestApproveData = RequestApproveStateData( fee = feeState, - approveData = transactionData, fromTokenAmount = swapAmount, spenderAddress = spenderAddress, ), @@ -2140,23 +2130,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( return PriceImpact.Value(value) } - private suspend fun getApproveData( - networkId: String, - derivationPath: String?, - fromToken: CryptoCurrency, - swapAmount: SwapAmount? = null, - spenderAddress: String, - ): String { - return repository.getApproveData( - userWalletId = userWalletId, - networkId = networkId, - derivationPath = derivationPath, - currency = fromToken, - amount = swapAmount?.value, - spenderAddress = spenderAddress, - ) - } - private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { val set = ids.mapNotNull { it.rawCurrencyId } .toSet() diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7698d4b61b..ec47aff56d 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-1033" +tangemBlockchainSdk = "develop-1034" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 8bdae098a7..baed3f3725 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -1,6 +1,7 @@ package com.tangem.lib.crypto import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.smartcontract.SmartContractCallData import com.tangem.lib.crypto.models.* import java.math.BigInteger @@ -14,7 +15,7 @@ interface TransactionManager { * @param currencyToSend currency to send in tx * @param destinationAddress address to send tx * @param increaseBy percents in format 125 = 25% - * @param data data for tx + * @param callData tx smart contract call data * @param derivationPath derivation path * @return */ @@ -26,7 +27,7 @@ interface TransactionManager { currencyToSend: Currency, destinationAddress: String, increaseBy: Int?, - data: String?, + callData: SmartContractCallData?, derivationPath: String?, ): ProxyFees From f608d0e31c8787e8de46f67f86c71f6e82d97318 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Apr 2025 12:31:17 +0500 Subject: [PATCH 59/72] Updated on 2026-08-14 --- .../send/v2/subcomponents/fee/model/SendFeeModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 8f01d213c4..b2bd054e2a 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 @@ -8,7 +8,7 @@ import com.tangem.core.decompose.model.Model 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.GetFeeUseCase +import com.tangem.domain.transaction.usecase.GetTransferFeeUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.impl.R @@ -40,7 +40,7 @@ internal class SendFeeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val isFeeApproximateUseCase: IsFeeApproximateUseCase, - private val getFeeUseCase: GetFeeUseCase, + private val getTransferFeeUseCase: GetTransferFeeUseCase, private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, private val sendFeeAlertFactory: SendFeeAlertFactory, @@ -192,7 +192,7 @@ internal class SendFeeModel @Inject constructor( if (isShowLoading) { _uiState.update(SendFeeLoadingTransformer) } - getFeeUseCase.invoke( + getTransferFeeUseCase.invoke( amount = amountValue, destination = destinationAddress, userWallet = params.userWallet, @@ -222,7 +222,7 @@ internal class SendFeeModel @Inject constructor( private fun checkLoadFee() { modelScope.launch { - getFeeUseCase.invoke( + getTransferFeeUseCase.invoke( amount = params.sendAmount, destination = params.destinationAddress, userWallet = params.userWallet, From 29504be863d24e230e64fd2a4752edda43979632 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Apr 2025 13:48:25 +0500 Subject: [PATCH 60/72] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 34 +++++++++++++++++++ .../models/CryptoNetworkResponse.kt | 11 ++++++ .../NotificationApplicationCreateBody.kt | 14 ++++++++ .../NotificationApplicationIdResponse.kt | 9 +++++ .../NotificationApplicationUpdateBody.kt | 12 +++++++ .../api/tangemTech/models/WalletBody.kt | 10 ++++++ .../api/tangemTech/models/WalletIdBody.kt | 9 +++++ .../api/tangemTech/models/WalletResponse.kt | 11 ++++++ 8 files changed, 110 insertions(+) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CryptoNetworkResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationCreateBody.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationIdResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationUpdateBody.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletBody.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletResponse.kt 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 c109e5b91e..7b474dd442 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 @@ -140,6 +140,40 @@ interface TangemTechApi { @GET("stories/{story_id}") suspend fun getStoryById(@Path("story_id") storyId: String): ApiResponse + // region push notifications + @GET("notification/push_notifications_eligible_networks") + suspend fun getEligibleNetworksForPushNotifications(): ApiResponse> + + @POST("user-wallets/applications/") + suspend fun createApplicationId( + @Body + body: NotificationApplicationCreateBody, + ): ApiResponse + + @PATCH("user-wallets/applications/{application_id}") + suspend fun updatePushTokenForApplicationId( + @Path("application_id") applicationId: String, + @Body body: NotificationApplicationCreateBody, + ): ApiResponse + + @PATCH("user-wallets/wallets/{wallet_id}/notify") + suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse + // endregion + + // region wallets + @PATCH("user-wallets/wallets/{wallet_id}") + suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse + + @POST("user-wallets/wallets/create-and-connect-by-appuid/{application_id}") + suspend fun associateApplicationIdWithWallets( + @Path("application_id") applicationId: String, + @Body body: List, + ): ApiResponse + + @GET("user-wallets/wallets/{wallet_id}") + suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse + // endregion + companion object { val marketsQuoteFields = listOf( "price", diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CryptoNetworkResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CryptoNetworkResponse.kt new file mode 100644 index 0000000000..610606eae8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CryptoNetworkResponse.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CryptoNetworkResponse( + @Json(name = "id") val id: Int, + @Json(name = "networkId") val networkId: String, + @Json(name = "name") val name: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationCreateBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationCreateBody.kt new file mode 100644 index 0000000000..e28589eefa --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationCreateBody.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class NotificationApplicationCreateBody( + @Json(name = "pushToken") val pushToken: String, + @Json(name = "platform") val platform: String, + @Json(name = "device") val device: String, + @Json(name = "systemVersion") val systemVersion: String, + @Json(name = "language") val language: String, + @Json(name = "timezone") val timezone: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationIdResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationIdResponse.kt new file mode 100644 index 0000000000..4a2191fc4e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationIdResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class NotificationApplicationIdResponse( + @Json(name = "uid") val appId: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationUpdateBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationUpdateBody.kt new file mode 100644 index 0000000000..ff5d0863cb --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/NotificationApplicationUpdateBody.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class NotificationApplicationUpdateBody( + @Json(name = "pushToken") val pushToken: String, + @Json(name = "systemVersion") val systemVersion: String? = null, + @Json(name = "language") val language: String? = null, + @Json(name = "timezone") val timezone: String? = null, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletBody.kt new file mode 100644 index 0000000000..d7a5fa0b91 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletBody.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class WalletBody( + @Json(name = "notifyStatus") val notifyStatus: String? = null, + @Json(name = "name") val name: String? = null, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt new file mode 100644 index 0000000000..dbadddd6fc --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class WalletIdBody( + @Json(name = "id") val walletId: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletResponse.kt new file mode 100644 index 0000000000..ad0ff4ebc6 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletResponse.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class WalletResponse( + @Json(name = "notifyStatus") val notifyStatus: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "id") val id: String, +) \ No newline at end of file From e608f81717eb0246ad0cf6622aa4cb996b6377bd Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Apr 2025 14:17:51 +0500 Subject: [PATCH 61/72] Updated on 2026-08-14 --- .../transformer/UpdateDataStateTransformer.kt | 2 +- .../collections/ui/NFTCollectionsContent.kt | 105 +++++++++++------- .../nft/receive/ui/NFTReceiveNetworksEmpty.kt | 23 +++- 3 files changed, 81 insertions(+), 49 deletions(-) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt index 77f0c846ee..234ba96ceb 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt @@ -68,7 +68,7 @@ internal class UpdateDataStateTransformer( is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, - -> true + -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt index e1301e1146..bb30235ff8 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt @@ -9,11 +9,13 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.key 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.tooling.preview.Preview import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.components.PrimaryButton @@ -36,6 +38,8 @@ import kotlinx.collections.immutable.persistentListOf internal fun NFTCollectionsContent(content: NFTCollectionsUM.Content, modifier: Modifier = Modifier) { val listState = rememberLazyListState() + val bottomPadding = TangemTheme.dimens.spacing60 + Box( modifier = modifier .fillMaxSize() @@ -54,55 +58,72 @@ internal fun NFTCollectionsContent(content: NFTCollectionsUM.Content, modifier: state = content.search, colors = TangemSearchBarDefaults.secondaryTextFieldColors, ) - content.warnings.fastForEach { - key(it.id) { - NFTCollectionWarning( + if (content.collections.isEmpty()) { + Box( + modifier = modifier + .fillMaxSize() + .padding(bottom = bottomPadding), + ) { + Text( modifier = Modifier - .padding(top = TangemTheme.dimens.spacing16), - state = it, + .align(Alignment.Center), + text = stringResourceSafe(id = R.string.nft_empty_search), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, ) } - } - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding( - top = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing60, - ) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.primary), - state = listState, - ) { - content.collections.fastForEach { collection -> - item(key = collection.id) { - NFTCollection( - modifier = Modifier.fillMaxWidth(), - state = collection, + } else { + content.warnings.fastForEach { + key(it.id) { + NFTCollectionWarning( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16), + state = it, ) } - when (val assets = collection.assets) { - is NFTCollectionAssetsListUM.Init -> Unit - is NFTCollectionAssetsListUM.Loading -> { - assetsListLoading( - collectionId = collection.id, - content = assets, - expanded = collection.isExpanded, + } + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing16, + bottom = bottomPadding, + ) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.primary), + state = listState, + ) { + content.collections.fastForEach { collection -> + item(key = collection.id) { + NFTCollection( + modifier = Modifier.fillMaxWidth(), + state = collection, ) } - is NFTCollectionAssetsListUM.Failed -> { - assetsListFailed( - collectionId = collection.id, - content = assets, - expanded = collection.isExpanded, - ) - } - is NFTCollectionAssetsListUM.Content -> { - assetsListContent( - collectionId = collection.id, - content = assets, - expanded = collection.isExpanded, - ) + when (val assets = collection.assets) { + is NFTCollectionAssetsListUM.Init -> Unit + is NFTCollectionAssetsListUM.Loading -> { + assetsListLoading( + collectionId = collection.id, + content = assets, + expanded = collection.isExpanded, + ) + } + is NFTCollectionAssetsListUM.Failed -> { + assetsListFailed( + collectionId = collection.id, + content = assets, + expanded = collection.isExpanded, + ) + } + is NFTCollectionAssetsListUM.Content -> { + assetsListContent( + collectionId = collection.id, + content = assets, + expanded = collection.isExpanded, + ) + } } } } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksEmpty.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksEmpty.kt index cebaa5a91d..931138a97d 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksEmpty.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksEmpty.kt @@ -1,7 +1,11 @@ package com.tangem.features.nft.receive.ui +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -9,10 +13,17 @@ import com.tangem.features.nft.impl.R @Composable internal fun NFTReceiveNetworksEmpty(modifier: Modifier = Modifier) { - Text( - modifier = modifier, - text = stringResourceSafe(id = R.string.nft_empty_search), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary), + ) { + Text( + modifier = Modifier + .align(Alignment.Center), + text = stringResourceSafe(id = R.string.nft_empty_search), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } } \ No newline at end of file From 80173d43496a2ede0e947807cb1e8f816158470a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Apr 2025 15:43:31 +0500 Subject: [PATCH 62/72] Updated on 2026-08-14 --- domain/nft/build.gradle.kts | 1 + .../domain/nft/analytics/NFTAnalyticsEvent.kt | 60 +++++++++++++++++++ .../nft/details/model/NFTDetailsModel.kt | 17 +++++- .../nft/receive/model/NFTReceiveModel.kt | 16 +++-- .../intents/WalletContentClickIntents.kt | 16 +++++ 5 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt diff --git a/domain/nft/build.gradle.kts b/domain/nft/build.gradle.kts index b5ce316d58..518f509278 100644 --- a/domain/nft/build.gradle.kts +++ b/domain/nft/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) + implementation(projects.core.analytics.models) implementation(projects.core.utils) implementation(projects.domain.core) diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt new file mode 100644 index 0000000000..dbb2139d18 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt @@ -0,0 +1,60 @@ +package com.tangem.domain.nft.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.COUNT +import com.tangem.core.analytics.models.AnalyticsParam.Key.STATE + +sealed class NFTAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent( + category = "NFT", + event = event, + params = params, +) { + + data class NFTListScreenOpened( + val state: State, + ) : NFTAnalyticsEvent( + event = "NFT List Screen Opened", + params = buildMap { + put(STATE, state.value) + if (state is State.Full) { + put(COUNT, state.count.toString()) + } + }, + ) { + sealed class State(val value: String) { + data object Empty : State("Empty") + data class Full(val count: Int) : State("Full") + } + } + + object Receive { + data object ScreenOpened : NFTAnalyticsEvent(event = "Receive NFT Screen Opened") + + data class BlockchainChosen( + private val blockchain: String, + ) : NFTAnalyticsEvent(event = "Blockchain Chosen", params = mapOf(BLOCKCHAIN to blockchain)) + + data class CopyAddress( + private val blockchain: String, + ) : NFTAnalyticsEvent(event = "Button - Copy Address", params = mapOf(BLOCKCHAIN to blockchain)) + + data class ShareAddress( + private val blockchain: String, + ) : NFTAnalyticsEvent(event = "Button - Share Address", params = mapOf(BLOCKCHAIN to blockchain)) + } + + object Details { + data class ScreenOpened( + private val blockchain: String, + ) : NFTAnalyticsEvent(event = "NFT Details Screen Opened", params = mapOf(BLOCKCHAIN to blockchain)) + + data object ButtonReadMore : NFTAnalyticsEvent(event = "Button - Read More") + data object ButtonSeeAll : NFTAnalyticsEvent(event = "Button - See All") + data object ButtonExplore : NFTAnalyticsEvent(event = "Button - Explore") + data object ButtonSend : NFTAnalyticsEvent(event = "Button - Send") + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index c81f317e67..918cfd4ab6 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -1,10 +1,12 @@ package com.tangem.features.nft.details.model import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler 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.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.features.nft.details.NFTDetailsComponent import com.tangem.features.nft.details.entity.NFTAssetUM import com.tangem.features.nft.details.entity.NFTDetailsUM @@ -18,6 +20,7 @@ import javax.inject.Inject internal class NFTDetailsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, + private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { @@ -37,22 +40,30 @@ internal class NFTDetailsModel @Inject constructor( ), onBackClick = params.onBackClick, onReadMoreClick = ::onReadMoreClick, - onSeeAllTraitsClick = params.onAllTraitsClick, + onSeeAllTraitsClick = { + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSeeAll) + params.onAllTraitsClick() + }, onExploreClick = ::onExploreClick, onSendClick = ::onSendClick, bottomSheetConfig = null, ), ) + init { + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ScreenOpened(params.nftAsset.network.name)) + } + private fun onReadMoreClick() { - // TODO implement + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore) } private fun onExploreClick() { - // TODO implement + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonExplore) } private fun onSendClick() { + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSend) router.push( AppRoute.NFTSend( userWalletId = params.userWalletId, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt index 83bfeedb42..3d62d4c7e2 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.nft.receive.model +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -11,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase import com.tangem.domain.nft.GetNFTAvailableNetworksUseCase import com.tangem.domain.nft.GetNFTNetworkStatusUseCase +import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.features.nft.impl.R @@ -36,6 +38,7 @@ internal class NFTReceiveModel @Inject constructor( private val getNFTNetworkStatusUseCase: GetNFTNetworkStatusUseCase, private val clipboardManager: ClipboardManager, private val shareManager: ShareManager, + private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { @@ -53,6 +56,7 @@ internal class NFTReceiveModel @Inject constructor( val state: StateFlow get() = _state init { + analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ScreenOpened) subscribeToNFTAvailableNetworks() } @@ -106,6 +110,8 @@ internal class NFTReceiveModel @Inject constructor( private fun onNetworkClick(network: Network) { modelScope.launch { + analyticsEventHandler.send(NFTAnalyticsEvent.Receive.BlockchainChosen(network.name)) + val networkStatus = getNFTNetworkStatusUseCase.invoke( userWalletId = params.userWalletId, network = network, @@ -118,8 +124,8 @@ internal class NFTReceiveModel @Inject constructor( network = network, networkAddress = value.address, onDismissBottomSheet = ::onReceiveBottomSheetDismiss, - onCopyClick = ::onCopyClick, - onShareClick = ::onShareClick, + onCopyClick = { text -> onCopyClick(text, network) }, + onShareClick = { text -> onShareClick(text, network) }, ).transform(it) } } @@ -131,11 +137,13 @@ internal class NFTReceiveModel @Inject constructor( } } - private fun onCopyClick(text: String) { + private fun onCopyClick(text: String, network: Network) { + analyticsEventHandler.send(NFTAnalyticsEvent.Receive.CopyAddress(network.name)) clipboardManager.setText(text = text, isSensitive = true) } - private fun onShareClick(text: String) { + private fun onShareClick(text: String, network: Network) { + analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ShareAddress(network.name)) shareManager.shareText(text = text) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index c1e1f1fde0..680e373b9b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -2,7 +2,9 @@ package com.tangem.feature.wallet.child.wallet.model.intents import arrow.core.getOrElse import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase @@ -75,6 +77,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val reduxStateHolder: ReduxStateHolder, private val walletEventSender: WalletEventSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : BaseWalletClickIntents(), WalletContentClickIntents { override fun onDetailsClick() { @@ -240,6 +243,19 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } override fun onNFTClick(userWalletId: UserWalletId) { + val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return + val analyticsState = when (val nftState = selectedWallet.nftState) { + is WalletNFTItemUM.Content -> NFTAnalyticsEvent.NFTListScreenOpened.State.Full(nftState.assetsCount) + is WalletNFTItemUM.Empty -> NFTAnalyticsEvent.NFTListScreenOpened.State.Empty + is WalletNFTItemUM.Failed, + is WalletNFTItemUM.Hidden, + is WalletNFTItemUM.Loading, + -> null + } + analyticsState?.let { + analyticsEventHandler.send(NFTAnalyticsEvent.NFTListScreenOpened(analyticsState)) + } + router.openNFT(userWalletId) } } \ No newline at end of file From 887ea85b3b6c9d22f617380f465617db3020d826 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Apr 2025 15:44:17 +0400 Subject: [PATCH 63/72] Updated on 2026-08-14 --- common/test/build.gradle.kts | 2 + .../MockYieldBalanceWrapperDTOFactory.kt | 51 +++++ .../token/converter/YieldBalanceConverter.kt | 2 +- data/staking/build.gradle.kts | 7 + .../store/DefaultYieldsBalancesStore.kt | 158 +++++++++++++++ .../data/staking/store/YieldsBalancesStore.kt | 31 +++ .../tangem/data/staking/YieldBalanceExt.kt | 10 + .../store/YieldsBalancesStoreGetMethodTest.kt | 83 ++++++++ .../YieldsBalancesStoreInitializationTest.kt | 80 ++++++++ .../YieldsBalancesStoreUpdateMethodsTest.kt | 188 ++++++++++++++++++ .../staking/model/stakekit/YieldBalance.kt | 17 +- 11 files changed, 625 insertions(+), 4 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt diff --git a/common/test/build.gradle.kts b/common/test/build.gradle.kts index 4ab99487fb..f2de491c8c 100644 --- a/common/test/build.gradle.kts +++ b/common/test/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.data.common) + implementation(projects.data.staking) implementation(projects.domain.legacy) implementation(projects.domain.models) @@ -23,6 +24,7 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(deps.androidx.datastore) + implementation(deps.jodatime) implementation(deps.test.coroutine) implementation(tangemDeps.blockchain) diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt new file mode 100644 index 0000000000..521604e487 --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt @@ -0,0 +1,51 @@ +package com.tangem.common.test.data.staking + +import com.tangem.data.staking.store.YieldsBalancesStore.StakingID +import com.tangem.datasource.api.stakekit.models.request.Address +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +object MockYieldBalanceWrapperDTOFactory { + + val defaultStakingId = StakingID( + integrationId = "ton-ton-chorus-one-pools-staking", + address = "0x1", + ) + + fun createWithBalance(stakingId: StakingID = defaultStakingId): YieldBalanceWrapperDTO { + return YieldBalanceWrapperDTO( + addresses = Address(address = stakingId.address), + balances = listOf( + BalanceDTO( + groupId = "groupId", + type = BalanceDTO.BalanceTypeDTO.UNKNOWN, + amount = BigDecimal.ONE, + date = null, + pricePerShare = BigDecimal.ZERO, + pendingActions = listOf(), + pendingActionConstraints = null, + tokenDTO = TokenDTO( + name = "The-Open-Network", + network = NetworkTypeDTO.TON, + symbol = "TON", + decimals = 8, + address = null, + coinGeckoId = null, + logoURI = null, + isPoints = null, + ), + validatorAddress = null, + validatorAddresses = null, + providerId = null, + ), + ), + integrationId = stakingId.integrationId, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt index 051e63780e..de55373f39 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt @@ -7,7 +7,7 @@ import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceItem import com.tangem.utils.converter.Converter -internal class YieldBalanceConverter( +class YieldBalanceConverter( private val source: StatusSource, ) : Converter { diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 9af26ac76e..b4d8282fac 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -62,4 +62,11 @@ dependencies { } // endregion + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(tangemDeps.card.core) + testImplementation(projects.common.test) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt new file mode 100644 index 0000000000..32beb14ec3 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt @@ -0,0 +1,158 @@ +package com.tangem.data.staking.store + +import androidx.datastore.core.DataStore +import com.tangem.data.staking.store.YieldsBalancesStore.StakingID +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.token.converter.YieldBalanceConverter +import com.tangem.domain.models.StatusSource +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch + +internal typealias WalletIdWithWrappers = Map> +internal typealias WalletIdWithBalances = Map> + +/** + * Default implementation of [YieldsBalancesStore] + * + * @property runtimeStore runtime store + * @property persistenceStore persistence store + * @param dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultYieldsBalancesStore( + private val runtimeStore: RuntimeSharedStore, + private val persistenceStore: DataStore, + dispatchers: CoroutineDispatcherProvider, +) : YieldsBalancesStore { + + private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) + + init { + scope.launch { + val cachedStatuses = persistenceStore.data.firstOrNull() ?: return@launch + + runtimeStore.store( + value = cachedStatuses.map { (stringWalletId, wrappers) -> + val key = UserWalletId(stringWalletId) + val value = YieldBalanceConverter(isCached = true).convertSet(input = wrappers) + + key to value + } + .toMap(), + ) + } + } + + override fun get(userWalletId: UserWalletId): Flow> { + return runtimeStore.get().mapNotNull { it[userWalletId] } + } + + override suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) { + updateBalanceInRuntime(userWalletId, stakingId) { + it.copySealed(source = StatusSource.CACHE) + } + } + + override suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) { + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + val storedBalances = this[userWalletId].orEmpty() + + val balances = stakingIds.mapTo(hashSetOf()) { stakingId -> + val balance = storedBalances.firstOrNull { + it.integrationId == stakingId.integrationId && + it.address == stakingId.address + } + ?: createDefaultBalance(id = stakingId) + + balance.copySealed(source = StatusSource.CACHE) + } + + val updatedBalances = storedBalances.addOrReplace(balances) { old, new -> + old.integrationId == new.integrationId && old.address == new.address + } + + put(key = userWalletId, value = updatedBalances) + } + } + } + + override suspend fun storeActual(userWalletId: UserWalletId, values: Set) { + coroutineScope { + launch { storeInRuntime(userWalletId = userWalletId, values = values) } + launch { storeInPersistence(userWalletId = userWalletId, values = values) } + } + } + + override suspend fun storeError(userWalletId: UserWalletId, stakingId: StakingID) { + updateBalanceInRuntime(userWalletId, stakingId) { + it.copySealed(source = StatusSource.ONLY_CACHE) + } + } + + private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set) { + val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values) + + runtimeStore.update(default = emptyMap()) { saved -> + saved.toMutableMap().apply { + this[userWalletId] = saved[userWalletId] + ?.addOrReplace(newBalances) { old, new -> + old.integrationId == new.integrationId && old.address == new.address + } + ?: newBalances + } + } + } + + private suspend fun storeInPersistence(userWalletId: UserWalletId, values: Set) { + persistenceStore.updateData { current -> + current.toMutableMap().apply { + this[userWalletId.stringValue] = current[userWalletId.stringValue] + ?.addOrReplace(items = values) { old, new -> + old.integrationId == new.integrationId && old.addresses.address == new.addresses.address + } + ?: values + } + } + } + + private suspend fun updateBalanceInRuntime( + userWalletId: UserWalletId, + stakingID: StakingID, + update: (YieldBalance) -> YieldBalance, + ) { + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + val balance = this[userWalletId].orEmpty() + .firstOrNull { + it.integrationId == stakingID.integrationId && + it.address == stakingID.address + } + ?: createDefaultBalance(id = stakingID) + + val updatedBalances = this[userWalletId].orEmpty() + .addOrReplace(item = update(balance)) { + it.integrationId == balance.integrationId && + it.address == balance.address + } + + put(key = userWalletId, value = updatedBalances) + } + } + } + + private fun createDefaultBalance(id: StakingID): YieldBalance { + return YieldBalance.Error(integrationId = id.integrationId, address = id.address) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt new file mode 100644 index 0000000000..2e5c062f3d --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt @@ -0,0 +1,31 @@ +package com.tangem.data.staking.store + +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Store of [YieldBalance]'s set + * +[REDACTED_AUTHOR] + */ +interface YieldsBalancesStore { + + /** Get flow of [YieldBalance]'s set by [userWalletId] */ + fun get(userWalletId: UserWalletId): Flow> + + /** Refresh balance of [stakingId] by [userWalletId] */ + suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) + + /** Refresh balances of [stakingIds] by [userWalletId] */ + suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) + + /** Store actual [values] by [userWalletId] */ + suspend fun storeActual(userWalletId: UserWalletId, values: Set) + + /** Store error by [userWalletId] and [stakingId] */ + suspend fun storeError(userWalletId: UserWalletId, stakingId: StakingID) + + data class StakingID(val integrationId: String, val address: String) +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt new file mode 100644 index 0000000000..c04560fbeb --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt @@ -0,0 +1,10 @@ +package com.tangem.data.staking + +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.token.converter.YieldBalanceConverter +import com.tangem.domain.models.StatusSource +import com.tangem.domain.staking.model.stakekit.YieldBalance + +internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): YieldBalance { + return YieldBalanceConverter(source = source).convert(this) +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt new file mode 100644 index 0000000000..7900b655e1 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt @@ -0,0 +1,83 @@ +package com.tangem.data.staking.store + +import com.google.common.truth.Truth +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.staking.toDomain +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class YieldsBalancesStoreGetMethodTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultYieldsBalancesStore( + runtimeStore = runtimeStore, + persistenceStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `test get if runtime store is empty`() = runTest { + val actual = store.get(userWalletId = userWalletId) + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values).isEqualTo(emptyList>()) + } + + @Test + fun `test get if runtime store contains empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + + val actual = store.get(userWalletId = userWalletId) + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values).isEqualTo(emptyList>()) + } + + @Test + fun `test get if runtime store contains portfolio with empty balances`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId to emptySet()), + ) + + val actual = store.get(userWalletId = userWalletId) + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) + } + + @Test + fun `test get if runtime store is not empty`() = runTest { + val wrapper = MockYieldBalanceWrapperDTOFactory.createWithBalance() + + runtimeStore.store( + value = mapOf(userWalletId to setOf(wrapper.toDomain())), + ) + + val actual = store.get(userWalletId = userWalletId) + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(setOf(wrapper.toDomain()))) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + } +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt new file mode 100644 index 0000000000..13bb51d6e3 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt @@ -0,0 +1,80 @@ +package com.tangem.data.staking.store + +import androidx.datastore.core.DataStore +import com.google.common.truth.Truth +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.data.staking.toDomain +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.wallets.models.UserWalletId +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 + +/** +[REDACTED_AUTHOR] + */ +internal class YieldsBalancesStoreInitializationTest { + + @Test + fun `test initialization if cache store is empty`() = runTest { + val runtimeStore = RuntimeSharedStore() + val persistenceStore: DataStore = mockk() + + every { persistenceStore.data } returns emptyFlow() + + DefaultYieldsBalancesStore( + runtimeStore = runtimeStore, + persistenceStore = 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()) + + DefaultYieldsBalancesStore( + runtimeStore = runtimeStore, + persistenceStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `test initialization if cache store is not empty`() = runTest { + val runtimeStore = RuntimeSharedStore() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + val wrapper = MockYieldBalanceWrapperDTOFactory.createWithBalance() + + persistenceStore.updateData { + it.toMutableMap().apply { + put(userWalletId.stringValue, setOf(wrapper)) + } + } + + DefaultYieldsBalancesStore( + runtimeStore = runtimeStore, + persistenceStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val expected = mapOf(userWalletId to setOf(wrapper.toDomain())) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(expected) + } + + private companion object { + val userWalletId = UserWalletId(stringValue = "011") + } +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt new file mode 100644 index 0000000000..f64a6da8e8 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt @@ -0,0 +1,188 @@ +package com.tangem.data.staking.store + +import com.google.common.truth.Truth +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.data.staking.toDomain +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class YieldsBalancesStoreUpdateMethodsTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultYieldsBalancesStore( + runtimeStore = runtimeStore, + persistenceStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `refresh the single id if runtime store is empty`() = runTest { + store.refresh(userWalletId = userWalletId, stakingId = stakingId) + + val runtimeExpected = mapOf( + userWalletId to setOf( + YieldBalance.Error( + integrationId = stakingId.integrationId, + address = stakingId.address, + ), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `refresh the single id if runtime store contains balance with this id`() = runTest { + val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance().toDomain(source = StatusSource.ACTUAL) + + runtimeStore.store( + value = mapOf(userWalletId to setOf(balance)), + ) + + store.refresh(userWalletId = userWalletId, stakingId = stakingId) + + val runtimeExpected = mapOf( + userWalletId to setOf( + balance.copySealed(source = StatusSource.CACHE), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `refresh the multi ids if runtime store is empty`() = runTest { + store.refresh(userWalletId = userWalletId, stakingIds = stakingIds) + + val runtimeExpected = mapOf( + userWalletId to stakingIds.mapTo(hashSetOf()) { + YieldBalance.Error(integrationId = it.integrationId, address = it.address) + }, + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `refresh the multi ids if runtime store contains balance with this id`() = runTest { + val firstBalance = MockYieldBalanceWrapperDTOFactory.createWithBalance(stakingId = stakingIds.first()) + .toDomain(source = StatusSource.ACTUAL) + + val secondBalance = MockYieldBalanceWrapperDTOFactory.createWithBalance(stakingId = stakingIds.last()) + .toDomain(source = StatusSource.ACTUAL) + + runtimeStore.store( + value = mapOf(userWalletId to setOf(firstBalance, secondBalance)), + ) + + store.refresh(userWalletId = userWalletId, stakingIds = stakingIds) + + val runtimeExpected = mapOf( + userWalletId to setOf( + firstBalance.copySealed(source = StatusSource.CACHE), + secondBalance.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 balance with this id`() = runTest { + val prevWrapper = MockYieldBalanceWrapperDTOFactory.createWithBalance() + + runtimeStore.store( + value = mapOf(userWalletId to setOf(prevWrapper.toDomain(source = StatusSource.CACHE))), + ) + + persistenceStore.updateData { + it.toMutableMap().apply { + put(userWalletId.stringValue, setOf(prevWrapper)) + } + } + + val newWrapper = MockYieldBalanceWrapperDTOFactory.createWithBalance() + + store.storeActual(userWalletId, setOf(newWrapper)) + + val runtimeExpected = mapOf( + userWalletId to setOf(newWrapper.toDomain(source = StatusSource.ACTUAL)), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newWrapper), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store error if runtime store is empty`() = runTest { + store.storeError(userWalletId = userWalletId, stakingId = stakingId) + + val runtimeExpected = mapOf( + userWalletId to setOf( + YieldBalance.Error( + integrationId = stakingId.integrationId, + address = stakingId.address, + ), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `store error if runtime store contains balance with this id`() = runTest { + val wrapper = MockYieldBalanceWrapperDTOFactory.createWithBalance(stakingId) + + runtimeStore.store( + value = mapOf( + userWalletId to setOf(wrapper.toDomain(source = StatusSource.CACHE)), + ), + ) + + store.storeError(userWalletId = userWalletId, stakingId = stakingId) + + val runtimeExpected = mapOf( + userWalletId to setOf(wrapper.toDomain(source = StatusSource.ONLY_CACHE)), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + val stakingId = MockYieldBalanceWrapperDTOFactory.defaultStakingId + + val stakingIds = setOf( + stakingId, + YieldsBalancesStore.StakingID( + integrationId = "solana-sol-native-multivalidator-staking", + address = "0x1", + ), + ) + } +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index ce016aee04..74d271b455 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -9,21 +9,32 @@ sealed class YieldBalance { abstract val integrationId: String? abstract val address: String? + abstract val source: StatusSource + + fun copySealed(source: StatusSource): YieldBalance { + return when (this) { + is Data -> copy(source = source) + is Empty -> copy(source = source) + is Error -> this + } + } data class Data( override val integrationId: String?, override val address: String, + override val source: StatusSource, val balance: YieldBalanceItem, - val source: StatusSource, ) : YieldBalance() data class Empty( override val integrationId: String?, override val address: String, - val source: StatusSource, + override val source: StatusSource, ) : YieldBalance() - data class Error(override val integrationId: String?, override val address: String?) : YieldBalance() + data class Error(override val integrationId: String?, override val address: String?) : YieldBalance() { + override val source: StatusSource = StatusSource.ACTUAL + } } data class YieldBalanceItem( From e6de882b3bdccb104a7affee2262b080d8c7f914 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Apr 2025 21:50:36 +0700 Subject: [PATCH 64/72] Updated on 2026-08-14 --- data/wallet-connect/build.gradle.kts | 1 + .../di/WalletConnectDataModule.kt | 2 + ...e.kt => DefaultWcEthMessageSignUseCase.kt} | 26 ++++++--- .../DefaultWcEthSendTransactionUseCase.kt | 57 ++++++++++++++++++ .../DefaultWcEthSignTransactionUseCase.kt | 58 +++++++++++++++++++ .../network/ethereum/WcEthNetwork.kt | 31 ++++++++-- .../walletconnect/sign/BaseWcSignUseCase.kt | 8 ++- domain/wallet-connect/build.gradle.kts | 10 +++- .../domain/walletconnect/model/WcEthMethod.kt | 10 +++- .../model/WcEthTransactionParams.kt | 28 +++++++++ ...nUseCase.kt => WcEthMessageSignUseCase.kt} | 5 +- .../ethereum/WcEthTransactionUseCase.kt | 24 ++++++++ .../walletconnect/usecase/pair/WcPairState.kt | 23 -------- .../usecase/pair/WcPairUseCase.kt | 20 +++++++ .../walletconnect/usecase/sign/WcSignState.kt | 14 ----- .../usecase/sign/WcSignUseCase.kt | 18 ++++-- 16 files changed, 273 insertions(+), 62 deletions(-) rename data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/{DefaultWcPersonalEthSignUseCase.kt => DefaultWcEthMessageSignUseCase.kt} (78%) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthSendTransactionUseCase.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthSignTransactionUseCase.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthTransactionParams.kt rename domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/{WcPersonalEthSignUseCase.kt => WcEthMessageSignUseCase.kt} (66%) create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthTransactionUseCase.kt delete mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt delete mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignState.kt diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index e130216844..cb90e05209 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.domain.walletConnect) implementation(projects.domain.walletConnect.models) implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) 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 609ebb2959..1a5345959e 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 @@ -131,10 +131,12 @@ internal object WalletConnectDataModule { @SdkMoshi moshi: Moshi, excludedBlockchains: ExcludedBlockchains, sessionsManager: WcSessionsManager, + factories: WcEthNetwork.Factories, ): WcEthNetwork = WcEthNetwork( moshi = moshi, excludedBlockchains = excludedBlockchains, sessionsManager = sessionsManager, + factories = factories, ) @Provides diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt similarity index 78% rename from data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt rename to data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt index 98e6f2db49..8421ccb613 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcPersonalEthSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt @@ -14,23 +14,26 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.domain.transaction.usecase.SignUseCase import com.tangem.domain.walletconnect.model.WcEthMethod -import com.tangem.domain.walletconnect.usecase.ethereum.WcPersonalEthSignUseCase +import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMessageSignUseCase import com.tangem.domain.walletconnect.usecase.sign.WcSignState import com.tangem.domain.walletmanager.WalletManagersFacade +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow -internal class DefaultWcPersonalEthSignUseCase( +internal class DefaultWcEthMessageSignUseCase @AssistedInject constructor( override val respondService: WcRespondService, - override val context: WcMethodUseCaseContext, - private val method: WcEthMethod.PersonalEthSign, + @Assisted override val context: WcMethodUseCaseContext, + @Assisted private val method: WcEthMethod.MessageSign, private val walletManagersFacade: WalletManagersFacade, private val signUseCase: SignUseCase, -) : BaseWcSignUseCase(), - WcPersonalEthSignUseCase { +) : BaseWcSignUseCase(), + WcEthMessageSignUseCase { - override val onSign: OnSign = collector@{ state -> + override val onSign: OnSign = collector@{ state -> val hashToSign = LegacySdkHelper.createMessageData(state.signModel.rawMsg) val userWallet = session.wallet val walletManager = walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network) @@ -50,14 +53,19 @@ internal class DefaultWcPersonalEthSignUseCase( emit(state.toResult(wcRespondResult)) } - override fun invoke(): Flow> = flow { - val model = WcPersonalEthSignUseCase.SignModel( + override fun invoke(): Flow> = flow { + val model = WcEthMessageSignUseCase.SignModel( rawMsg = method.message, account = method.account, humanMsg = LegacySdkHelper.hexToAscii(method.message).orEmpty(), ) emitAll(delegate(model)) } + + @AssistedFactory + interface Factory { + fun create(context: WcMethodUseCaseContext, method: WcEthMethod.MessageSign): DefaultWcEthMessageSignUseCase + } } object LegacySdkHelper { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthSendTransactionUseCase.kt new file mode 100644 index 0000000000..76e1b410de --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthSendTransactionUseCase.kt @@ -0,0 +1,57 @@ +package com.tangem.data.walletconnect.network.ethereum + +import arrow.core.left +import com.tangem.blockchain.common.HEX_PREFIX +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.data.walletconnect.sign.BaseWcSignUseCase +import com.tangem.data.walletconnect.sign.OnSign +import com.tangem.data.walletconnect.sign.SignStateConverter.toResult +import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.usecase.ethereum.WcEthSendTransactionUseCase +import com.tangem.domain.walletconnect.usecase.sign.WcSignState +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow + +internal class DefaultWcEthSendTransactionUseCase @AssistedInject constructor( + @Assisted override val context: WcMethodUseCaseContext, + @Assisted private val method: WcEthMethod.SendTransaction, + override val respondService: WcRespondService, + private val sendTransaction: SendTransactionUseCase, +) : BaseWcSignUseCase(), + WcEthSendTransactionUseCase { + + override val onSign: OnSign = collector@{ state -> + val hash = sendTransaction(state.signModel, wallet, network) + .onLeft { error -> + val sendError = IllegalArgumentException(error.toString()) // todo(wc) use domain error + emit(state.toResult(sendError.left())) + } + .getOrNull() ?: return@collector + val respondHash = if (hash.startsWith(HEX_PREFIX)) hash else HEX_PREFIX + hash + val respondResult = respondService.respond(rawSdkRequest, respondHash) + emit(state.toResult(respondResult)) + } + + override fun updateFee(fee: TransactionFee) { + TODO("Not yet implemented") + } + + override fun invoke(): Flow> { + method + TODO("Not yet implemented") + } + + @AssistedFactory + interface Factory { + fun create( + context: WcMethodUseCaseContext, + method: WcEthMethod.SendTransaction, + ): DefaultWcEthSendTransactionUseCase + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthSignTransactionUseCase.kt new file mode 100644 index 0000000000..d8a01823c7 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthSignTransactionUseCase.kt @@ -0,0 +1,58 @@ +package com.tangem.data.walletconnect.network.ethereum + +import arrow.core.left +import com.tangem.blockchain.common.HEX_PREFIX +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.extensions.toHexString +import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.data.walletconnect.sign.BaseWcSignUseCase +import com.tangem.data.walletconnect.sign.OnSign +import com.tangem.data.walletconnect.sign.SignStateConverter.toResult +import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.usecase.ethereum.WcEthSignTransactionUseCase +import com.tangem.domain.walletconnect.usecase.sign.WcSignState +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow + +internal class DefaultWcEthSignTransactionUseCase @AssistedInject constructor( + override val respondService: WcRespondService, + private val prepareForSend: PrepareForSendUseCase, + @Assisted override val context: WcMethodUseCaseContext, + @Assisted private val method: WcEthMethod.SignTransaction, +) : BaseWcSignUseCase(), + WcEthSignTransactionUseCase { + + override val onSign: OnSign = collector@{ state -> + val hash = prepareForSend(state.signModel, wallet, network) + .onLeft { error -> + emit(state.toResult(error.left())) + } + .getOrNull() ?: return@collector + val hashString = hash.toHexString() + val respondHash = if (hashString.startsWith(HEX_PREFIX)) hashString else HEX_PREFIX + hashString + val respondResult = respondService.respond(rawSdkRequest, respondHash) + emit(state.toResult(respondResult)) + } + + override fun updateFee(fee: TransactionFee) { + TODO("Not yet implemented") + } + + override fun invoke(): Flow> { + method + TODO("Not yet implemented") + } + + @AssistedFactory + interface Factory { + fun create( + context: WcMethodUseCaseContext, + method: WcEthMethod.SignTransaction, + ): DefaultWcEthSignTransactionUseCase + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index a682ccfd11..171bb54185 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -7,18 +7,22 @@ import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.data.walletconnect.model.NamespaceKey import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson +import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.model.WcEthTransactionParams import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.WcMethodUseCase import com.tangem.domain.wallets.models.UserWallet +import jakarta.inject.Inject internal class WcEthNetwork constructor( private val moshi: Moshi, private val excludedBlockchains: ExcludedBlockchains, private val sessionsManager: WcSessionsManager, + private val factories: Factories, ) : WcRequestToUseCaseConverter, WcNamespaceConverter { override val namespaceKey: NamespaceKey = NamespaceKey("eip155") @@ -29,8 +33,12 @@ internal class WcEthNetwork constructor( val method: WcEthMethod = name.toMethod(request) ?: return null val session = sessionsManager.findSessionByTopic(request.topic) ?: return null val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null + val context = WcMethodUseCaseContext(session = session, rawSdkRequest = request, network = network) + return when (method) { - is WcEthMethod.PersonalEthSign -> TODO() + is WcEthMethod.MessageSign -> factories.messageSign.create(context, method) + is WcEthMethod.SendTransaction -> factories.sendTransaction.create(context, method) + is WcEthMethod.SignTransaction -> factories.signTransaction.create(context, method) } } @@ -44,12 +52,21 @@ internal class WcEthNetwork constructor( val messageIndex = if (this == Name.EthSign) 1 else 0 val account = list.getOrNull(accountIndex) ?: return@let null val message = list.getOrNull(messageIndex) ?: return@let null - WcEthMethod.PersonalEthSign(account = account, message = message) + WcEthMethod.MessageSign(account = account, message = message) } Name.SignTypeData -> TODO() Name.SignTypeDataV4 -> TODO() - Name.SignTransaction -> TODO() - Name.SendTransaction -> TODO() + Name.SignTransaction, + Name.SendTransaction, + -> moshi.fromJson>(rawParams) + ?.firstOrNull() + ?.let { + if (this == Name.SignTransaction) { + WcEthMethod.SignTransaction(transaction = it) + } else { + WcEthMethod.SendTransaction(transaction = it) + } + } } } @@ -81,4 +98,10 @@ internal class WcEthNetwork constructor( reference = chainId.toString(), ) } + + internal class Factories @Inject constructor( + val messageSign: DefaultWcEthMessageSignUseCase.Factory, + val sendTransaction: DefaultWcEthSendTransactionUseCase.Factory, + val signTransaction: DefaultWcEthSignTransactionUseCase.Factory, + ) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt index c1d53f51b9..d4e8696371 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt @@ -1,17 +1,18 @@ package com.tangem.data.walletconnect.sign -import com.tangem.domain.walletconnect.model.WcSession -import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.WcMethodUseCase import com.tangem.domain.walletconnect.usecase.sign.WcSignState import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase +import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.FlowCollector internal abstract class BaseWcSignUseCase : WcMethodUseCase, - WcSignUseCase.FinalAction, + WcSignUseCase, FinalActionCollector, MiddleActionCollector { @@ -21,6 +22,7 @@ internal abstract class BaseWcSignUseCase : override val network: Network get() = context.network override val session: WcSession get() = context.session override val rawSdkRequest: WcSdkSessionRequest get() = context.rawSdkRequest + val wallet: UserWallet get() = session.wallet protected val delegate by lazy { WcSignUseCaseDelegate( diff --git a/domain/wallet-connect/build.gradle.kts b/domain/wallet-connect/build.gradle.kts index e6bd5dea5d..f20c31f3c4 100644 --- a/domain/wallet-connect/build.gradle.kts +++ b/domain/wallet-connect/build.gradle.kts @@ -1,8 +1,13 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.walletconnect" +} + dependencies { /* Project - Domain */ implementation(projects.domain.core) @@ -12,4 +17,7 @@ dependencies { /* Other */ implementation(deps.moshi.adapters) + + /* Tangem libraries */ + implementation(tangemDeps.blockchain) } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt index ee9e7bfc98..15d22104ba 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt @@ -2,8 +2,16 @@ package com.tangem.domain.walletconnect.model sealed interface WcEthMethod : WcMethod { - data class PersonalEthSign( + data class MessageSign( val message: String, val account: String, ) : WcEthMethod + + data class SendTransaction( + val transaction: WcEthTransactionParams, + ) : WcEthMethod + + data class SignTransaction( + val transaction: WcEthTransactionParams, + ) : WcEthMethod } \ No newline at end of file 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 new file mode 100644 index 0000000000..cdde8975d6 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthTransactionParams.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.walletconnect.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class WcEthTransactionParams( + @Json(name = "from") + val from: String, + + @Json(name = "to") + val to: String?, + + @Json(name = "data") + val data: String, + + @Json(name = "gas") + val gas: String?, + + @Json(name = "gasPrice") + val gasPrice: String?, + + @Json(name = "value") + val value: String?, + + @Json(name = "nonce") + val nonce: String?, +) \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcPersonalEthSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMessageSignUseCase.kt similarity index 66% rename from domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcPersonalEthSignUseCase.kt rename to domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMessageSignUseCase.kt index b853bbb096..0895241c36 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcPersonalEthSignUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMessageSignUseCase.kt @@ -2,10 +2,9 @@ package com.tangem.domain.walletconnect.usecase.ethereum import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase -interface WcPersonalEthSignUseCase : +interface WcEthMessageSignUseCase : WcSignUseCase, - WcSignUseCase.FinalAction, - WcSignUseCase.SimpleRun { + WcSignUseCase.SimpleRun { data class SignModel( val humanMsg: String, diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthTransactionUseCase.kt new file mode 100644 index 0000000000..7df50defdc --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthTransactionUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.walletconnect.usecase.ethereum + +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase + +interface WcEthSendTransactionUseCase : + WcSignUseCase, + WcSignUseCase.SimpleRun { + + fun updateFee(fee: TransactionFee) +} + +interface WcEthSignTransactionUseCase : + WcSignUseCase, + WcSignUseCase.SimpleRun { + + fun updateFee(fee: TransactionFee) +} + +data class WcEthTransaction( + val isFeeByDap: Boolean, + val fee: TransactionFee, +) \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt deleted file mode 100644 index 07edd2b5f8..0000000000 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.domain.walletconnect.usecase.pair - -import arrow.core.Either -import com.tangem.domain.walletconnect.model.WcPairError -import com.tangem.domain.walletconnect.model.WcSession -import com.tangem.domain.walletconnect.model.WcSessionApprove -import com.tangem.domain.walletconnect.model.WcSessionProposal - -sealed interface WcPairState { - data object Loading : WcPairState - data class Error(val error: WcPairError) : WcPairState - data class Proposal(val dAppSession: WcSessionProposal) : WcPairState - - sealed interface Approving : WcPairState { - val session: WcSessionApprove - - data class Loading(override val session: WcSessionApprove) : Approving - data class Result( - override val session: WcSessionApprove, - val result: Either, - ) : Approving - } -} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt index 2de6663dbd..b926213140 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt @@ -1,6 +1,10 @@ package com.tangem.domain.walletconnect.usecase.pair +import arrow.core.Either +import com.tangem.domain.walletconnect.model.WcPairError +import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove +import com.tangem.domain.walletconnect.model.WcSessionProposal import kotlinx.coroutines.flow.Flow interface WcPairUseCase { @@ -11,4 +15,20 @@ interface WcPairUseCase { fun reject() enum class Source { QR, DEEPLINK, CLIPBOARD, ETC } +} + +sealed interface WcPairState { + data object Loading : WcPairState + data class Error(val error: WcPairError) : WcPairState + data class Proposal(val dAppSession: WcSessionProposal) : WcPairState + + sealed interface Approving : WcPairState { + val session: WcSessionApprove + + data class Loading(override val session: WcSessionApprove) : Approving + data class Result( + override val session: WcSessionApprove, + val result: Either, + ) : Approving + } } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignState.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignState.kt deleted file mode 100644 index 047148fade..0000000000 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.walletconnect.usecase.sign - -import arrow.core.Either - -data class WcSignState( - val signModel: SignModel, - val domainStep: WcSignStep, -) - -sealed interface WcSignStep { - data object PreSign : WcSignStep - data object Signing : WcSignStep - data class Result(val result: Either) : WcSignStep -} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignUseCase.kt index a0db5aa0e7..f011b82087 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/sign/WcSignUseCase.kt @@ -1,14 +1,13 @@ package com.tangem.domain.walletconnect.usecase.sign +import arrow.core.Either import com.tangem.domain.walletconnect.usecase.WcMethodUseCase import kotlinx.coroutines.flow.Flow interface WcSignUseCase : WcMethodUseCase { - interface FinalAction { - fun cancel() - fun sign() - } + fun cancel() + fun sign() interface SimpleRun { operator fun invoke(): Flow> @@ -17,4 +16,15 @@ interface WcSignUseCase : WcMethodUseCase { interface ArgsRun { operator fun invoke(args: Args): Flow> } +} + +data class WcSignState( + val signModel: SignModel, + val domainStep: WcSignStep, +) + +sealed interface WcSignStep { + data object PreSign : WcSignStep + data object Signing : WcSignStep + data class Result(val result: Either) : WcSignStep } \ No newline at end of file From c9b99c587d075c4ffe7e0bd2f647f2f6ad2f6ccd Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Apr 2025 09:17:35 +0300 Subject: [PATCH 65/72] Updated on 2026-08-14 --- .../MockYieldBalanceWrapperDTOFactory.kt | 8 + .../multi/DefaultMultiYieldBalanceProducer.kt | 44 ++++ .../DefaultMultiYieldBalanceProducerTest.kt | 190 ++++++++++++++++++ .../multi/MultiYieldBalanceProducer.kt | 17 ++ .../multi/MultiYieldBalanceSupplier.kt | 18 ++ 5 files changed, 277 insertions(+) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt index 521604e487..71ad65f3d2 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt @@ -48,4 +48,12 @@ object MockYieldBalanceWrapperDTOFactory { integrationId = stakingId.integrationId, ) } + + fun createWithEmptyBalance(stakingId: StakingID = defaultStakingId): YieldBalanceWrapperDTO { + return YieldBalanceWrapperDTO( + addresses = Address(address = stakingId.address), + balances = emptyList(), + integrationId = stakingId.integrationId, + ) + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt new file mode 100644 index 0000000000..533610817a --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt @@ -0,0 +1,44 @@ +package com.tangem.data.staking.multi + +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.multi.MultiYieldBalanceProducer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEmpty + +/** + * Default implementation of [MultiYieldBalanceProducer] + * + * @property params params + * @property yieldsBalancesStore yields balances store + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultMultiYieldBalanceProducer @AssistedInject constructor( + @Assisted val params: MultiYieldBalanceProducer.Params, + private val yieldsBalancesStore: YieldsBalancesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : MultiYieldBalanceProducer { + + override val fallback: Set + get() = setOf() + + override fun produce(): Flow> { + return yieldsBalancesStore.get(userWalletId = params.userWalletId) + .distinctUntilChanged() + .onEmpty { emit(value = hashSetOf()) } + .flowOn(dispatchers.default) + } + + @AssistedFactory + interface Factory : MultiYieldBalanceProducer.Factory { + override fun create(params: MultiYieldBalanceProducer.Params): DefaultMultiYieldBalanceProducer + } +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt new file mode 100644 index 0000000000..a545c87d92 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt @@ -0,0 +1,190 @@ +package com.tangem.data.staking.multi + +import com.google.common.truth.Truth +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.toDomain +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.multi.MultiYieldBalanceProducer +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiYieldBalanceProducerTest { + + private val params = MultiYieldBalanceProducer.Params(userWalletId = UserWalletId("011")) + + private val yieldsBalancesStore = mockk() + private val dispatchers = TestingCoroutineDispatcherProvider() + + private val producer = DefaultMultiYieldBalanceProducer( + params = params, + yieldsBalancesStore = yieldsBalancesStore, + dispatchers = dispatchers, + ) + + @Test + fun `test that flow is mapped for user wallet id from params`() = runTest { + val balances = setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + + val networksStatusesFlow = flowOf(balances) + + every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + + val actual = producer.produce() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values.first()).isEqualTo(balances) + } + + @Test + fun `test that flow is updated if balances are updated`() = runTest { + val networksStatusesFlow = MutableSharedFlow>(replay = 2) + + every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + + val actual = producer.produce() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + + // first emit + val balances = setOf( + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(), + ) + + networksStatusesFlow.emit(balances) + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first()).isEqualTo(balances) + + // second emit + val updatedWrappers = setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + + networksStatusesFlow.emit(updatedWrappers) + + val values2 = getEmittedValues(flow = actual) + + val expected = listOf(balances, updatedWrappers) + Truth.assertThat(values2.size).isEqualTo(2) + Truth.assertThat(values2).isEqualTo(expected) + } + + @Test + fun `test that flow is filtered the same balance`() = runTest { + val networksStatusesFlow = MutableSharedFlow>(replay = 2) + + every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + + val actual = producer.produce() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + + // first emit + val wrappers = setOf( + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(), + ) + + networksStatusesFlow.emit(wrappers) + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first()).isEqualTo(wrappers) + + // second emit + networksStatusesFlow.emit(wrappers) + + val values2 = getEmittedValues(flow = actual) + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2.first()).isEqualTo(wrappers) + } + + @Test + fun `test if flow throws exception`() = runTest { + val exception = IllegalStateException() + val balances = setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + + val innerFlow = MutableStateFlow(value = false) + val networksStatusesFlow = flow { + if (innerFlow.value) { + emit(balances) + } else { + throw exception + } + } + .buffer(capacity = 5) + + every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + + val actual = producer.produceWithFallback() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(emptySet())) + + innerFlow.emit(value = true) + + val values2 = getEmittedValues(flow = actual) + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(balances)) + } + + @Test + fun `test that flow is empty`() = runTest { + every { yieldsBalancesStore.get(params.userWalletId) } returns emptyFlow() + + val actual = producer.produce() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) + } + + private companion object { + + val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId + val solanaId = YieldsBalancesStore.StakingID( + integrationId = "solana-sol-native-multivalidator-staking", + address = "0x1", + ) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt new file mode 100644 index 0000000000..eaaff5ad56 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.staking.multi + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Producer of all yield balances for selected wallet [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface MultiYieldBalanceProducer : FlowProducer> { + + data class Params(val userWalletId: UserWalletId) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt new file mode 100644 index 0000000000..4c6cbed1f3 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking.multi + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.staking.model.stakekit.YieldBalance + +/** + * Supplier of all yield balances for selected wallet [MultiYieldBalanceProducer.Params] + * + * @property factory factory for creating [MultiYieldBalanceProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class MultiYieldBalanceSupplier( + override val factory: FlowProducer.Factory, + override val keyCreator: (MultiYieldBalanceProducer.Params) -> String, +) : FlowCachingSupplier>() \ No newline at end of file From 7a8f55b5895054304d8f52586a9331b4c1ed1015 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Apr 2025 22:10:17 +0300 Subject: [PATCH 66/72] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + build.gradle.kts | 36 +++++++++++++++++-- core/datasource/build.gradle.kts | 1 + .../datasource/api/common/config/ApiConfig.kt | 1 + .../datasource/api/common/config/Express.kt | 1 + .../managers/ProdApiConfigsManagerTest.kt | 3 ++ .../extension/AppExtensionConfigurations.kt | 10 +++--- .../plugin/configuration/model/BuildType.kt | 21 +++++++++++ 8 files changed, 68 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c2091e3c79..700a9a7754 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -320,6 +320,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) mockedImplementation(deps.chuckerStub) + externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/build.gradle.kts b/build.gradle.kts index 1b240b2cfd..4cbeef6686 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,9 +59,41 @@ val assembleInternalQA by tasks.registering { } } +val assembleExternalQA by tasks.registering { + group = "build" + description = "Builds external APK to 'build/outputs' directory" + + val appOutputApkDir = "$projectDir/app/build/outputs/apk/external" + val rootOutputApkDir = "$buildDir/outputs" + val injected = objects.newInstance() + + dependsOn(":app:assembleExternal") + + doFirst { + injected.fs.delete { + delete(appOutputApkDir) + delete("$rootOutputApkDir/app-external.apk") + } + } + doLast { + injected.fs.copy { + from("$appOutputApkDir/app-external.apk") + into(rootOutputApkDir) + } + } +} + +val assembleQA by tasks.registering { + group = "build" + description = "Builds internal and external APKs to 'build/outputs' directory" + + dependsOn(assembleInternalQA) + dependsOn(assembleExternalQA) +} + val generateComposeMetrics by tasks.registering { group = "other" - description = "Build internal APK and generates compose metrics to 'build/compose-metrics' directory" + description = "Build external APK and generates compose metrics to 'build/compose-metrics' directory" subprojects { tasks.withType { @@ -86,5 +118,5 @@ val generateComposeMetrics by tasks.registering { } } - finalizedBy(assembleInternalQA) + finalizedBy(assembleExternalQA) } \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 0efd84702a..ebde9b43cd 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -72,6 +72,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) mockedImplementation(deps.chuckerStub) + externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) 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 66924d6e19..88c03b6435 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 @@ -44,6 +44,7 @@ sealed class ApiConfig { internal const val DEBUG_BUILD_TYPE = "debug" internal const val INTERNAL_BUILD_TYPE = "internal" internal const val MOCKED_BUILD_TYPE = "mocked" + internal const val EXTERNAL_BUILD_TYPE = "external" internal const val RELEASE_BUILD_TYPE = "release" } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index 3ad70b9000..3babcef973 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -73,6 +73,7 @@ internal class Express( INTERNAL_BUILD_TYPE, MOCKED_BUILD_TYPE, -> ApiEnvironment.STAGE + EXTERNAL_BUILD_TYPE, RELEASE_BUILD_TYPE, -> ApiEnvironment.PROD else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") 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 d4158d4965..d9f1787025 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 @@ -6,6 +6,7 @@ import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.config.* import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_BUILD_TYPE +import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE @@ -100,6 +101,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { INTERNAL_BUILD_TYPE, MOCKED_BUILD_TYPE, -> ApiEnvironment.STAGE + EXTERNAL_BUILD_TYPE, RELEASE_BUILD_TYPE, -> ApiEnvironment.PROD else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") @@ -115,6 +117,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { INTERNAL_BUILD_TYPE, MOCKED_BUILD_TYPE, -> "[REDACTED_ENV_URL]" + EXTERNAL_BUILD_TYPE, RELEASE_BUILD_TYPE, -> "https://express.tangem.com/v1/" else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") 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 89c5d2f72d..03d2b74d0c 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 @@ -69,17 +69,19 @@ private fun AppExtension.configureBuildTypes() { private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, buildType: BuildType) { when (buildType) { + BuildType.Release -> { + isDebuggable = false + } BuildType.Debug -> { isDebuggable = true } - BuildType.Internal -> { + BuildType.Internal, + BuildType.External + -> { initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) } - BuildType.Release -> { - isDebuggable = false - } BuildType.Mocked -> { initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 485c960dcb..bac38b3402 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -78,6 +78,27 @@ internal enum class BuildType( ), ), + /** + * Build type for QA and business + * + * Features: + * - Env: prod + * - Signing config: debug + * - Proguard + * */ + External( + id = "external", + appIdSuffix = "external", + versionSuffix = "external", + configFields = listOf( + BuildConfigField.Environment(value = "prod"), + BuildConfigField.TestActionEnabled(isEnabled = false), + BuildConfigField.LogEnabled(isEnabled = false), + BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.MockDataSource(isEnabled = false), + ), + ), + /** * Production ready build type for clients * From 2af08f578c803f663221de9a5d553c9dae00ce57 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Apr 2025 12:47:25 +0400 Subject: [PATCH 67/72] Updated on 2026-08-14 --- .../api/common/blockaid/BlockAidApi.kt | 21 +++ .../models/request/DomainScanRequest.kt | 9 + .../request/EvmTransactionScanRequest.kt | 22 +++ .../request/SolanaTransactionScanRequest.kt | 16 ++ .../models/response/AccountSummaryResponse.kt | 10 ++ .../blockaid/models/response/AssetDiff.kt | 25 +++ .../models/response/DomainScanResponse.kt | 10 ++ .../blockaid/models/response/Exposure.kt | 22 +++ .../models/response/SimulationResponse.kt | 10 ++ .../models/response/TransactionMetadata.kt | 9 + .../response/TransactionScanResponse.kt | 10 ++ .../models/response/ValidationResponse.kt | 10 ++ .../datasource/api/common/config/ApiConfig.kt | 2 + .../datasource/api/common/config/BlockAid.kt | 27 +++ .../tangem/datasource/di/ApiConfigsModule.kt | 6 + .../com/tangem/datasource/di/NetworkModule.kt | 24 +++ .../config/environment/EnvironmentConfig.kt | 1 + .../converter/EnvironmentConfigConverter.kt | 1 + .../models/EnvironmentConfigModel.kt | 1 + .../managers/ProdApiConfigsManagerTest.kt | 11 ++ data/blockaid/.gitignore | 1 + data/blockaid/build.gradle.kts | 41 +++++ .../tangem/data/blockaid/BlockAidMapper.kt | 128 +++++++++++++++ .../data/blockaid/BlockAidRepository.kt | 13 ++ .../blockaid/DefaultBlockAidRepository.kt | 40 +++++ .../data/blockaid/DefaultBlockAidVerifier.kt | 31 ++++ .../blockaid/di/BlockAidDataInternalModule.kt | 18 ++ .../data/blockaid/di/BlockAidDataModule.kt | 18 ++ .../data/blockaid/BlockAidMapperTest.kt | 155 ++++++++++++++++++ .../blockaid/DefaultBlockAidRepositoryTest.kt | 122 ++++++++++++++ domain/blockaid/.gitignore | 1 + domain/blockaid/build.gradle.kts | 13 ++ domain/blockaid/models/.gitignore | 1 + domain/blockaid/models/build.gradle.kts | 10 ++ .../blockaid/models/dapp/CheckDAppResult.kt | 22 +++ .../domain/blockaid/models/dapp/DAppData.kt | 11 ++ .../transaction/CheckTransactionResult.kt | 12 ++ .../models/transaction/SimulationResult.kt | 21 +++ .../models/transaction/TransactionData.kt | 36 ++++ .../models/transaction/ValidationResult.kt | 22 +++ .../transaction/simultation/AmountInfo.kt | 8 + .../transaction/simultation/ApprovedAmount.kt | 9 + .../transaction/simultation/SimulationData.kt | 22 +++ .../transaction/simultation/TokenInfo.kt | 7 + .../domain/blockaid/BlockAidVerifier.kt | 23 +++ settings.gradle.kts | 3 + 46 files changed, 1035 insertions(+) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/DomainScanRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AccountSummaryResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/DomainScanResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SimulationResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionMetadata.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionScanResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt create mode 100644 data/blockaid/.gitignore create mode 100644 data/blockaid/build.gradle.kts create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidVerifier.kt create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataInternalModule.kt create mode 100644 data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt create mode 100644 data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt create mode 100644 data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt create mode 100644 domain/blockaid/.gitignore create mode 100644 domain/blockaid/build.gradle.kts create mode 100644 domain/blockaid/models/.gitignore create mode 100644 domain/blockaid/models/build.gradle.kts create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/CheckDAppResult.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/DAppData.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/SimulationResult.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/ValidationResult.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/AmountInfo.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt create mode 100644 domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidVerifier.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt new file mode 100644 index 0000000000..605bc8a391 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.api.common.blockaid + +import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest +import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest +import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest +import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse +import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface BlockAidApi { + + @POST("site/scan") + suspend fun scanDomain(@Body request: DomainScanRequest): DomainScanResponse + + @POST("evm/json-rpc/scan") + suspend fun scanJsonRpc(@Body request: EvmTransactionScanRequest): TransactionScanResponse + + @POST("solana/message/scan") + suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): TransactionScanResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/DomainScanRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/DomainScanRequest.kt new file mode 100644 index 0000000000..e88eca28ed --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/DomainScanRequest.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.common.blockaid.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DomainScanRequest( + @Json(name = "url") val url: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt new file mode 100644 index 0000000000..fd5aeecfaf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.api.common.blockaid.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata + +@JsonClass(generateAdapter = true) +data class EvmTransactionScanRequest( + @Json(name = "chain") val chain: String, + @Json(name = "account_address") val accountAddress: String, + @Json(name = "method") val method: String, + @Json(name = "data") val data: RpcData, + @Json(name = "options") val options: List = listOf("simulation", "validation"), + @Json(name = "metadata") val metadata: TransactionMetadata, +) + +@JsonClass(generateAdapter = true) +data class RpcData( + @Json(name = "jsonrpc") val jsonrpc: String = "2.0", + @Json(name = "method") val method: String, + @Json(name = "params") val params: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt new file mode 100644 index 0000000000..72ec506f01 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.common.blockaid.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata + +@JsonClass(generateAdapter = true) +data class SolanaTransactionScanRequest( + @Json(name = "encoding") val encoding: String = "base64", + @Json(name = "chain") val chain: String, + @Json(name = "method") val method: String, + @Json(name = "options") val options: List = listOf("simulation, validation"), + @Json(name = "metadata") val metadata: TransactionMetadata, + @Json(name = "account_address") val accountAddress: String, + @Json(name = "transactions") val transactions: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AccountSummaryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AccountSummaryResponse.kt new file mode 100644 index 0000000000..e0188fa7dc --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AccountSummaryResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AccountSummaryResponse( + @Json(name = "assets_diffs") val assetsDiffs: List, + @Json(name = "exposures") val exposures: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt new file mode 100644 index 0000000000..645409ec89 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AssetDiff( + @Json(name = "asset_type") val assetType: String, + @Json(name = "asset") val asset: Asset, + @Json(name = "in") val inTransfer: List? = null, + @Json(name = "out") val outTransfer: List? = null, +) + +@JsonClass(generateAdapter = true) +data class Asset( + @Json(name = "chain_id") val chainId: Int? = null, + @Json(name = "logo_url") val logoUrl: String? = null, + @Json(name = "symbol") val symbol: String, +) + +@JsonClass(generateAdapter = true) +data class Transfer( + @Json(name = "value") val value: String, + @Json(name = "raw_value") val rawValue: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/DomainScanResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/DomainScanResponse.kt new file mode 100644 index 0000000000..b11e7a6c88 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/DomainScanResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DomainScanResponse( + @Json(name = "status") val status: String, + @Json(name = "is_malicious") val isMalicious: Boolean?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt new file mode 100644 index 0000000000..90038d41c4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Exposure( + @Json(name = "asset") val asset: Asset, + @Json(name = "spenders") val spenders: Map, +) + +@JsonClass(generateAdapter = true) +data class SpenderDetails( + @Json(name = "exposure") val exposure: List, + @Json(name = "is_approved_for_all") val isApprovedForAll: Boolean? = null, +) + +@JsonClass(generateAdapter = true) +data class ExposureDetail( + @Json(name = "value") val value: String, + @Json(name = "raw_value") val rawValue: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SimulationResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SimulationResponse.kt new file mode 100644 index 0000000000..a62c20d307 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SimulationResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SimulationResponse( + @Json(name = "status") val status: String, + @Json(name = "account_summary") val accountSummary: AccountSummaryResponse, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionMetadata.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionMetadata.kt new file mode 100644 index 0000000000..13f229ead5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionMetadata.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TransactionMetadata( + @Json(name = "domain") val domain: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionScanResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionScanResponse.kt new file mode 100644 index 0000000000..9d45809013 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/TransactionScanResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TransactionScanResponse( + @Json(name = "validation") val validation: ValidationResponse, + @Json(name = "simulation") val simulation: SimulationResponse, +) \ No newline at end of file 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 new file mode 100644 index 0000000000..08e86a0697 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ValidationResponse( + @Json(name = "status") val status: String, + @Json(name = "result_type") val resultType: String, +) \ No newline at end of file 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 88c03b6435..78db8380c3 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 @@ -27,6 +27,7 @@ sealed class ApiConfig { TangemVisaAuth, TangemVisa, TangemCardSdk, + BlockAid, } private fun initializeId(): ID { @@ -37,6 +38,7 @@ sealed class ApiConfig { is TangemVisaAuth -> ID.TangemVisaAuth is TangemVisa -> ID.TangemVisa is TangemCardSdk -> ID.TangemCardSdk + is BlockAid -> ID.BlockAid } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt new file mode 100644 index 0000000000..008cd9ef09 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt @@ -0,0 +1,27 @@ +package com.tangem.datasource.api.common.config + +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.utils.ProviderSuspend + +internal class BlockAid( + private val environmentConfigStorage: EnvironmentConfigStorage, +) : ApiConfig() { + + override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD + + override val environmentConfigs = listOf( + createProdEnvironment(), + ) + + private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.blockaid.io/v0/", + headers = buildMap { + environmentConfigStorage.getConfigSync().blockAidApiKey?.let { apiKey -> + put("X-API-KEY", ProviderSuspend { apiKey }) + } + put("accept", ProviderSuspend { "application/json" }) + put("content-type", ProviderSuspend { "application/json" }) + }, + ) +} \ No newline at end of file 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 5035774b7f..1a90974c75 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 @@ -46,6 +46,12 @@ internal object ApiConfigsModule { @IntoSet fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider) + @Provides + @IntoSet + fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig { + return BlockAid(environmentConfigStorage) + } + @Provides @IntoSet fun provideTangemCardSdkConfig(): ApiConfig = TangemCardSdk() 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 dd14d5f63a..e05ca4cf1a 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 @@ -4,6 +4,7 @@ import android.content.Context import com.squareup.moshi.Moshi import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.datasource.BuildConfig +import com.tangem.datasource.api.common.blockaid.BlockAidApi import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.managers.ApiConfigsManager @@ -282,6 +283,29 @@ internal object NetworkModule { .create(T::class.java) } + @Provides + @Singleton + fun provideBlockAidApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, + apiConfigsManager: ApiConfigsManager, + appLogsStore: AppLogsStore, + ): BlockAidApi { + return createApi( + id = ApiConfig.ID.BlockAid, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, + clientBuilder = { + addInterceptor( + NetworkLogsSaveInterceptor(appLogsStore), + ).applyTimeoutAnnotations() + }, + ) + } + private inline fun createApi( id: ApiConfig.ID, moshi: Moshi, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt index d4bc388d6d..9da0b616fc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt @@ -14,4 +14,5 @@ data class EnvironmentConfig( val express: ExpressModel? = null, val devExpress: ExpressModel? = null, val stakeKitApiKey: String? = null, + val blockAidApiKey: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt index 02d7967f47..80f41f68b3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt @@ -23,6 +23,7 @@ internal object EnvironmentConfigConverter : Converter createVisaAuthModel() is TangemVisa -> createVisaModel() is TangemCardSdk -> createTangemCardSdkModel() + is BlockAid -> createBlockAidSdkModel() } } @@ -218,6 +219,16 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { ) } + private fun createBlockAidSdkModel(): Model { + return Model( + id = ApiConfig.ID.BlockAid, + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.blockaid.io/v0/", + ), + ) + } + private fun String.checkHeaderValueOrEmpty(): String { for (i in this.indices) { val c = this[i] diff --git a/data/blockaid/.gitignore b/data/blockaid/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/blockaid/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/blockaid/build.gradle.kts b/data/blockaid/build.gradle.kts new file mode 100644 index 0000000000..c1855c991d --- /dev/null +++ b/data/blockaid/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.android.library) + id("configuration") +} + +android { + namespace = "com.tangem.data.blockaid" +} + +dependencies { + /* Project - Domain */ + implementation(projects.data.common) + implementation(projects.domain.blockaid) + implementation(projects.domain.blockaid.models) + + /* Project - Data */ + implementation(projects.core.datasource) + + /* Project - Core */ + implementation(projects.core.utils) + + /* DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + + /* Tangem libraries */ + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) + + /* Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + + /* Tests */ + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.turbine) +} \ 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 new file mode 100644 index 0000000000..5336bf9657 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt @@ -0,0 +1,128 @@ +package com.tangem.data.blockaid + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.transaction.* +import com.domain.blockaid.models.transaction.simultation.ApprovedAmount +import com.domain.blockaid.models.transaction.simultation.SimulationData +import com.domain.blockaid.models.transaction.simultation.TokenInfo +import com.domain.blockaid.models.transaction.simultation.AmountInfo +import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest +import com.tangem.datasource.api.common.blockaid.models.request.RpcData +import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest +import com.tangem.datasource.api.common.blockaid.models.response.* +import javax.inject.Inject + +private const val SUCCESS_STATUS = "Success" +private const val DOMAIN_CHECKED_STATUS = "hit" +private const val VALIDATION_SAFE_STATUS = "Benign" + +internal class BlockAidMapper @Inject constructor() { + + fun mapToDomain(from: DomainScanResponse): CheckDAppResult { + return when { + from.status != DOMAIN_CHECKED_STATUS -> CheckDAppResult.FAILED_TO_VERIFY + from.isMalicious == true -> CheckDAppResult.UNSAFE + else -> CheckDAppResult.SAFE + } + } + + fun mapToDomain(from: TransactionScanResponse): CheckTransactionResult { + return CheckTransactionResult( + validation = when { + from.validation.status != SUCCESS_STATUS -> ValidationResult.FAILED_TO_VALIDATE + from.validation.resultType == VALIDATION_SAFE_STATUS -> ValidationResult.SAFE + else -> ValidationResult.UNSAFE + }, + simulation = if (from.simulation.status != SUCCESS_STATUS) { + SimulationResult.FailedToSimulate + } else { + mapSimulationSuccessResult(from.simulation.accountSummary) + }, + ) + } + + fun mapToEvmRequest(from: TransactionData): EvmTransactionScanRequest { + return EvmTransactionScanRequest( + chain = from.chain, + accountAddress = from.accountAddress, + method = from.method, + data = RpcData( + method = from.method, + params = (from.params as TransactionParams.Evm).params, + ), + metadata = TransactionMetadata(from.domainUrl), + ) + } + + fun mapToSolanaRequest(from: TransactionData): SolanaTransactionScanRequest { + return SolanaTransactionScanRequest( + chain = from.chain, + accountAddress = from.accountAddress, + metadata = TransactionMetadata(from.domainUrl), + method = from.method, + transactions = (from.params as TransactionParams.Solana).transactions, + ) + } + + private fun mapSimulationSuccessResult(from: AccountSummaryResponse): SimulationResult { + return when { + from.assetsDiffs.isEmpty() && from.exposures.isNotEmpty() -> mapApproveTransaction(from.exposures) + from.assetsDiffs.isNotEmpty() && from.exposures.isEmpty() -> mapSendReceiveTransaction(from.assetsDiffs) + else -> SimulationResult.FailedToSimulate + } + } + + private fun mapApproveTransaction(exposures: List): SimulationResult { + val amounts = exposures.flatMap { exposure -> + val tokenInfo = TokenInfo( + chainId = exposure.asset.chainId, + logoUrl = exposure.asset.logoUrl, + symbol = exposure.asset.symbol, + ) + exposure.spenders.flatMap { (_, spender) -> + val isUnlimited = spender.isApprovedForAll == true + spender.exposure.mapNotNull { detail -> + ApprovedAmount( + approvedAmount = detail.value.toBigDecimalOrNull() ?: return@mapNotNull null, + isUnlimited = isUnlimited, + tokenInfo = tokenInfo, + ) + } + } + } + return if (amounts.isNotEmpty()) { + SimulationResult.Success(SimulationData.Approve(amounts)) + } else { + SimulationResult.FailedToSimulate + } + } + + private fun mapSendReceiveTransaction(assetDiffs: List): SimulationResult { + val sendInfo = arrayListOf() + val receiveInfo = arrayListOf() + + assetDiffs.forEach { diff -> + val token = TokenInfo( + chainId = diff.asset.chainId, + logoUrl = diff.asset.logoUrl, + symbol = diff.asset.symbol, + ) + diff.outTransfer.orEmpty().forEach { transfer -> + transfer.value.toBigDecimalOrNull()?.let { amount -> + sendInfo.add(AmountInfo(amount = amount, token = token)) + } + } + diff.inTransfer.orEmpty().forEach { transfer -> + transfer.value.toBigDecimalOrNull()?.let { amount -> + receiveInfo.add(AmountInfo(amount = amount, token = token)) + } + } + } + + return if (sendInfo.isNotEmpty() || receiveInfo.isNotEmpty()) { + SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = receiveInfo)) + } else { + SimulationResult.FailedToSimulate + } + } +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt new file mode 100644 index 0000000000..57e8db2559 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.data.blockaid + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData + +interface BlockAidRepository { + + suspend fun verifyDAppDomain(data: DAppData): CheckDAppResult + + suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt new file mode 100644 index 0000000000..837b699192 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt @@ -0,0 +1,40 @@ +package com.tangem.data.blockaid + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData +import com.domain.blockaid.models.transaction.TransactionParams +import com.tangem.datasource.api.common.blockaid.BlockAidApi +import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultBlockAidRepository @Inject constructor( + private val api: BlockAidApi, + private val dispatcherProvider: CoroutineDispatcherProvider, + private val mapper: BlockAidMapper, +) : BlockAidRepository { + + override suspend fun verifyDAppDomain(data: DAppData): CheckDAppResult { + val response = withContext(dispatcherProvider.io) { + api.scanDomain(DomainScanRequest(data.url)) + } + return mapper.mapToDomain(response) + } + + override suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult { + val response = withContext(dispatcherProvider.io) { + when (data.params) { + is TransactionParams.Evm -> { + api.scanJsonRpc(mapper.mapToEvmRequest(data)) + } + is TransactionParams.Solana -> { + api.scanSolanaMessage(mapper.mapToSolanaRequest(data)) + } + } + } + return mapper.mapToDomain(response) + } +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidVerifier.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidVerifier.kt new file mode 100644 index 0000000000..95d5070701 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidVerifier.kt @@ -0,0 +1,31 @@ +package com.tangem.data.blockaid + +import arrow.core.Either +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData +import com.tangem.domain.blockaid.BlockAidVerifier +import javax.inject.Inject + +/** + * Verifies the safety of DApps and WalletConnect transactions + */ +class DefaultBlockAidVerifier @Inject constructor( + private val repository: BlockAidRepository, +) : BlockAidVerifier { + + /** + * Checks if a DApp is safe to use + */ + override suspend fun verifyDApp(data: DAppData): Either { + return Either.catch { repository.verifyDAppDomain(data) } + } + + /** + * Checks the safety of a WalletConnect transaction and provides a simulation result + */ + override suspend fun verifyTransaction(data: TransactionData): Either { + return Either.catch { repository.verifyTransaction(data) } + } +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataInternalModule.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataInternalModule.kt new file mode 100644 index 0000000000..894e9aca12 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataInternalModule.kt @@ -0,0 +1,18 @@ +package com.tangem.data.blockaid.di + +import com.tangem.data.blockaid.BlockAidRepository +import com.tangem.data.blockaid.DefaultBlockAidRepository +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 BlockAidDataInternalModule { + + @Binds + @Singleton + fun bindRepository(repository: DefaultBlockAidRepository): BlockAidRepository +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt new file mode 100644 index 0000000000..1efc366513 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt @@ -0,0 +1,18 @@ +package com.tangem.data.blockaid.di + +import com.tangem.data.blockaid.DefaultBlockAidVerifier +import com.tangem.domain.blockaid.BlockAidVerifier +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface BlockAidDataModule { + + @Binds + @Singleton + fun bindVerifier(verifier: DefaultBlockAidVerifier): BlockAidVerifier +} \ No newline at end of file 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 new file mode 100644 index 0000000000..ac9639b167 --- /dev/null +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt @@ -0,0 +1,155 @@ +package com.tangem.data.blockaid + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.transaction.SimulationResult +import com.domain.blockaid.models.transaction.ValidationResult +import com.domain.blockaid.models.transaction.simultation.SimulationData +import com.tangem.datasource.api.common.blockaid.models.response.* +import org.junit.Assert.* +import org.junit.Test +import java.math.BigDecimal + +class BlockAidMapperTest { + + private val mapper = BlockAidMapper() + + @Test + fun whenStatusHitAndIsMaliciousFalseThenMapToDomainReturnsSafe() { + val response = DomainScanResponse(status = "hit", isMalicious = false) + val result = mapper.mapToDomain(response) + assertEquals(CheckDAppResult.SAFE, result) + } + + @Test + fun whenStatusHitAndIsMaliciousTrueThenMapToDomainReturnsUnsafe() { + val response = DomainScanResponse(status = "hit", isMalicious = true) + val result = mapper.mapToDomain(response) + assertEquals(CheckDAppResult.UNSAFE, result) + } + + @Test + fun whenStatusNotHitThenMapToDomainReturnsFailedToVerify() { + val response = DomainScanResponse(status = "miss", isMalicious = false) + val result = mapper.mapToDomain(response) + assertEquals(CheckDAppResult.FAILED_TO_VERIFY, result) + } + + @Test + fun whenResponseBenignValidationThenReturnsSafeValidation() { + val spenderDetails = SpenderDetails( + isApprovedForAll = true, + exposure = listOf(ExposureDetail(value = "1000.0", rawValue = "0x123")), + ) + val exposure = Exposure( + asset = Asset(chainId = 1, logoUrl = "logo", symbol = "PEPE"), + spenders = mapOf("spender" to spenderDetails), + ) + val response = TransactionScanResponse( + validation = ValidationResponse(status = "Success", resultType = "Benign"), + simulation = SimulationResponse( + status = "Success", + accountSummary = AccountSummaryResponse(assetsDiffs = emptyList(), exposures = listOf(exposure)), + ), + ) + + val result = mapper.mapToDomain(response) + assertEquals(ValidationResult.SAFE, result.validation) + + val simulation = result.simulation as? SimulationResult.Success + assertNotNull(simulation) + + val approve = simulation?.data as? SimulationData.Approve + assertNotNull(approve) + assertEquals(1, approve?.approvedAmounts?.size) + assertEquals(BigDecimal("1000.0"), approve?.approvedAmounts?.first()?.approvedAmount) + assertTrue(approve?.approvedAmounts?.first()?.isUnlimited == true) + } + + @Test + fun whenResponseBenignValidationAndSuccessSimulationThenReturnsSendReceiveResult() { + val assetDiff = AssetDiff( + assetType = "ERC20", + asset = Asset(chainId = 1, logoUrl = "logo", symbol = "ETH"), + inTransfer = listOf(Transfer(value = "2.0", rawValue = "0x1")), + outTransfer = listOf(Transfer(value = "1.5", rawValue = "0x2")), + ) + val response = TransactionScanResponse( + validation = ValidationResponse(status = "Success", resultType = "Benign"), + simulation = SimulationResponse( + status = "Success", + accountSummary = AccountSummaryResponse(exposures = emptyList(), assetsDiffs = listOf(assetDiff)), + ), + ) + + val result = mapper.mapToDomain(response) + assertEquals(ValidationResult.SAFE, result.validation) + + val simulation = result.simulation as? SimulationResult.Success + assertNotNull(simulation) + + val data = simulation?.data as? SimulationData.SendAndReceive + assertNotNull(data) + assertEquals(BigDecimal("1.5"), data?.send?.first()?.amount) + assertEquals(BigDecimal("2.0"), data?.receive?.first()?.amount) + } + + @Test + fun whenResponseErrorValidationThenReturnsFailedToValidate() { + val response = TransactionScanResponse( + validation = ValidationResponse(status = "Error", resultType = "Benign"), + simulation = SimulationResponse( + status = "Success", + accountSummary = AccountSummaryResponse(emptyList(), emptyList()), + ), + ) + + val result = mapper.mapToDomain(response) + assertEquals(ValidationResult.FAILED_TO_VALIDATE, result.validation) + assertTrue(result.simulation is SimulationResult.FailedToSimulate) + } + + @Test + fun whenResponseNotBenignThenReturnsValidationUnsafe() { + val response = TransactionScanResponse( + validation = ValidationResponse(status = "Success", resultType = "Phishing"), + simulation = SimulationResponse( + status = "Success", + accountSummary = AccountSummaryResponse(emptyList(), emptyList()), + ), + ) + + val result = mapper.mapToDomain(response) + assertEquals(ValidationResult.UNSAFE, result.validation) + } + + @Test + fun whenResponseSimulationNotSuccessThenReturnsSimulationFailedToSimulate() { + val response = TransactionScanResponse( + validation = ValidationResponse(status = "Success", resultType = "Benign"), + simulation = SimulationResponse( + status = "Error", + accountSummary = AccountSummaryResponse(emptyList(), emptyList()), + ), + ) + + val result = mapper.mapToDomain(response) + assertTrue(result.simulation is SimulationResult.FailedToSimulate) + } + + @Test + fun whenResponseSimulationIsEmptyThenReturnsFailedToSimulate() { + val txResponse = TransactionScanResponse( + validation = ValidationResponse(status = "Success", resultType = "Benign"), + simulation = SimulationResponse( + status = "Success", + accountSummary = AccountSummaryResponse( + assetsDiffs = emptyList(), + exposures = emptyList(), + ), + ), + ) + + val result = mapper.mapToDomain(txResponse) + assertTrue(result.simulation is SimulationResult.FailedToSimulate) + } +} \ No newline at end of file diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt new file mode 100644 index 0000000000..bb919dbdb7 --- /dev/null +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt @@ -0,0 +1,122 @@ +package com.tangem.data.blockaid + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData +import com.domain.blockaid.models.transaction.TransactionParams +import com.tangem.datasource.api.common.blockaid.BlockAidApi +import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest +import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest +import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest +import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse +import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import io.mockk.* +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.* +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultBlockAidRepositoryTest { + + @MockK + private lateinit var api: BlockAidApi + + @MockK + private lateinit var mapper: BlockAidMapper + + @MockK + private lateinit var dispatcherProvider: CoroutineDispatcherProvider + + private lateinit var repository: DefaultBlockAidRepository + + private val testDispatcher = StandardTestDispatcher() + + @Before + fun setup() { + MockKAnnotations.init(this) + Dispatchers.setMain(testDispatcher) + every { dispatcherProvider.io } returns testDispatcher + repository = DefaultBlockAidRepository(api, dispatcherProvider, mapper) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun whenVerifyAppDomainThenCallsApiAndMapsResult() = runTest { + val url = "https://example.com" + val domainData = DAppData(url) + val domainResponse = DomainScanResponse(status = "hit", isMalicious = false) + val expectedResult = CheckDAppResult.SAFE + + coEvery { api.scanDomain(DomainScanRequest(url)) } returns domainResponse + every { mapper.mapToDomain(domainResponse) } returns expectedResult + + val result = repository.verifyDAppDomain(domainData) + + assertEquals(expectedResult, result) + coVerify { api.scanDomain(DomainScanRequest(url)) } + verify { mapper.mapToDomain(domainResponse) } + } + + @Test + fun whenVerifyEvmTransactionThenCallsScanJsonRpcAndMaps() = runTest { + val data = TransactionData( + chain = "ethereum", + accountAddress = "0xabc", + domainUrl = "https://uniswap.org", + method = "eth_sendTransaction", + params = TransactionParams.Evm(params = "some-params"), + ) + + val request = mockk() + val response = mockk() + val expectedResult = mockk() + + every { mapper.mapToEvmRequest(data) } returns request + coEvery { api.scanJsonRpc(request) } returns response + every { mapper.mapToDomain(response) } returns expectedResult + + val result = repository.verifyTransaction(data) + + assertEquals(expectedResult, result) + coVerify { api.scanJsonRpc(request) } + verify { mapper.mapToEvmRequest(data) } + verify { mapper.mapToDomain(response) } + } + + @Test + fun whenVerifySolanaTransactionThenCallsScanSolanaMessageAndMapsResult() = runTest { + val data = TransactionData( + chain = "mainnet", + accountAddress = "/Rd2TLl...", + domainUrl = "https://example.com", + method = "signTransaction", + params = TransactionParams.Solana(transactions = listOf("TX_PAYLOAD_BASE64")), + ) + + val request = mockk() + val response = mockk() + val expectedResult = mockk() + + every { mapper.mapToSolanaRequest(data) } returns request + coEvery { api.scanSolanaMessage(request) } returns response + every { mapper.mapToDomain(response) } returns expectedResult + + val result = repository.verifyTransaction(data) + + assertEquals(expectedResult, result) + coVerify { api.scanSolanaMessage(request) } + verify { mapper.mapToSolanaRequest(data) } + verify { mapper.mapToDomain(response) } + } +} \ No newline at end of file diff --git a/domain/blockaid/.gitignore b/domain/blockaid/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/blockaid/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/blockaid/build.gradle.kts b/domain/blockaid/build.gradle.kts new file mode 100644 index 0000000000..9fda0da981 --- /dev/null +++ b/domain/blockaid/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + /* Project - Domain */ + implementation(projects.domain.core) + implementation(projects.domain.blockaid.models) + + /* Other */ + implementation(deps.moshi.adapters) +} \ No newline at end of file diff --git a/domain/blockaid/models/.gitignore b/domain/blockaid/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/blockaid/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/blockaid/models/build.gradle.kts b/domain/blockaid/models/build.gradle.kts new file mode 100644 index 0000000000..14f878155b --- /dev/null +++ b/domain/blockaid/models/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.ksp) + id("configuration") +} +dependencies { + /* Other */ + implementation(deps.moshi) + ksp(deps.moshi.kotlin.codegen) +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/CheckDAppResult.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/CheckDAppResult.kt new file mode 100644 index 0000000000..480b3d8fd9 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/CheckDAppResult.kt @@ -0,0 +1,22 @@ +package com.domain.blockaid.models.dapp + +/** + * Result of BlockAid's DApp domain check + */ +enum class CheckDAppResult { + + /** + * DApp was confirmed safe + */ + SAFE, + + /** + * DApp was confirmed unsafe (known security risk) + */ + UNSAFE, + + /** + * Check wasn't performed, BlockAid cannot guarantee DApp's safety + */ + FAILED_TO_VERIFY, +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/DAppData.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/DAppData.kt new file mode 100644 index 0000000000..25d1882031 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/dapp/DAppData.kt @@ -0,0 +1,11 @@ +package com.domain.blockaid.models.dapp + +/** + * Data BlockAid needs to verify DApp domain + * + * @property url DApp's domain url + */ +@JvmInline +value class DAppData( + val url: String, +) \ No newline at end of file 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 new file mode 100644 index 0000000000..d7244d6023 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt @@ -0,0 +1,12 @@ +package com.domain.blockaid.models.transaction + +/** + * Result of BlockAid's transaction check + * + * @property validation Indicates whether the transaction is considered safe or unsafe + * @property simulation Provides insight into the expected outcome of the transaction + */ +data class CheckTransactionResult( + val validation: ValidationResult, + val simulation: SimulationResult, +) \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/SimulationResult.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/SimulationResult.kt new file mode 100644 index 0000000000..c44441a922 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/SimulationResult.kt @@ -0,0 +1,21 @@ +package com.domain.blockaid.models.transaction + +import com.domain.blockaid.models.transaction.simultation.SimulationData + +/** + * Result of BlockAid's transaction simulation + */ +sealed class SimulationResult { + + /** + * Simulation was successfully performed and returned data + */ + data class Success( + val data: SimulationData, + ) : SimulationResult() + + /** + * Simulation wasn't performed, BlockAid cannot guarantee transaction's behavior + */ + data object FailedToSimulate : SimulationResult() +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt new file mode 100644 index 0000000000..01fb3a9321 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt @@ -0,0 +1,36 @@ +package com.domain.blockaid.models.transaction + +/** + * Input data for BlockAid's transaction check + * + * @property chain Chain name, for ex. "ethereum" + * @property accountAddress The address of the account (wallet) received the request in hex string format + * @property method Transaction method, for ex. "eth_signTransaction" + * @property domainUrl Url of the DApp domain + */ +data class TransactionData( + val chain: String, + val accountAddress: String, + val method: String, + val domainUrl: String, + val params: TransactionParams, +) + +sealed class TransactionParams { + + /** + * Parameters for Ethereum based transactions, can be taken from [WcSdkSessionRequest.JSONRPCRequest.params] + */ + data class Evm( + val params: String, + ) : TransactionParams() + + /** + * Parameters for Solana transactions + * + * @property transactions Base64-encoded serialized list of transactions + */ + data class Solana( + val transactions: List, + ) : TransactionParams() +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/ValidationResult.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/ValidationResult.kt new file mode 100644 index 0000000000..0adde02aa8 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/ValidationResult.kt @@ -0,0 +1,22 @@ +package com.domain.blockaid.models.transaction + +/** + * Result of BlockAid's transaction validation + */ +enum class ValidationResult { + + /** + * Transaction was confirmed safe + */ + SAFE, + + /** + * Transaction was confirmed unsafe + */ + UNSAFE, + + /** + * Validation wasn't performed, BlockAid cannot guarantee transaction's safety + */ + FAILED_TO_VALIDATE, +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/AmountInfo.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/AmountInfo.kt new file mode 100644 index 0000000000..14e8b4c6d6 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/AmountInfo.kt @@ -0,0 +1,8 @@ +package com.domain.blockaid.models.transaction.simultation + +import java.math.BigDecimal + +data class AmountInfo( + val amount: BigDecimal, + val token: TokenInfo, +) \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt new file mode 100644 index 0000000000..6fdad58ec2 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt @@ -0,0 +1,9 @@ +package com.domain.blockaid.models.transaction.simultation + +import java.math.BigDecimal + +data class ApprovedAmount( + val approvedAmount: BigDecimal, + val isUnlimited: Boolean, + val tokenInfo: TokenInfo, +) \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt new file mode 100644 index 0000000000..afd8fea9e9 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt @@ -0,0 +1,22 @@ +package com.domain.blockaid.models.transaction.simultation + +/** + * Result of a successful transaction simulation. + */ +sealed class SimulationData { + + /** + * Represents a swap/send/sell operations with specified send and receive amounts (can be multiple amounts for NFT) + */ + data class SendAndReceive( + val send: List, + val receive: List, + ) : SimulationData() + + /** + * Represents an approve operation with the specified amount (can be multiple amounts for NFT) + */ + data class Approve( + val approvedAmounts: List, + ) : SimulationData() +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt new file mode 100644 index 0000000000..889bcf1d1e --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/TokenInfo.kt @@ -0,0 +1,7 @@ +package com.domain.blockaid.models.transaction.simultation + +data class TokenInfo( + val chainId: Int?, + val logoUrl: String?, + val symbol: String, +) \ No newline at end of file diff --git a/domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidVerifier.kt b/domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidVerifier.kt new file mode 100644 index 0000000000..e41f57e783 --- /dev/null +++ b/domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidVerifier.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.blockaid + +import arrow.core.Either +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData + +/** + * Verifies the safety of DApps and WalletConnect transactions + */ +interface BlockAidVerifier { + + /** + * Checks if a DApp is safe to use + */ + suspend fun verifyDApp(data: DAppData): Either + + /** + * Checks the safety of a WalletConnect transaction and provides a simulation result + */ + suspend fun verifyTransaction(data: TransactionData): Either +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3f7384a646..01e4240001 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -289,6 +289,8 @@ include(":domain:nft") include(":domain:nft:models") include(":domain:networks") include(":domain:quotes") +include(":domain:blockaid") +include(":domain:blockaid:models") // endregion Domain modules // region Data modules @@ -316,4 +318,5 @@ include(":data:networks") include(":data:nft") include(":data:onramp") include(":data:quotes") +include(":data:blockaid") // endregion Data modules \ No newline at end of file From 12d448340d714a3e1dad09b336ae44b49674efe5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Apr 2025 12:11:08 +0300 Subject: [PATCH 68/72] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 29f73d909e..7a607593e3 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 29f73d909e14cb3760118b197b1929a6088bc40e +Subproject commit 7a607593e34f0977474613f077532215eaf0ffe6 From 41dcdeb66d0eaeaa7618b8b037f98987962b0bb8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Apr 2025 14:31:30 +0500 Subject: [PATCH 69/72] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 14 + .../main/res/drawable/ic_connect_new_24.xml | 16 + .../src/main/res/drawable/ic_select_18_24.xml | 18 + .../main/res/drawable/ic_wallet_new_24.xml | 17 + features/walletconnect/impl/build.gradle.kts | 1 + .../components/WcAppInfoComponent.kt | 31 ++ .../connections/entity/WcAppInfoUM.kt | 25 + .../connections/ui/InternalComponents.kt | 69 +++ .../connections/ui/WcAppInfoContent.kt | 447 ++++++++++++++++++ 9 files changed, 638 insertions(+) create mode 100644 core/ui/src/main/res/drawable/ic_connect_new_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_select_18_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_wallet_new_24.xml create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 7a0bd7ff8e..0e158723f2 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -504,6 +504,7 @@ Pull this up or tap the search bar to add tokens directly from the market Add tokens NFC is not available on your device + About NFT %d item %d items @@ -1251,13 +1252,26 @@ Verified domain Wrong card or ring selected in Tangem App We\'ve got some kind of problem + Connect + Network + Networks + Wallet + View your wallet balance and activity + Sign transactions without your notice + Request approval for transactions + Will not be able to + Would like to + Connection request Connections Disconnect all Text about discnected all dApps Disconect All dApps + Add the %s network to your profile for this wallet + The wallet has no required networks New connection Connect your wallet to a different dApps No sessions + Wallet connect Discard You have an interrupted backup. Do you want to resume? Yes, resume diff --git a/core/ui/src/main/res/drawable/ic_connect_new_24.xml b/core/ui/src/main/res/drawable/ic_connect_new_24.xml new file mode 100644 index 0000000000..523f1a7759 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_connect_new_24.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_select_18_24.xml b/core/ui/src/main/res/drawable/ic_select_18_24.xml new file mode 100644 index 0000000000..5a131c0a6a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_select_18_24.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_wallet_new_24.xml b/core/ui/src/main/res/drawable/ic_wallet_new_24.xml new file mode 100644 index 0000000000..5add836f95 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_wallet_new_24.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index e85bbfc291..841499063e 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) implementation(projects.common.routing) + implementation(projects.common.ui) /** Domain models */ implementation(projects.domain.qrScanning.models) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoComponent.kt new file mode 100644 index 0000000000..bba2e61463 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoComponent.kt @@ -0,0 +1,31 @@ +package com.tangem.features.walletconnect.connections.components + +import androidx.compose.runtime.Composable +import com.tangem.core.decompose.context.AppComponentContext +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.features.walletconnect.connections.ui.WcAppInfoModalBottomSheet + +internal class WcAppInfoComponent( + private val appComponentContext: AppComponentContext, + private val params: Params, +) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + WcAppInfoModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, // todo(wc) will be fixed in next PR's + ), + ) + } + + data class Params(val onDismiss: () -> Unit) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt new file mode 100644 index 0000000000..a588d1ddd0 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -0,0 +1,25 @@ +package com.tangem.features.walletconnect.connections.entity + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal data class WcAppInfoUM( + val appName: String, + val appIcon: String, + val isVerified: Boolean, + val appSubtitle: String, + val notificationUM: NotificationUM?, + val walletName: String, + val networksInfo: WcNetworksInfo, + val onDismiss: () -> Unit, + val onConnect: () -> Unit, +) : TangemBottomSheetConfigContent + +@Immutable +internal sealed class WcNetworksInfo { + data class MissingRequiredNetworkInfo(val network: String) : WcNetworksInfo() + data class ContainsAllRequiredNetworks(val icons: ImmutableList) : WcNetworksInfo() +} \ No newline at end of file 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 new file mode 100644 index 0000000000..ca17e5f44f --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt @@ -0,0 +1,69 @@ +package com.tangem.features.walletconnect.connections.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.impl.R + +@Composable +internal fun WcAppInfoItem( + iconUrl: String, + title: String, + subtitle: String, + isVerified: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.padding(TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + AsyncImage( + modifier = Modifier + .size(TangemTheme.dimens.size48) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .background(Color.Black), + model = iconUrl, + contentDescription = title, + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f, fill = false), + text = title, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h3, + ) + if (isVerified) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.img_approvale2_20), + contentDescription = null, + tint = Color.Unspecified, + ) + } + } + Text( + text = subtitle, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + ) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt new file mode 100644 index 0000000000..5ce783ebb7 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt @@ -0,0 +1,447 @@ +package com.tangem.features.walletconnect.connections.ui + +import android.content.res.Configuration +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +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.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +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.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +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.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo +import com.tangem.features.walletconnect.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun WcAppInfoModalBottomSheet(config: TangemBottomSheetConfig) { + TangemModalBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + title = { state -> + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wc_wallet_connect), + endIconRes = R.drawable.ic_close_24, + onEndClick = state.onDismiss, + ) + }, + content = { state -> WcAppInfoModalBottomSheetContent(state) }, + ) +} + +@Composable +private fun WcAppInfoModalBottomSheetContent(state: WcAppInfoUM) { + Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { + val blocksModifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + WcAppInfoFirstBlock(modifier = blocksModifier, state = state) + state.notificationUM?.config?.let { config -> + Notification( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing14), + config = config, + iconTint = TangemTheme.colors.icon.attention, + ) + } + WcAppInfoSecondBlock( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing14) + .then(blocksModifier), + state = state, + ) + WcAppInfoButtons( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), + onDismiss = state.onDismiss, + onConnect = state.onConnect, + ) + } +} + +@Composable +private fun WcAppInfoFirstBlock(state: WcAppInfoUM, modifier: Modifier = Modifier) { + var connectionRequestExpanded by remember { mutableStateOf(false) } + Column(modifier = modifier.animateContentSize()) { + WcAppInfoItem( + iconUrl = state.appIcon, + title = state.appName, + subtitle = state.appSubtitle, + isVerified = state.isVerified, + ) + HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) + ConnectionRequestBlock( + modifier = Modifier + .fillMaxWidth() + .clickable { connectionRequestExpanded = !connectionRequestExpanded } + .padding(TangemTheme.dimens.spacing12), + expanded = connectionRequestExpanded, + ) + if (connectionRequestExpanded) { + ConnectionRequestDescription( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing12) + .padding(top = 8.dp, bottom = 12.dp), + ) + } + } +} + +@Composable +private fun ConnectionRequestBlock(expanded: Boolean, modifier: Modifier = Modifier) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_connect_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4) + .weight(1f), + text = stringResourceSafe(R.string.wc_connection_request), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .size(width = 18.dp, height = 24.dp), + painter = painterResource(if (expanded) R.drawable.ic_chevron_up_24 else R.drawable.ic_chevron_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Composable +private fun ConnectionRequestDescription(modifier: Modifier = Modifier) { + Column(modifier = modifier) { + Text( + text = stringResourceSafe(R.string.wc_connection_reqeust_would_like), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + ConnectionRequestDescriptionRow( + iconPainter = painterResource(R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + text = stringResourceSafe(R.string.wc_connection_reqeust_can_view_balance), + ) + ConnectionRequestDescriptionRow( + iconPainter = painterResource(R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + text = stringResourceSafe(R.string.wc_connection_reqeust_request_approval), + ) + HorizontalDivider( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + thickness = 0.5.dp, + color = TangemTheme.colors.stroke.primary, + ) + Text( + modifier = Modifier.padding(top = 12.dp), + text = stringResourceSafe(R.string.wc_connection_reqeust_will_not), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + ConnectionRequestDescriptionRow( + iconPainter = painterResource(R.drawable.ic_close_24), + tint = TangemTheme.colors.icon.warning, + text = stringResourceSafe(R.string.wc_connection_reqeust_cant_sign), + ) + } +} + +@Composable +private fun ConnectionRequestDescriptionRow( + iconPainter: Painter, + tint: Color, + text: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = iconPainter, + contentDescription = null, + tint = tint, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = text, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Composable +private fun WcAppInfoSecondBlock(state: WcAppInfoUM, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + val itemsModifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12) + WalletRowItem(modifier = itemsModifier, walletName = state.walletName) + HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) + SelectNetworksBlock( + networksInfo = state.networksInfo, + modifier = itemsModifier, + ) + when (state.networksInfo) { + is WcNetworksInfo.ContainsAllRequiredNetworks -> Unit + is WcNetworksInfo.MissingRequiredNetworkInfo -> { + HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) + Notification( + config = NotificationConfig( + iconResId = R.drawable.ic_alert_circle_24, + title = resourceReference(R.string.wc_missing_required_network_title), + subtitle = resourceReference( + R.string.wc_missing_required_network_description, + wrappedList(state.networksInfo.network), + ), + ), + iconTint = TangemTheme.colors.icon.attention, + containerColor = TangemTheme.colors.background.action, + ) + } + } + } +} + +@Composable +private fun WalletRowItem(walletName: String, modifier: Modifier = Modifier) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_wallet_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4) + .weight(1f), + text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing16) + .weight(1f), + text = walletName, + textAlign = TextAlign.End, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .size(width = 18.dp, height = 24.dp), + painter = painterResource(R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Composable +private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_network_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4) + .weight(1f), + text = stringResourceSafe(R.string.wc_common_networks), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + when (networksInfo) { + is WcNetworksInfo.ContainsAllRequiredNetworks -> NetworkIcons(icons = networksInfo.icons) + is WcNetworksInfo.MissingRequiredNetworkInfo -> Unit + } + 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 + }, + ), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun NetworkIcons(icons: ImmutableList, modifier: Modifier = Modifier) { + val (iconsToShow, remainingCount) = remember(icons) { + when { + icons.size <= 4 -> icons to 0 + else -> icons.take(3) to icons.size - 3 + } + } + Box(modifier = modifier.wrapContentWidth()) { + iconsToShow.forEachIndexed { index, iconRes -> + Image( + painter = painterResource(id = iconRes), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing16.times(index)) + .border( + width = 2.dp, + color = TangemTheme.colors.background.action, + shape = CircleShape, + ) + .padding(2.dp) + .clip(CircleShape) + .size(20.dp), + ) + } + if (remainingCount > 0) { + Box( + modifier = modifier + .padding(start = 48.dp) + .border( + width = 2.dp, + color = TangemTheme.colors.background.action, + shape = CircleShape, + ) + .padding(2.dp) + .background(color = TangemTheme.colors.background.action) + .size(20.dp) + .clip(CircleShape) + .background(color = TangemTheme.colors.icon.primary1.copy(alpha = 0.1F)), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = "+$remainingCount", + style = TangemTheme.typography.overline, + color = TangemTheme.colors.text.secondary, + ) + } + } + } +} + +@Composable +private fun WcAppInfoButtons(onDismiss: () -> Unit, onConnect: () -> Unit, modifier: Modifier = Modifier) { + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + SecondaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = stringResourceSafe(R.string.common_cancel), + onClick = onDismiss, + ) + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = stringResourceSafe(R.string.wc_common_connect), + onClick = onConnect, + ) + } +} + +@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 WcAppInfoBottomSheetPreview(@PreviewParameter(WcAppInfoStateProvider::class) state: WcAppInfoUM) { + TangemThemePreview { + WcAppInfoModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = state, + ), + ) + } +} + +private class WcAppInfoStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + WcAppInfoUM( + appName = "React App", + appIcon = "", + isVerified = true, + appSubtitle = "react-app.walletconnect.com", + notificationUM = null, + walletName = "Tangem 2.0", + networksInfo = WcNetworksInfo.ContainsAllRequiredNetworks( + icons = persistentListOf( + R.drawable.img_optimism_22, + R.drawable.img_bsc_22, + R.drawable.img_avalanche_22, + R.drawable.img_solana_22, + R.drawable.img_avalanche_22, + ), + ), + onDismiss = {}, + onConnect = {}, + ), + WcAppInfoUM( + appName = "React App", + appIcon = "", + isVerified = false, + appSubtitle = "react-app.walletconnect.com", + notificationUM = NotificationUM.Info( + title = resourceReference(R.string.wc_alert_audit_unknown_domain), + subtitle = resourceReference(R.string.wc_alert_domain_issues_description), + ), + walletName = "Tangem 2.0", + networksInfo = WcNetworksInfo.MissingRequiredNetworkInfo(network = "Solana"), + onDismiss = {}, + onConnect = {}, + ), + ), +) \ No newline at end of file From 514d6f5e177a20acd2fca8017f6ebd449123b1e0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Apr 2025 12:24:51 +0500 Subject: [PATCH 70/72] Updated on 2026-08-14 --- .../state/converters/BalanceItemConverter.kt | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index ac9f4c0942..da6dc0fcff 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -2,23 +2,28 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.core.ui.extensions.* 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.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.BalanceType.Companion.isClickable import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.staking.utils.getRewardStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.isTon import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.toPersistentList import org.joda.time.DateTime +import java.math.BigDecimal import java.util.Calendar internal class BalanceItemConverter( @@ -34,8 +39,7 @@ internal class BalanceItemConverter( val validator = yield.validators.firstOrNull { value.validatorAddress?.contains(it.address, ignoreCase = true) == true } - - val cryptoAmount = value.amount + val cryptoAmount = value.getBalanceValue() val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) val title = value.type.getTitle(validator?.name) @@ -53,11 +57,12 @@ internal class BalanceItemConverter( ), fiatAmount = fiatAmount, formattedFiatAmount = stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), + fiatAmount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, ), rawCurrencyId = value.rawCurrencyId, pendingActions = value.pendingActions.toPersistentList(), @@ -67,6 +72,18 @@ internal class BalanceItemConverter( } } + private fun BalanceItem.getBalanceValue(): BigDecimal { + val isIncludeStakingTotalBalance = BlockchainUtils.isIncludeStakingTotalBalance( + blockchainId = cryptoCurrencyStatus.currency.network.id.value, + ) + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data + return if (isIncludeStakingTotalBalance) { + amount + } else { + amount - yieldBalance?.getRewardStakingBalance().orZero() + } + } + private fun BalanceItem.isClickable(): Boolean { val networkId = cryptoCurrencyStatus.currency.network.id.value return when { From d0b078a793469bc9a0b6169951b4f2be8fa8d171 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 14 Apr 2025 12:24:12 +0500 Subject: [PATCH 71/72] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 7 + .../tangem/data/nft/DefaultNFTRepository.kt | 9 +- .../DefaultWalletManagersFacade.kt | 5 + .../walletmanager/WalletManagersFacade.kt | 2 + .../domain/nft/GetNFTExploreUrlUseCase.kt | 21 +++ .../domain/nft/repository/NFTRepository.kt | 3 + features/nft/impl/build.gradle.kts | 1 + .../nft/common/DefaultNFTComponent.kt | 3 + .../nft/details/NFTDetailsComponent.kt | 33 +++++ .../features/nft/details/entity/NFTAssetUM.kt | 10 +- .../entity/NFTDetailsBottomSheetConfig.kt | 12 ++ .../nft/details/entity/NFTDetailsUM.kt | 3 - .../entity/NFTInfoBottomSheetConfig.kt | 9 -- .../entity/factory/NFTDetailsUMFactory.kt | 124 ++++++++++++++++++ .../info/DefaultNFTDetailsInfoComponent.kt | 54 ++++++++ .../details/info/NFTDetailsInfoComponent.kt | 15 +++ .../nft/details/info/ui/NFTInfoBottomSheet.kt | 69 ++++++++++ .../nft/details/model/NFTDetailsModel.kt | 61 ++++++--- .../features/nft/details/ui/NFTDetails.kt | 12 -- .../nft/details/ui/NFTDetailsAsset.kt | 10 +- .../nft/details/ui/NFTDetailsInfoGroup.kt | 92 ++++++++++--- .../ui/bottomsheet/NFTInfoBottomSheet.kt | 73 ----------- .../features/nft/di/NFTFeatureModule.kt | 8 ++ 23 files changed, 495 insertions(+), 141 deletions(-) create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTExploreUrlUseCase.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsBottomSheetConfig.kt delete mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/DefaultNFTDetailsInfoComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/NFTDetailsInfoComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/ui/NFTInfoBottomSheet.kt delete mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/bottomsheet/NFTInfoBottomSheet.kt 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 a8cb6b030d..de24b448ec 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 @@ -66,4 +66,11 @@ internal object NFTDomainModule { GetNFTNetworkStatusUseCase( networksRepository = networksRepository, ) + + @Provides + @Singleton + fun providesGetNFTExploreUrlUseCase(nftRepository: NFTRepository): GetNFTExploreUrlUseCase = + GetNFTExploreUrlUseCase( + nftRepository = nftRepository, + ) } \ No newline at end of file diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 197249ec85..7b806c688a 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -11,6 +11,7 @@ import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter import com.tangem.domain.models.StatusSource +import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.NFTSalePrice @@ -117,7 +118,7 @@ internal class DefaultNFTRepository @Inject constructor( assets.forEach { val assetId = NFTSdkAssetIdentifierConverter.convert(it.identifier) val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId) - if (price is NFTSalePrice.Error) { + if (price is NFTSalePrice.Empty || price is NFTSalePrice.Error) { refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier) } } @@ -155,6 +156,12 @@ internal class DefaultNFTRepository @Inject constructor( override suspend fun isNFTSupported(network: Network): Boolean = network.canHandleNFTs() + override suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? = + walletManagersFacade.getNFTExploreUrl( + network = network, + assetIdentifier = NFTSdkAssetIdentifierConverter.convertBack(assetIdentifier), + ) + private suspend fun refreshSalePrice( userWalletId: UserWalletId, network: Network, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 1e16b212a1..ba14ca68b2 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -709,6 +709,11 @@ class DefaultWalletManagersFacade( return walletManager.getSalePrice(collectionIdentifier, assetIdentifier) } + override suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? { + val blockchain = Blockchain.fromId(network.id.value) + return blockchain.getNFTExploreUrl(assetIdentifier) + } + override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean { val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network) val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return true diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index b40c497186..45a93fff76 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -273,6 +273,8 @@ interface WalletManagersFacade { assetIdentifier: NFTAsset.Identifier, ): NFTAsset.SalePrice? + suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? + /** * If wallet manager implements [InitializableAccount] then returns [InitializableAccount.isAccountInitialized] * value. Otherwise always return true diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTExploreUrlUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTExploreUrlUseCase.kt new file mode 100644 index 0000000000..d6794fd7fe --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTExploreUrlUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.nft + +import arrow.core.raise.catch +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.model.Network + +class GetNFTExploreUrlUseCase( + private val nftRepository: NFTRepository, +) { + + suspend operator fun invoke(network: Network, assetIdentifier: NFTAsset.Identifier): String? = catch( + block = { + nftRepository.getNFTExploreUrl( + network = network, + assetIdentifier = assetIdentifier, + ) + }, + catch = { null }, + ) +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt index 5d811e9a39..36ca075957 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt @@ -1,5 +1,6 @@ package com.tangem.domain.nft.repository +import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.tokens.model.Network @@ -14,4 +15,6 @@ interface NFTRepository { suspend fun refreshAssets(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier) suspend fun isNFTSupported(network: Network): Boolean + + suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? } \ No newline at end of file diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 7aba5590b7..df09cb6d6a 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.core.datasource) /** Domain modules */ + implementation(projects.domain.appCurrency.models) implementation(projects.domain.models) implementation(projects.domain.nft) implementation(projects.domain.nft.models) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt index 1c85ec5d39..95e4a2ad8f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt @@ -18,6 +18,7 @@ import com.tangem.features.nft.collections.NFTCollectionsComponent import com.tangem.features.nft.common.ui.NFTContent import com.tangem.features.nft.component.NFTComponent import com.tangem.features.nft.details.NFTDetailsComponent +import com.tangem.features.nft.details.info.NFTDetailsInfoComponent import com.tangem.features.nft.receive.NFTReceiveComponent import com.tangem.features.nft.traits.NFTAssetTraitsComponent import dagger.assisted.Assisted @@ -29,6 +30,7 @@ import kotlinx.coroutines.launch internal class DefaultNFTComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: NFTComponent.Params, + private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory, ) : NFTComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -142,6 +144,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( ) }, ), + nftDetailsInfoComponentFactory = nftDetailsInfoComponentFactory, ) private fun getAssetTraitsComponent( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt index 274d87d25e..2333a6a0e0 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/NFTDetailsComponent.kt @@ -4,11 +4,19 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.nft.details.entity.NFTDetailsBottomSheetConfig +import com.tangem.features.nft.details.info.NFTDetailsInfoComponent import com.tangem.features.nft.details.model.NFTDetailsModel import com.tangem.features.nft.details.ui.NFTDetails import dagger.assisted.Assisted @@ -17,15 +25,40 @@ import dagger.assisted.AssistedInject internal class NFTDetailsComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: Params, + private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory, ) : ComposableContentComponent, AppComponentContext by context { private val model: NFTDetailsModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() NFTDetails(state, modifier) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: NFTDetailsBottomSheetConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is NFTDetailsBottomSheetConfig.Info -> nftDetailsInfoComponentFactory.create( + context = childByContext(componentContext), + params = NFTDetailsInfoComponent.Params( + text = config.text, + onDismiss = { + model.bottomSheetNavigation.dismiss() + }, + ), + ) } data class Params( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt index 2b85b58fe8..22243c075c 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt @@ -3,7 +3,9 @@ package com.tangem.features.nft.details.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal data class NFTAssetUM( val name: String, @@ -37,7 +39,13 @@ data class NFTAssetUM( sealed class SalePrice { data object Loading : SalePrice() data object Empty : SalePrice() - data class Content(val value: String, val fiatValue: String) : SalePrice() + data class Content( + val value: BigDecimal, + val symbol: String, + val decimals: Int, + val rate: BigDecimal?, + val appCurrency: AppCurrency, + ) : SalePrice() } @Immutable diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsBottomSheetConfig.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsBottomSheetConfig.kt new file mode 100644 index 0000000000..e8478f38ac --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsBottomSheetConfig.kt @@ -0,0 +1,12 @@ +package com.tangem.features.nft.details.entity + +import com.tangem.core.ui.extensions.TextReference +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface NFTDetailsBottomSheetConfig { + @Serializable + data class Info( + val text: TextReference, + ) : NFTDetailsBottomSheetConfig +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt index 67955452f9..95844969ae 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt @@ -1,7 +1,5 @@ package com.tangem.features.nft.details.entity -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig - internal data class NFTDetailsUM( val nftAsset: NFTAssetUM, val onBackClick: () -> Unit, @@ -9,5 +7,4 @@ internal data class NFTDetailsUM( val onSeeAllTraitsClick: () -> Unit, val onExploreClick: () -> Unit, val onSendClick: () -> Unit, - val bottomSheetConfig: TangemBottomSheetConfig?, ) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt deleted file mode 100644 index 3cb7f5273c..0000000000 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTInfoBottomSheetConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.features.nft.details.entity - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference - -internal data class NFTInfoBottomSheetConfig( - val title: TextReference, - val text: TextReference, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt new file mode 100644 index 0000000000..faf5d73b1d --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt @@ -0,0 +1,124 @@ +package com.tangem.features.nft.details.entity.factory + +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.nft.models.NFTSalePrice +import com.tangem.features.nft.details.entity.NFTAssetUM +import com.tangem.features.nft.details.entity.NFTDetailsUM +import com.tangem.features.nft.impl.R +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class NFTDetailsUMFactory( + private val onBackClick: () -> Unit, + private val onReadMoreClick: () -> Unit, + private val onSeeAllTraitsClick: () -> Unit, + private val onExploreClick: () -> Unit, + private val onSendClick: () -> Unit, +) { + + fun getInitialState(nftAsset: NFTAsset): NFTDetailsUM = NFTDetailsUM( + nftAsset = nftAsset.transform(), + onBackClick = onBackClick, + onReadMoreClick = onReadMoreClick, + onSeeAllTraitsClick = onSeeAllTraitsClick, + onExploreClick = onExploreClick, + onSendClick = onSendClick, + ) + + private fun NFTAsset.transform(): NFTAssetUM = NFTAssetUM( + name = name.orEmpty(), + media = media?.let { + NFTAssetUM.Media.Content( + mimetype = it.mimetype, + url = it.url, + ) + } ?: NFTAssetUM.Media.Empty, + topInfo = when { + salePrice is NFTSalePrice.Empty && description.isNullOrEmpty() && rarity == null -> { + NFTAssetUM.TopInfo.Empty + } + else -> { + val hasSalePrice = salePrice !is NFTSalePrice.Empty + val hasDescription = !description.isNullOrEmpty() + val rarity = rarity + NFTAssetUM.TopInfo.Content( + title = if (hasSalePrice) { + resourceReference(R.string.nft_details_last_sale_price) + } else { + null + }, + salePrice = NFTAssetUM.SalePrice.Empty, + description = description, + rarity = if (rarity != null) { + NFTAssetUM.Rarity.Content( + rank = rarity.rank, + label = rarity.label, + showDivider = hasSalePrice || hasDescription, + ) + } else { + NFTAssetUM.Rarity.Empty + }, + ) + } + }, + traits = traits.take(MAX_TRAITS_COUNT).map { + NFTAssetUM.BlockItem( + title = stringReference(it.name), + value = it.value, + ) + }.toImmutableList(), + baseInfoItems = buildBaseInfoItems(), + ) + + private fun NFTAsset.buildBaseInfoItems() = when (val id = id) { + is NFTAsset.Identifier.EVM -> persistentListOf( + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_token_standard), + value = contractType, + ), + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_contract_address), + value = id.tokenAddress, + valueTextEllipsis = TextEllipsis.Middle, + ), + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_token_id), + value = id.tokenId, + ), + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_chain), + value = network.name, + ), + ) + is NFTAsset.Identifier.TON -> persistentListOf( + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_token_address), + value = id.tokenAddress, + valueTextEllipsis = TextEllipsis.Middle, + ), + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_chain), + value = network.name, + ), + ) + is NFTAsset.Identifier.Solana -> persistentListOf( + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_token_address), + value = id.tokenAddress, + valueTextEllipsis = TextEllipsis.Middle, + ), + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_chain), + value = network.name, + ), + ) + is NFTAsset.Identifier.Unknown -> persistentListOf() + } + + companion object { + private const val MAX_TRAITS_COUNT = 6 + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/DefaultNFTDetailsInfoComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/DefaultNFTDetailsInfoComponent.kt new file mode 100644 index 0000000000..26dc1a1dd6 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/DefaultNFTDetailsInfoComponent.kt @@ -0,0 +1,54 @@ +package com.tangem.features.nft.details.info + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.nft.details.info.ui.NFTInfoBottomSheet +import com.tangem.features.nft.details.info.ui.NFTInfoBottomSheetContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultNFTDetailsInfoComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: NFTDetailsInfoComponent.Params, +) : NFTDetailsInfoComponent, AppComponentContext by context { + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val bottomSheetConfig = remember(key1 = this) { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + NFTInfoBottomSheet( + config = bottomSheetConfig, + content = { + NFTInfoBottomSheetContent( + text = params.text, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + }, + ) + } + + @AssistedFactory + interface Factory : NFTDetailsInfoComponent.Factory { + override fun create( + context: AppComponentContext, + params: NFTDetailsInfoComponent.Params, + ): DefaultNFTDetailsInfoComponent + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/NFTDetailsInfoComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/NFTDetailsInfoComponent.kt new file mode 100644 index 0000000000..90650da63b --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/NFTDetailsInfoComponent.kt @@ -0,0 +1,15 @@ +package com.tangem.features.nft.details.info + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.TextReference + +internal interface NFTDetailsInfoComponent : ComposableBottomSheetComponent { + + data class Params( + val text: TextReference, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/ui/NFTInfoBottomSheet.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/ui/NFTInfoBottomSheet.kt new file mode 100644 index 0000000000..b95179836f --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/info/ui/NFTInfoBottomSheet.kt @@ -0,0 +1,69 @@ +package com.tangem.features.nft.details.info.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +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.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +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.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.impl.R + +@Composable +internal fun NFTInfoBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { + TangemBottomSheet( + config = config, + titleText = resourceReference(R.string.nft_about_title), + content = { content() }, + ) +} + +@Composable +internal fun NFTInfoBottomSheetContent(text: TextReference, modifier: Modifier = Modifier) { + val scrollState = rememberScrollState() + Column( + modifier = modifier.verticalScroll(scrollState), + ) { + Text( + text = text.resolveReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) + } +} + +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_NFTInfoBottomSheetContent() { + TangemThemePreview { + NFTInfoBottomSheetContent( + text = stringReference( + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius neque vel ligula + tincidunt, nec faucibus nulla ultricies. Maecenas euismod arcu in nunc volutpat, + at bibendum eros lacinia. Proin hendrerit massa non velit congue, + in volutpat nisi consequat. Sed vitae justo nec orci tincidunt malesuada. + Nullam feugiat purus vel lectus efficitur, vel fringilla urna volutpat. + Donec sagittis enim in metus lacinia, vel tempor nunc bibendum. + """.trimIndent(), + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index 918cfd4ab6..e81bb43891 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.nft.details.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -7,13 +9,18 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.domain.nft.analytics.NFTAnalyticsEvent +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.nft.GetNFTExploreUrlUseCase import com.tangem.features.nft.details.NFTDetailsComponent import com.tangem.features.nft.details.entity.NFTAssetUM +import com.tangem.features.nft.details.entity.NFTDetailsBottomSheetConfig import com.tangem.features.nft.details.entity.NFTDetailsUM +import com.tangem.features.nft.details.entity.factory.NFTDetailsUMFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @ModelScoped @@ -21,45 +28,57 @@ internal class NFTDetailsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val analyticsEventHandler: AnalyticsEventHandler, + private val urlOpener: UrlOpener, + private val getNFTExploreUrlUseCase: GetNFTExploreUrlUseCase, paramsContainer: ParamsContainer, ) : Model() { - @Suppress("UnusedPrivateMember") private val params: NFTDetailsComponent.Params = paramsContainer.require() val state: StateFlow get() = _state - private val _state = MutableStateFlow( - value = NFTDetailsUM( - nftAsset = NFTAssetUM( - name = "", - media = NFTAssetUM.Media.Empty, - topInfo = NFTAssetUM.TopInfo.Empty, - traits = persistentListOf(), - baseInfoItems = persistentListOf(), - ), - onBackClick = params.onBackClick, - onReadMoreClick = ::onReadMoreClick, - onSeeAllTraitsClick = { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSeeAll) - params.onAllTraitsClick() - }, - onExploreClick = ::onExploreClick, - onSendClick = ::onSendClick, - bottomSheetConfig = null, - ), + private val stateFactory: NFTDetailsUMFactory = NFTDetailsUMFactory( + onBackClick = { params.onBackClick() }, + onReadMoreClick = ::onReadMoreClick, + onSeeAllTraitsClick = { params.onAllTraitsClick() }, + onExploreClick = ::onExploreClick, + onSendClick = ::onSendClick, ) + private val _state = MutableStateFlow( + value = stateFactory.getInitialState(params.nftAsset), + ) + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { analyticsEventHandler.send(NFTAnalyticsEvent.Details.ScreenOpened(params.nftAsset.network.name)) } private fun onReadMoreClick() { analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore) + when (val topInfo = _state.value.nftAsset.topInfo) { + is NFTAssetUM.TopInfo.Empty -> Unit + is NFTAssetUM.TopInfo.Content -> { + bottomSheetNavigation.activate( + NFTDetailsBottomSheetConfig.Info( + text = stringReference(topInfo.description.orEmpty()), + ), + ) + } + } } private fun onExploreClick() { analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonExplore) + modelScope.launch { + val url = getNFTExploreUrlUseCase.invoke( + network = params.nftAsset.network, + assetIdentifier = params.nftAsset.id, + ) + if (url != null) { + urlOpener.openUrl(url) + } + } } private fun onSendClick() { diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt index 1fba75b717..b662151b8d 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt @@ -11,12 +11,9 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.nft.details.entity.NFTDetailsUM -import com.tangem.features.nft.details.entity.NFTInfoBottomSheetConfig -import com.tangem.features.nft.details.ui.bottomsheet.NFTInfoBottomSheet import com.tangem.features.nft.impl.R @Composable @@ -45,7 +42,6 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { modifier = Modifier .padding(innerPadding), ) - ShowBottomSheet(state.bottomSheetConfig) }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { @@ -58,12 +54,4 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { ) }, ) -} - -@Composable -fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { - if (bottomSheetConfig == null) return - when (bottomSheetConfig.content) { - is NFTInfoBottomSheetConfig -> NFTInfoBottomSheet(bottomSheetConfig) - } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt index b077835ec8..233c56d281 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt @@ -16,9 +16,11 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference 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.nft.details.entity.NFTAssetUM import com.tangem.features.nft.impl.R import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal @Composable internal fun NFTDetailsAsset( @@ -120,7 +122,13 @@ private class NFTAssetProvider : CollectionPreviewParameterProvider( ), topInfo = NFTAssetUM.TopInfo.Content( title = resourceReference(R.string.nft_details_last_sale_price), - salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"), + salePrice = NFTAssetUM.SalePrice.Content( + value = BigDecimal("18.75"), + symbol = "ETH", + decimals = 18, + rate = BigDecimal("1"), + appCurrency = AppCurrency.Default, + ), description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3), rarity = NFTAssetUM.Rarity.Content( rank = "Top 1% rarity", diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt index aa34278977..8f71b0c87c 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt @@ -3,24 +3,34 @@ package com.tangem.features.nft.details.ui import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier 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.common.ui.amountScreen.utils.getFiatString import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.ReadMoreText import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +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.nft.details.entity.NFTAssetUM import com.tangem.features.nft.impl.R +import java.math.BigDecimal + +private const val READ_MORE_MAX_LINES = 3 @Composable internal fun NFTDetailsInfoGroup( @@ -41,7 +51,10 @@ internal fun NFTDetailsInfoGroup( contentHorizontalPadding = 0.dp, ) { Column( - modifier = Modifier, + modifier = Modifier + .padding( + bottom = TangemTheme.dimens.spacing12, + ), ) { SalePrice(state.salePrice) Description(state.description, onReadMoreClick) @@ -68,7 +81,6 @@ private fun SalePrice(state: NFTAssetUM.SalePrice, modifier: Modifier = Modifier .padding( start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, ), ) { TextShimmer( @@ -92,19 +104,23 @@ private fun SalePrice(state: NFTAssetUM.SalePrice, modifier: Modifier = Modifier .padding( start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, ), verticalArrangement = Arrangement.SpaceAround, ) { Text( - text = state.value, + text = state.value.format { + crypto( + symbol = state.symbol, + decimals = state.decimals, + ) + }, style = TangemTheme.typography.head, color = TangemTheme.colors.text.primary1, ) Text( modifier = Modifier .padding(top = TangemTheme.dimens.spacing4), - text = state.fiatValue, + text = getFiatString(state.value, state.rate, state.appCurrency), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) @@ -117,18 +133,29 @@ private fun SalePrice(state: NFTAssetUM.SalePrice, modifier: Modifier = Modifier @Composable private fun Description(description: String?, onReadMoreClick: () -> Unit, modifier: Modifier = Modifier) { if (!description.isNullOrEmpty()) { - // TODO configure right way to display 3 rows and "Read more" label - EllipsisText( + val interactionSource = remember { MutableInteractionSource() } + ReadMoreText( modifier = modifier - .clickable { onReadMoreClick() } + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onReadMoreClick, + ) .padding( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, ), + expanded = false, text = description, style = TangemTheme.typography.body2.copy( color = TangemTheme.colors.text.tertiary, ), + readMoreText = resourceReference(R.string.common_read_more).resolveReference(), + readMoreMaxLines = READ_MORE_MAX_LINES, + readMoreStyle = TangemTheme.typography.body2.copy( + color = TangemTheme.colors.text.accent, + ).toSpanStyle(), ) } } @@ -141,7 +168,9 @@ private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) { if (state.showDivider) { HorizontalDivider( modifier = Modifier.padding( - horizontal = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing8, + end = TangemTheme.dimens.spacing12, ), color = TangemTheme.colors.stroke.primary, ) @@ -150,8 +179,9 @@ private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) { modifier = modifier .fillMaxWidth() .padding( - horizontal = TangemTheme.dimens.spacing6, - vertical = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing6, + top = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing6, ), ) { NFTDetailsGroupBlock( @@ -159,7 +189,6 @@ private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) { .weight(1f) .padding( horizontal = TangemTheme.dimens.spacing6, - vertical = TangemTheme.dimens.spacing4, ), title = resourceReference(R.string.nft_details_rarity_label), value = stringReference(state.label), @@ -169,7 +198,6 @@ private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) { .weight(1f) .padding( horizontal = TangemTheme.dimens.spacing6, - vertical = TangemTheme.dimens.spacing4, ), title = resourceReference(R.string.nft_details_rarity_rank), value = stringReference(state.rank), @@ -196,7 +224,13 @@ private class NFTAssetInfoProvider : CollectionPreviewParameterProvider( - config = config, - title = { content -> - TangemBottomSheetTitle(title = content.title) - }, - ) { content -> - Column( - modifier = Modifier.verticalScroll(scrollState), - ) { - Text( - text = content.text.resolveReference(), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - modifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) - } - } -} - -@Composable -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview_StakingInfoBottomSheet() { - TangemThemePreview { - NFTInfoBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = NFTInfoBottomSheetConfig( - title = stringReference("Title"), - text = stringReference( - """ - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius neque vel ligula - tincidunt, nec faucibus nulla ultricies. Maecenas euismod arcu in nunc volutpat, - at bibendum eros lacinia. Proin hendrerit massa non velit congue, - in volutpat nisi consequat. Sed vitae justo nec orci tincidunt malesuada. - Nullam feugiat purus vel lectus efficitur, vel fringilla urna volutpat. - Donec sagittis enim in metus lacinia, vel tempor nunc bibendum. - """.trimIndent(), - ), - ), - ), - ) - } -} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt index abc85dcd0a..9c3ac6fbb8 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt @@ -8,6 +8,8 @@ import com.tangem.features.nft.collections.model.NFTCollectionsModel import com.tangem.features.nft.common.DefaultNFTComponent import com.tangem.features.nft.component.* import com.tangem.features.nft.details.block.DefaultNFTDetailsBlockComponent +import com.tangem.features.nft.details.info.DefaultNFTDetailsInfoComponent +import com.tangem.features.nft.details.info.NFTDetailsInfoComponent import com.tangem.features.nft.details.model.NFTDetailsModel import com.tangem.features.nft.receive.model.NFTReceiveModel import com.tangem.features.nft.traits.model.NFTAssetTraitsModel @@ -34,6 +36,12 @@ internal object NFTFeatureModule { @Module @InstallIn(SingletonComponent::class) internal interface NFTFeatureModuleBinds { + @Binds + @Singleton + fun bindNFTDetailsInfoComponentFactory( + factory: DefaultNFTDetailsInfoComponent.Factory, + ): NFTDetailsInfoComponent.Factory + @Binds @Singleton fun bindNFTComponentFactory(impl: DefaultNFTComponent.Factory): NFTComponent.Factory From 5193b7ada4a8d4dca20842ca5ba973343ee6b821 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Apr 2025 18:19:05 +0500 Subject: [PATCH 72/72] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../features/nft/collections/entity/NFTCollectionUM.kt | 2 +- .../entity/transformer/UpdateDataStateTransformer.kt | 2 +- .../com/tangem/features/nft/collections/ui/NFTCollection.kt | 3 ++- .../nft/details/entity/factory/NFTDetailsUMFactory.kt | 6 ++++-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 0e158723f2..42b9c6762c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -534,6 +534,7 @@ Tap here to receive first NFT NFT collections Unable to load the data + No collection Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt index e7e24ded3b..771f9515fc 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.TextReference internal data class NFTCollectionUM( val id: String, - val name: String, + val name: String?, @DrawableRes val networkIconId: Int, val logoUrl: String?, val description: TextReference, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt index 234ba96ceb..a0cfab6956 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt @@ -83,7 +83,7 @@ internal class UpdateDataStateTransformer( NFTCollectionUM( id = it.id.toString(), networkIconId = getActiveIconRes(it.network.id.value), - name = it.name.orEmpty(), + name = it.name, description = TextReference.PluralRes( R.plurals.nft_collections_count, it.count, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt index e89b5bbe41..3851542d90 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider 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.nft.collections.entity.NFTCollectionAssetsListUM @@ -89,7 +90,7 @@ private fun RowScope.Text(state: NFTCollectionUM) { ) { Text( modifier = Modifier, - text = state.name, + text = state.name.takeUnless { it.isNullOrEmpty() } ?: stringResourceSafe(R.string.nft_no_collection), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.primary1, maxLines = 1, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt index faf5d73b1d..f23446ab57 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt @@ -37,11 +37,11 @@ internal class NFTDetailsUMFactory( ) } ?: NFTAssetUM.Media.Empty, topInfo = when { - salePrice is NFTSalePrice.Empty && description.isNullOrEmpty() && rarity == null -> { + !hasSalePrice() && description.isNullOrEmpty() && rarity == null -> { NFTAssetUM.TopInfo.Empty } else -> { - val hasSalePrice = salePrice !is NFTSalePrice.Empty + val hasSalePrice = hasSalePrice() val hasDescription = !description.isNullOrEmpty() val rarity = rarity NFTAssetUM.TopInfo.Content( @@ -73,6 +73,8 @@ internal class NFTDetailsUMFactory( baseInfoItems = buildBaseInfoItems(), ) + private fun NFTAsset.hasSalePrice() = salePrice !is NFTSalePrice.Empty && salePrice !is NFTSalePrice.Error + private fun NFTAsset.buildBaseInfoItems() = when (val id = id) { is NFTAsset.Identifier.EVM -> persistentListOf( NFTAssetUM.BlockItem(